mirror of https://github.com/Nheko-Reborn/nheko
commit
3a0d5788e1
@ -0,0 +1,334 @@ |
||||
// SPDX-FileCopyrightText: Nheko Contributors |
||||
// |
||||
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
||||
import "./components" |
||||
import "./delegates" |
||||
import "./emoji" |
||||
import "./ui" |
||||
import "./dialogs" |
||||
import Qt.labs.platform 1.1 as Platform |
||||
import QtQuick |
||||
import QtQuick.Controls |
||||
import QtQuick.Layouts |
||||
import QtQuick.Window |
||||
import im.nheko |
||||
|
||||
TimelineEvent { |
||||
id: wrapper |
||||
ListView.delayRemove: true |
||||
width: chat.delegateMaxWidth |
||||
height: Math.max((section.item?.height ?? 0) + gridContainer.implicitHeight + reactionRow.implicitHeight + unreadRow.height, 10) |
||||
anchors.horizontalCenter: ListView.view.contentItem.horizontalCenter |
||||
//room: chatRoot.roommodel |
||||
|
||||
required property var day |
||||
required property bool isSender |
||||
required property int index |
||||
property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Day) |
||||
property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index + 1, Room.IsStateEvent) |
||||
property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index + 1, Room.UserId) |
||||
|
||||
required property date timestamp |
||||
required property string userId |
||||
required property string userName |
||||
required property string threadId |
||||
required property int userPowerlevel |
||||
required property bool isEdited |
||||
required property bool isEncrypted |
||||
required property var reactions |
||||
required property int status |
||||
required property int trustlevel |
||||
required property int notificationlevel |
||||
required property int type |
||||
required property bool isEditable |
||||
|
||||
required property QtObject messageContextMenu |
||||
required property QtObject replyContextMenu |
||||
required property Item messageActions |
||||
|
||||
property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) // align bubble with section header |
||||
|
||||
property alias hovered: messageHover.hovered |
||||
property bool scrolledToThis: false |
||||
|
||||
mainInset: (threadId ? (4 + Nheko.paddingSmall) : 0) + 4 |
||||
replyInset: mainInset + 4 + Nheko.paddingSmall |
||||
|
||||
property int bubbleMargin: 40 |
||||
|
||||
maxWidth: chat.delegateMaxWidth - avatarMargin - bubbleMargin |
||||
|
||||
data: [ |
||||
Loader { |
||||
id: section |
||||
|
||||
active: wrapper.previousMessageUserId !== wrapper.userId || wrapper.previousMessageDay !== wrapper.day || wrapper.previousMessageIsStateEvent !== wrapper.isStateEvent |
||||
//asynchronous: true |
||||
sourceComponent: TimelineSectionHeader { |
||||
day: wrapper.day |
||||
isSender: wrapper.isSender |
||||
isStateEvent: wrapper.isStateEvent |
||||
parentWidth: wrapper.width |
||||
previousMessageDay: wrapper.previousMessageDay |
||||
previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent |
||||
previousMessageUserId: wrapper.previousMessageUserId |
||||
timestamp: wrapper.timestamp |
||||
userId: wrapper.userId |
||||
userName: wrapper.userName |
||||
userPowerlevel: wrapper.userPowerlevel |
||||
} |
||||
visible: status == Loader.Ready |
||||
z: 4 |
||||
}, |
||||
Rectangle { |
||||
anchors.fill: gridContainer |
||||
property color threadColor: TimelineManager.userColor(wrapper.threadId, palette.base) |
||||
property color threadBackgroundColor: wrapper.threadId ? Qt.tint(palette.base, Qt.hsla(threadColor.hslHue, 0.7, threadColor.hslLightness, 0.1)) : "transparent" |
||||
color: (Settings.messageHoverHighlight && messageHover.hovered) ? palette.alternateBase : threadBackgroundColor |
||||
|
||||
// this looks better without margins |
||||
TapHandler { |
||||
acceptedButtons: Qt.RightButton |
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad |
||||
gesturePolicy: TapHandler.ReleaseWithinBounds |
||||
|
||||
onSingleTapped: messageContextMenu.show(wrapper.eventId, wrapper.threadId, wrapper.type, wrapper.isSender, wrapper.isEncrypted, wrapper.isEditable, wrapper.main.hoveredLink, wrapper.main.copyText) |
||||
} |
||||
}, |
||||
Rectangle { |
||||
id: scrollHighlight |
||||
anchors.fill: gridContainer |
||||
|
||||
color: palette.highlight |
||||
enabled: false |
||||
opacity: 0 |
||||
visible: true |
||||
z: 1 |
||||
|
||||
states: State { |
||||
name: "revealed" |
||||
when: wrapper.scrolledToThis |
||||
} |
||||
transitions: Transition { |
||||
from: "" |
||||
to: "revealed" |
||||
|
||||
SequentialAnimation { |
||||
PropertyAnimation { |
||||
duration: 500 |
||||
easing.type: Easing.InOutQuad |
||||
from: 0 |
||||
properties: "opacity" |
||||
target: scrollHighlight |
||||
to: 1 |
||||
} |
||||
PropertyAnimation { |
||||
duration: 500 |
||||
easing.type: Easing.InOutQuad |
||||
from: 1 |
||||
properties: "opacity" |
||||
target: scrollHighlight |
||||
to: 0 |
||||
} |
||||
ScriptAction { |
||||
script: wrapper.room.eventShown() |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
Item { |
||||
id: gridContainer |
||||
|
||||
width: wrapper.width - wrapper.avatarMargin |
||||
implicitHeight: messageBubble.implicitHeight |
||||
x: wrapper.avatarMargin |
||||
y: section.visible && section.active ? section.y + section.height : 0 |
||||
|
||||
HoverHandler { |
||||
id: messageHover |
||||
blocking: false |
||||
onHoveredChanged: () => { |
||||
if (!Settings.mobileMode && hovered) { |
||||
if (!messageActions.hovered) { |
||||
messageActions.model = wrapper; |
||||
messageActions.attached = wrapper; |
||||
messageActions.anchors.bottomMargin = -gridContainer.y |
||||
//messageActions.anchors.rightMargin = metadata.width |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
AbstractButton { |
||||
id: messageBubble |
||||
|
||||
anchors.left: (wrapper.isStateEvent || wrapper.isSender) ? undefined : parent.left |
||||
anchors.right: (wrapper.isStateEvent || !wrapper.isSender) ? undefined : parent.right |
||||
anchors.horizontalCenter: wrapper.isStateEvent ? parent.horizontalCenter : undefined |
||||
|
||||
property color userColor: TimelineManager.userColor(wrapper.main?.userId ?? '', palette.base) |
||||
|
||||
contentItem: Item { |
||||
id: contentPlacementContainer |
||||
|
||||
property bool fitsMetadata: ((wrapper.main?.width ?? 0) + wrapper.mainInset + metadata.width) < wrapper.maxWidth |
||||
|
||||
// This doesnt work because of tables. They might have content in the top of the cell, while the background reaches to the bottom. Maybe using the textDocument we could do more? |
||||
// property bool fitsMetadataInside: wrapper.main?.positionAt ? (wrapper.main.positionAt(wrapper.main.width, wrapper.main.height - 4) == wrapper.main.positionAt(wrapper.main.width - metadata.width, wrapper.main.height - 4)) : false |
||||
property bool fitsMetadataInside: false |
||||
|
||||
implicitWidth: Math.max((wrapper.reply?.width ?? 0) + wrapper.replyInset, (wrapper.main?.width ?? 0) + wrapper.mainInset + ((fitsMetadata && !fitsMetadataInside) ? metadata.width : 0)) |
||||
implicitHeight: contentColumn.implicitHeight + ((fitsMetadata || fitsMetadataInside) ? 0 : metadata.height) |
||||
|
||||
TimelineMetadata { |
||||
id: metadata |
||||
|
||||
scaling: 0.75 |
||||
|
||||
anchors.right: parent.right |
||||
anchors.bottom: parent.bottom |
||||
|
||||
visible: !wrapper.isStateEvent |
||||
|
||||
eventId: wrapper.eventId |
||||
status: wrapper.status |
||||
trustlevel: wrapper.trustlevel |
||||
isEdited: wrapper.isEdited |
||||
isEncrypted: wrapper.isEncrypted |
||||
threadId: wrapper.threadId |
||||
timestamp: wrapper.timestamp |
||||
room: wrapper.room |
||||
} |
||||
|
||||
Column { |
||||
id: contentColumn |
||||
|
||||
anchors.left: parent.left |
||||
anchors.right: parent.right |
||||
|
||||
AbstractButton { |
||||
id: replyRow |
||||
visible: wrapper.reply |
||||
|
||||
height: replyLine.height |
||||
anchors.left: parent.left |
||||
anchors.right: parent.right |
||||
|
||||
property color userColor: TimelineManager.userColor(wrapper.reply?.userId ?? '', palette.base) |
||||
|
||||
clip: true |
||||
|
||||
NhekoCursorShape { |
||||
anchors.fill: parent |
||||
cursorShape: Qt.PointingHandCursor |
||||
} |
||||
|
||||
contentItem: Row { |
||||
id: replyRowLay |
||||
|
||||
spacing: Nheko.paddingSmall |
||||
|
||||
Rectangle { |
||||
id: replyLine |
||||
height: Math.min( wrapper.reply?.height, timelineView.height / 10) + Nheko.paddingSmall + replyUserButton.height |
||||
color: replyRow.userColor |
||||
width: 4 |
||||
} |
||||
|
||||
Column { |
||||
spacing: 0 |
||||
|
||||
id: replyCol |
||||
|
||||
AbstractButton { |
||||
id: replyUserButton |
||||
|
||||
contentItem: Label { |
||||
id: userName_ |
||||
text: wrapper.reply?.userName ?? '' |
||||
color: replyRow.userColor |
||||
textFormat: Text.RichText |
||||
width: wrapper.maxWidth |
||||
//elideWidth: wrapper.maxWidth |
||||
} |
||||
onClicked: wrapper.room.openUserProfile(wrapper.reply?.userId) |
||||
} |
||||
data: [ |
||||
replyUserButton, |
||||
wrapper.reply, |
||||
] |
||||
} |
||||
} |
||||
|
||||
background: Rectangle { |
||||
//width: replyRow.implicitContentWidth |
||||
color: Qt.tint(palette.base, Qt.hsla(replyRow.userColor.hslHue, 0.5, replyRow.userColor.hslLightness, 0.1)) |
||||
} |
||||
|
||||
onClicked: { |
||||
let link = wrapper.reply.hoveredLink |
||||
if (link) { |
||||
Nheko.openLink(link) |
||||
} else { |
||||
console.log("Scrolling to "+wrapper.replyTo); |
||||
wrapper.room.showEvent(wrapper.replyTo) |
||||
} |
||||
} |
||||
onPressAndHold: wrapper.replyContextMenu.show(wrapper.reply.copyText ?? "", wrapper.reply.linkAt ? wrapper.reply.linkAt(pressX-replyLine.width - Nheko.paddingSmall, pressY - replyUserButton.implicitHeight) : "", wrapper.replyTo) |
||||
TapHandler { |
||||
acceptedButtons: Qt.RightButton |
||||
onSingleTapped: (eventPoint) => wrapper.replyContextMenu.show(wrapper.reply.copyText ?? "", wrapper.reply.linkAt ? wrapper.reply.linkAt(eventPoint.position.x-replyLine.width - Nheko.paddingSmall, eventPoint.position.y - replyUserButton.implicitHeight) : "", wrapper.replyTo) |
||||
gesturePolicy: TapHandler.ReleaseWithinBounds |
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad |
||||
} |
||||
} |
||||
|
||||
data: [replyRow, wrapper.main] |
||||
} |
||||
} |
||||
|
||||
padding: wrapper.isStateEvent ? 0 : 4 |
||||
background: Rectangle { |
||||
color: !wrapper.isStateEvent ? Qt.tint(palette.base, Qt.hsla(messageBubble.userColor.hslHue, wrapper.hovered ? 0.8 : 0.5, messageBubble.userColor.hslLightness, 0.2)) : "transparent" |
||||
radius: 4 |
||||
border.color: Nheko.theme.red |
||||
border.width: wrapper.notificationlevel == MtxEvent.Highlight ? 1 : 0 |
||||
} |
||||
} |
||||
}, |
||||
Reactions { |
||||
id: reactionRow |
||||
|
||||
eventId: wrapper.eventId |
||||
layoutDirection: (!wrapper.isStateEvent && wrapper.isSender) ? Qt.RightToLeft : Qt.LeftToRight |
||||
reactions: wrapper.reactions |
||||
width: wrapper.width - wrapper.avatarMargin |
||||
x: wrapper.avatarMargin |
||||
|
||||
anchors { |
||||
//left: row.bubbleOnRight ? undefined : row.left |
||||
//right: row.bubbleOnRight ? row.right : undefined |
||||
top: gridContainer.bottom |
||||
topMargin: -4 |
||||
} |
||||
}, |
||||
Rectangle { |
||||
id: unreadRow |
||||
|
||||
color: palette.highlight |
||||
height: visible ? 3 : 0 |
||||
visible: (wrapper.index > 0 && (wrapper.room.fullyReadEventId == wrapper.eventId)) |
||||
|
||||
anchors { |
||||
left: parent.left |
||||
right: parent.right |
||||
top: reactionRow.bottom |
||||
topMargin: 5 |
||||
} |
||||
} |
||||
] |
||||
} |
||||
|
@ -0,0 +1,327 @@ |
||||
// SPDX-FileCopyrightText: Nheko Contributors |
||||
// |
||||
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
||||
import "./components" |
||||
import "./delegates" |
||||
import "./emoji" |
||||
import "./ui" |
||||
import "./dialogs" |
||||
import Qt.labs.platform 1.1 as Platform |
||||
import QtQuick |
||||
import QtQuick.Controls |
||||
import QtQuick.Layouts |
||||
import QtQuick.Window |
||||
import im.nheko |
||||
|
||||
TimelineEvent { |
||||
id: wrapper |
||||
ListView.delayRemove: true |
||||
width: chat.delegateMaxWidth |
||||
height: Math.max((section.item?.height ?? 0) + gridContainer.implicitHeight + reactionRow.implicitHeight + unreadRow.height, 10) |
||||
anchors.horizontalCenter: ListView.view.contentItem.horizontalCenter |
||||
//room: chatRoot.roommodel |
||||
|
||||
required property var day |
||||
required property bool isSender |
||||
required property int index |
||||
property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Day) |
||||
property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index + 1, Room.IsStateEvent) |
||||
property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index + 1, Room.UserId) |
||||
|
||||
required property date timestamp |
||||
required property string userId |
||||
required property string userName |
||||
required property string threadId |
||||
required property int userPowerlevel |
||||
required property bool isEdited |
||||
required property bool isEncrypted |
||||
required property var reactions |
||||
required property int status |
||||
required property int trustlevel |
||||
required property int notificationlevel |
||||
required property int type |
||||
required property bool isEditable |
||||
|
||||
required property QtObject messageContextMenu |
||||
required property QtObject replyContextMenu |
||||
required property Item messageActions |
||||
|
||||
property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) // align bubble with section header |
||||
|
||||
property alias hovered: messageHover.hovered |
||||
property bool scrolledToThis: false |
||||
|
||||
mainInset: (threadId ? (4 + Nheko.paddingSmall) : 0) |
||||
replyInset: mainInset + 4 + Nheko.paddingSmall |
||||
|
||||
maxWidth: chat.delegateMaxWidth - avatarMargin - metadata.width |
||||
|
||||
data: [ |
||||
Loader { |
||||
id: section |
||||
|
||||
active: wrapper.previousMessageUserId !== wrapper.userId || wrapper.previousMessageDay !== wrapper.day || wrapper.previousMessageIsStateEvent !== wrapper.isStateEvent |
||||
//asynchronous: true |
||||
sourceComponent: TimelineSectionHeader { |
||||
day: wrapper.day |
||||
isSender: wrapper.isSender |
||||
isStateEvent: wrapper.isStateEvent |
||||
parentWidth: wrapper.width |
||||
previousMessageDay: wrapper.previousMessageDay |
||||
previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent |
||||
previousMessageUserId: wrapper.previousMessageUserId |
||||
timestamp: wrapper.timestamp |
||||
userId: wrapper.userId |
||||
userName: wrapper.userName |
||||
userPowerlevel: wrapper.userPowerlevel |
||||
} |
||||
visible: status == Loader.Ready |
||||
z: 4 |
||||
}, |
||||
Rectangle { |
||||
anchors.fill: gridContainer |
||||
color: (Settings.messageHoverHighlight && messageHover.hovered) ? palette.alternateBase : "transparent" |
||||
|
||||
// this looks better without margins |
||||
TapHandler { |
||||
acceptedButtons: Qt.RightButton |
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad |
||||
gesturePolicy: TapHandler.ReleaseWithinBounds |
||||
|
||||
onSingleTapped: messageContextMenu.show(wrapper.eventId, wrapper.threadId, wrapper.type, wrapper.isSender, wrapper.isEncrypted, wrapper.isEditable, wrapper.main.hoveredLink, wrapper.main.copyText) |
||||
} |
||||
}, |
||||
Rectangle { |
||||
id: scrollHighlight |
||||
anchors.fill: gridContainer |
||||
|
||||
color: palette.highlight |
||||
enabled: false |
||||
opacity: 0 |
||||
visible: true |
||||
z: 1 |
||||
|
||||
states: State { |
||||
name: "revealed" |
||||
when: wrapper.scrolledToThis |
||||
} |
||||
transitions: Transition { |
||||
from: "" |
||||
to: "revealed" |
||||
|
||||
SequentialAnimation { |
||||
PropertyAnimation { |
||||
duration: 500 |
||||
easing.type: Easing.InOutQuad |
||||
from: 0 |
||||
properties: "opacity" |
||||
target: scrollHighlight |
||||
to: 1 |
||||
} |
||||
PropertyAnimation { |
||||
duration: 500 |
||||
easing.type: Easing.InOutQuad |
||||
from: 1 |
||||
properties: "opacity" |
||||
target: scrollHighlight |
||||
to: 0 |
||||
} |
||||
ScriptAction { |
||||
script: wrapper.room.eventShown() |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
Rectangle { |
||||
anchors.top: gridContainer.top |
||||
anchors.left: gridContainer.left |
||||
anchors.topMargin: -2 |
||||
anchors.leftMargin: -2 |
||||
color: "transparent" |
||||
border.color: Nheko.theme.red |
||||
border.width: wrapper.notificationlevel == MtxEvent.Highlight ? 1 : 0 |
||||
radius: 4 |
||||
height: contentColumn.implicitHeight + 4 |
||||
width: contentColumn.implicitWidth + 4 |
||||
}, |
||||
Row { |
||||
id: gridContainer |
||||
|
||||
width: wrapper.width - wrapper.avatarMargin |
||||
x: wrapper.avatarMargin |
||||
y: section.visible && section.active ? section.y + section.height : 0 |
||||
spacing: Nheko.paddingSmall |
||||
|
||||
HoverHandler { |
||||
id: messageHover |
||||
blocking: false |
||||
onHoveredChanged: () => { |
||||
if (!Settings.mobileMode && hovered) { |
||||
if (!messageActions.hovered) { |
||||
messageActions.model = wrapper; |
||||
messageActions.attached = wrapper; |
||||
messageActions.anchors.bottomMargin = -gridContainer.y |
||||
messageActions.anchors.rightMargin = metadata.width |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
AbstractButton { |
||||
ToolTip.delay: Nheko.tooltipDelay |
||||
ToolTip.text: qsTr("Part of a thread") |
||||
ToolTip.visible: hovered |
||||
height: contentColumn.height |
||||
visible: wrapper.threadId |
||||
width: 4 |
||||
|
||||
onClicked: wrapper.room.thread = wrapper.threadId |
||||
|
||||
Rectangle { |
||||
id: threadLine |
||||
|
||||
anchors.fill: parent |
||||
color: TimelineManager.userColor(wrapper.threadId, palette.base) |
||||
} |
||||
} |
||||
|
||||
Item { |
||||
visible: wrapper.isStateEvent |
||||
width: (wrapper.maxWidth - (wrapper.main?.width ?? 0)) / 2 |
||||
height: 1 |
||||
} |
||||
|
||||
Column { |
||||
id: contentColumn |
||||
|
||||
AbstractButton { |
||||
id: replyRow |
||||
visible: wrapper.reply |
||||
|
||||
height: replyLine.height |
||||
|
||||
property color userColor: TimelineManager.userColor(wrapper.reply?.userId ?? '', palette.base) |
||||
|
||||
clip: true |
||||
|
||||
NhekoCursorShape { |
||||
anchors.fill: parent |
||||
cursorShape: Qt.PointingHandCursor |
||||
} |
||||
|
||||
contentItem: Row { |
||||
id: replyRowLay |
||||
|
||||
spacing: Nheko.paddingSmall |
||||
|
||||
Rectangle { |
||||
id: replyLine |
||||
height: Math.min( wrapper.reply?.height, timelineView.height / 10) + Nheko.paddingSmall + replyUserButton.height |
||||
color: replyRow.userColor |
||||
width: 4 |
||||
} |
||||
|
||||
Column { |
||||
spacing: 0 |
||||
|
||||
id: replyCol |
||||
|
||||
AbstractButton { |
||||
id: replyUserButton |
||||
|
||||
contentItem: Label { |
||||
id: userName_ |
||||
text: wrapper.reply?.userName ?? '' |
||||
color: replyRow.userColor |
||||
textFormat: Text.RichText |
||||
width: wrapper.maxWidth |
||||
//elideWidth: wrapper.maxWidth |
||||
} |
||||
onClicked: wrapper.room.openUserProfile(wrapper.reply?.userId) |
||||
} |
||||
data: [ |
||||
replyUserButton, |
||||
wrapper.reply, |
||||
] |
||||
} |
||||
} |
||||
|
||||
background: Rectangle { |
||||
//width: replyRow.implicitContentWidth |
||||
color: Qt.tint(palette.base, Qt.hsla(replyRow.userColor.hslHue, 0.5, replyRow.userColor.hslLightness, 0.1)) |
||||
} |
||||
|
||||
onClicked: { |
||||
let link = wrapper.reply.hoveredLink |
||||
if (link) { |
||||
Nheko.openLink(link) |
||||
} else { |
||||
console.log("Scrolling to "+wrapper.replyTo); |
||||
wrapper.room.showEvent(wrapper.replyTo) |
||||
} |
||||
} |
||||
onPressAndHold: wrapper.replyContextMenu.show(wrapper.reply.copyText ?? "", wrapper.reply.linkAt ? wrapper.reply.linkAt(pressX-replyLine.width - Nheko.paddingSmall, pressY - replyUserButton.implicitHeight) : "", wrapper.replyTo) |
||||
TapHandler { |
||||
acceptedButtons: Qt.RightButton |
||||
onSingleTapped: (eventPoint) => wrapper.replyContextMenu.show(wrapper.reply.copyText ?? "", wrapper.reply.linkAt ? wrapper.reply.linkAt(eventPoint.position.x-replyLine.width - Nheko.paddingSmall, eventPoint.position.y - replyUserButton.implicitHeight) : "", wrapper.replyTo) |
||||
gesturePolicy: TapHandler.ReleaseWithinBounds |
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad |
||||
} |
||||
} |
||||
|
||||
data: [ |
||||
replyRow, wrapper.main, |
||||
] |
||||
} |
||||
|
||||
}, |
||||
TimelineMetadata { |
||||
id: metadata |
||||
|
||||
scaling: 1 |
||||
|
||||
anchors.right: parent.right |
||||
y: section.visible && section.active ? section.y + section.height : 0 |
||||
|
||||
visible: !wrapper.isStateEvent |
||||
|
||||
eventId: wrapper.eventId |
||||
status: wrapper.status |
||||
trustlevel: wrapper.trustlevel |
||||
isEdited: wrapper.isEdited |
||||
isEncrypted: wrapper.isEncrypted |
||||
threadId: wrapper.threadId |
||||
timestamp: wrapper.timestamp |
||||
room: wrapper.room |
||||
}, |
||||
Reactions { |
||||
id: reactionRow |
||||
|
||||
eventId: wrapper.eventId |
||||
reactions: wrapper.reactions |
||||
width: wrapper.width - wrapper.avatarMargin |
||||
x: wrapper.avatarMargin |
||||
|
||||
anchors { |
||||
top: gridContainer.bottom |
||||
topMargin: -4 |
||||
} |
||||
}, |
||||
Rectangle { |
||||
id: unreadRow |
||||
|
||||
color: palette.highlight |
||||
height: visible ? 3 : 0 |
||||
visible: (wrapper.index > 0 && (wrapper.room.fullyReadEventId == wrapper.eventId)) |
||||
|
||||
anchors { |
||||
left: parent.left |
||||
right: parent.right |
||||
top: reactionRow.bottom |
||||
topMargin: 5 |
||||
} |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,266 @@ |
||||
// SPDX-FileCopyrightText: Nheko Contributors |
||||
// |
||||
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
||||
import "./components" |
||||
import "./delegates" |
||||
import "./emoji" |
||||
import "./ui" |
||||
import "./dialogs" |
||||
import Qt.labs.platform 1.1 as Platform |
||||
import QtQuick 2.15 |
||||
import QtQuick.Controls 2.15 |
||||
import QtQuick.Layouts 1.2 |
||||
import QtQuick.Window 2.13 |
||||
import im.nheko 1.0 |
||||
|
||||
EventDelegateChooser { |
||||
id: wrapper |
||||
|
||||
required property bool isStateEvent |
||||
|
||||
EventDelegateChoice { |
||||
roleValues: [MtxEvent.TextMessage, MtxEvent.NoticeMessage, MtxEvent.ElementEffectMessage, MtxEvent.UnknownMessage,] |
||||
|
||||
TextMessage { |
||||
required property string formattedBody |
||||
required property int type |
||||
required property string userId |
||||
required property string userName |
||||
|
||||
Layout.fillWidth: true |
||||
//Layout.maximumWidth: implicitWidth |
||||
|
||||
color: type == MtxEvent.NoticeMessage ? palette.buttonText : palette.text |
||||
font.italic: type == MtxEvent.NoticeMessage |
||||
formatted: formattedBody |
||||
keepFullText: true |
||||
} |
||||
} |
||||
EventDelegateChoice { |
||||
roleValues: [MtxEvent.EmoteMessage,] |
||||
|
||||
TextMessage { |
||||
required property string formattedBody |
||||
required property string userId |
||||
required property string userName |
||||
|
||||
Layout.fillWidth: true |
||||
//Layout.maximumWidth: implicitWidth |
||||
|
||||
color: TimelineManager.userColor(userId, palette.base) |
||||
font.italic: true |
||||
formatted: TimelineManager.escapeEmoji(userName) + " " + formattedBody |
||||
keepFullText: true |
||||
} |
||||
} |
||||
EventDelegateChoice { |
||||
roleValues: [MtxEvent.CanonicalAlias, MtxEvent.ServerAcl, MtxEvent.Name, MtxEvent.Topic, MtxEvent.Avatar, MtxEvent.PinnedEvents, MtxEvent.ImagePackInRoom, MtxEvent.SpaceParent, MtxEvent.RoomCreate, MtxEvent.PowerLevels, MtxEvent.PolicyRuleUser, MtxEvent.PolicyRuleRoom, MtxEvent.PolicyRuleServer, MtxEvent.RoomJoinRules, MtxEvent.RoomHistoryVisibility, MtxEvent.RoomGuestAccess,] |
||||
|
||||
TextMessage { |
||||
required property string formattedStateEvent |
||||
required property string userId |
||||
required property string userName |
||||
|
||||
body: '' |
||||
color: palette.buttonText |
||||
font.italic: true |
||||
font.pointSize: Settings.fontSize * 0.8 |
||||
formatted: '' |
||||
horizontalAlignment: Text.AlignHCenter |
||||
isOnlyEmoji: false |
||||
keepFullText: true |
||||
text: formattedStateEvent |
||||
} |
||||
} |
||||
EventDelegateChoice { |
||||
roleValues: [MtxEvent.CallInvite,] |
||||
|
||||
TextMessage { |
||||
required property string callType |
||||
required property string userId |
||||
required property string userName |
||||
|
||||
Layout.fillWidth: true |
||||
body: formatted |
||||
color: palette.buttonText |
||||
font.italic: true |
||||
formatted: { |
||||
switch (callType) { |
||||
case "voice": |
||||
return qsTr("%1 placed a voice call.").arg(TimelineManager.escapeEmoji(userName)); |
||||
case "video": |
||||
return qsTr("%1 placed a video call.").arg(TimelineManager.escapeEmoji(userName)); |
||||
default: |
||||
return qsTr("%1 placed a call.").arg(TimelineManager.escapeEmoji(userName)); |
||||
} |
||||
} |
||||
isOnlyEmoji: false |
||||
keepFullText: true |
||||
} |
||||
} |
||||
EventDelegateChoice { |
||||
roleValues: [MtxEvent.CallAnswer, MtxEvent.CallReject, MtxEvent.CallSelectAnswer, MtxEvent.CallHangUp, MtxEvent.CallCandidates, MtxEvent.CallNegotiate,] |
||||
|
||||
TextMessage { |
||||
required property int type |
||||
required property string userId |
||||
required property string userName |
||||
|
||||
Layout.fillWidth: true |
||||
body: formatted |
||||
color: palette.buttonText |
||||
font.italic: true |
||||
formatted: { |
||||
switch (type) { |
||||
case MtxEvent.CallAnswer: |
||||
return qsTr("%1 answered the call.").arg(TimelineManager.escapeEmoji(userName)); |
||||
case MtxEvent.CallReject: |
||||
return qsTr("%1 rejected the call.").arg(TimelineManager.escapeEmoji(userName)); |
||||
case MtxEvent.CallSelectAnswer: |
||||
return qsTr("%1 selected answer.").arg(TimelineManager.escapeEmoji(userName)); |
||||
case MtxEvent.CallHangUp: |
||||
return qsTr("%1 ended the call.").arg(TimelineManager.escapeEmoji(userName)); |
||||
case MtxEvent.CallCandidates: |
||||
return qsTr("%1 is negotiating the call...").arg(TimelineManager.escapeEmoji(userName)); |
||||
case MtxEvent.CallNegotiate: |
||||
return qsTr("%1 is negotiating the call...").arg(TimelineManager.escapeEmoji(userName)); |
||||
} |
||||
} |
||||
isOnlyEmoji: false |
||||
keepFullText: true |
||||
} |
||||
} |
||||
EventDelegateChoice { |
||||
roleValues: [MtxEvent.ImageMessage, MtxEvent.Sticker,] |
||||
|
||||
ImageMessage { |
||||
required property string userId |
||||
required property string userName |
||||
|
||||
Layout.fillWidth: true |
||||
//Layout.maximumWidth: tempWidth |
||||
//Layout.maximumHeight: timelineView.height / 8 |
||||
containerHeight: timelineView.height |
||||
} |
||||
} |
||||
EventDelegateChoice { |
||||
roleValues: [MtxEvent.FileMessage,] |
||||
|
||||
FileMessage { |
||||
required property string userId |
||||
required property string userName |
||||
|
||||
Layout.fillWidth: true |
||||
} |
||||
} |
||||
EventDelegateChoice { |
||||
roleValues: [MtxEvent.VideoMessage, MtxEvent.AudioMessage,] |
||||
|
||||
PlayableMediaMessage { |
||||
required property string userId |
||||
required property string userName |
||||
|
||||
Layout.fillWidth: true |
||||
} |
||||
} |
||||
EventDelegateChoice { |
||||
roleValues: [MtxEvent.Encrypted,] |
||||
|
||||
Encrypted { |
||||
required property string userId |
||||
required property string userName |
||||
|
||||
Layout.fillWidth: true |
||||
} |
||||
} |
||||
EventDelegateChoice { |
||||
roleValues: [MtxEvent.Encryption,] |
||||
|
||||
EncryptionEnabled { |
||||
required property string userId |
||||
|
||||
Layout.fillWidth: true |
||||
} |
||||
} |
||||
EventDelegateChoice { |
||||
roleValues: [MtxEvent.Redacted] |
||||
|
||||
Redacted { |
||||
required property string userId |
||||
required property string userName |
||||
|
||||
Layout.fillWidth: true |
||||
} |
||||
} |
||||
EventDelegateChoice { |
||||
roleValues: [MtxEvent.Member] |
||||
|
||||
ColumnLayout { |
||||
id: member |
||||
|
||||
required property string formattedStateEvent |
||||
required property Room room |
||||
required property string userId |
||||
required property string userName |
||||
|
||||
NoticeMessage { |
||||
Layout.fillWidth: true |
||||
body: formatted |
||||
formatted: member.formattedStateEvent |
||||
isOnlyEmoji: false |
||||
isReply: EventDelegateChooser.isReply |
||||
isStateEvent: true |
||||
keepFullText: true |
||||
} |
||||
Button { |
||||
Layout.alignment: Qt.AlignHCenter |
||||
text: qsTr("Allow them in") |
||||
visible: member.room.showAcceptKnockButton(member.eventId) |
||||
|
||||
onClicked: member.room.acceptKnock(member.eventId) |
||||
} |
||||
} |
||||
} |
||||
EventDelegateChoice { |
||||
roleValues: [MtxEvent.Tombstone] |
||||
|
||||
ColumnLayout { |
||||
id: tombstone |
||||
|
||||
required property string body |
||||
required property string eventId |
||||
required property Room room |
||||
required property string userId |
||||
required property string userName |
||||
|
||||
NoticeMessage { |
||||
Layout.fillWidth: true |
||||
body: formatted |
||||
formatted: qsTr("This room was replaced for the following reason: %1").arg(tombstone.body) |
||||
isOnlyEmoji: false |
||||
isReply: EventDelegateChooser.isReply |
||||
isStateEvent: true |
||||
keepFullText: true |
||||
} |
||||
Button { |
||||
Layout.alignment: Qt.AlignHCenter |
||||
text: qsTr("Go to replacement room") |
||||
|
||||
onClicked: tombstone.room.joinReplacementRoom(tombstone.eventId) |
||||
} |
||||
} |
||||
} |
||||
EventDelegateChoice { |
||||
roleValues: [] |
||||
|
||||
MatrixText { |
||||
required property string typeString |
||||
required property string userId |
||||
required property string userName |
||||
|
||||
Layout.fillWidth: true |
||||
text: "Unsupported: " + typeString |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,98 @@ |
||||
// SPDX-FileCopyrightText: Nheko Contributors |
||||
// |
||||
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
||||
import "./components" |
||||
import "./delegates" |
||||
import "./emoji" |
||||
import "./ui" |
||||
import "./dialogs" |
||||
import Qt.labs.platform 1.1 as Platform |
||||
import QtQuick |
||||
import QtQuick.Controls |
||||
import QtQuick.Layouts |
||||
import QtQuick.Window |
||||
import im.nheko |
||||
|
||||
RowLayout { |
||||
id: metadata |
||||
|
||||
property int iconSize: Math.floor(fontMetrics.ascent * scaling) |
||||
required property double scaling |
||||
|
||||
required property string eventId |
||||
required property int status |
||||
required property int trustlevel |
||||
required property bool isEdited |
||||
required property bool isEncrypted |
||||
required property string threadId |
||||
required property date timestamp |
||||
required property Room room |
||||
|
||||
spacing: 2 |
||||
|
||||
StatusIndicator { |
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter |
||||
eventId: metadata.eventId |
||||
height: parent.iconSize |
||||
status: metadata.status |
||||
width: parent.iconSize |
||||
} |
||||
Image { |
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter |
||||
ToolTip.delay: Nheko.tooltipDelay |
||||
ToolTip.text: qsTr("Edited") |
||||
ToolTip.visible: editHovered.hovered |
||||
height: parent.iconSize |
||||
source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((metadata.eventId == metadata.room.edit) ? palette.highlight : palette.buttonText) |
||||
sourceSize.height: parent.iconSize * Screen.devicePixelRatio |
||||
sourceSize.width: parent.iconSize * Screen.devicePixelRatio |
||||
visible: metadata.isEdited || metadata.eventId == metadata.room.edit |
||||
width: parent.iconSize |
||||
|
||||
HoverHandler { |
||||
id: editHovered |
||||
|
||||
} |
||||
} |
||||
ImageButton { |
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter |
||||
ToolTip.delay: Nheko.tooltipDelay |
||||
ToolTip.text: qsTr("Part of a thread") |
||||
ToolTip.visible: hovered |
||||
buttonTextColor: TimelineManager.userColor(metadata.threadId, palette.base) |
||||
height: parent.iconSize |
||||
image: ":/icons/icons/ui/thread.svg" |
||||
visible: metadata.threadId |
||||
width: parent.iconSize |
||||
|
||||
onClicked: metadata.room.thread = threadId |
||||
} |
||||
EncryptionIndicator { |
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter |
||||
encrypted: metadata.isEncrypted |
||||
height: parent.iconSize |
||||
sourceSize.height: parent.iconSize * Screen.devicePixelRatio |
||||
sourceSize.width: parent.iconSize * Screen.devicePixelRatio |
||||
trust: metadata.trustlevel |
||||
visible: metadata.room.isEncrypted |
||||
width: parent.iconSize |
||||
} |
||||
Label { |
||||
id: ts |
||||
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop |
||||
Layout.preferredWidth: implicitWidth |
||||
ToolTip.delay: Nheko.tooltipDelay |
||||
ToolTip.text: Qt.formatDateTime(metadata.timestamp, Qt.DefaultLocaleLongDate) |
||||
ToolTip.visible: ma.hovered |
||||
color: palette.inactive.text |
||||
font.pointSize: fontMetrics.font.pointSize * parent.scaling |
||||
text: metadata.timestamp.toLocaleTimeString(Locale.ShortFormat) |
||||
|
||||
HoverHandler { |
||||
id: ma |
||||
|
||||
} |
||||
} |
||||
} |
@ -1,349 +0,0 @@ |
||||
// SPDX-FileCopyrightText: Nheko Contributors |
||||
// |
||||
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
||||
import "./delegates" |
||||
import "./emoji" |
||||
import QtQuick 2.15 |
||||
import QtQuick.Controls 2.3 |
||||
import QtQuick.Layouts 1.2 |
||||
import QtQuick.Window 2.13 |
||||
import im.nheko 1.0 |
||||
|
||||
AbstractButton { |
||||
id: r |
||||
|
||||
required property string blurhash |
||||
required property string body |
||||
required property string callType |
||||
required property int duration |
||||
required property int encryptionError |
||||
required property string eventId |
||||
required property string filename |
||||
required property string filesize |
||||
required property string formattedBody |
||||
required property int index |
||||
required property bool isEditable |
||||
required property bool isEdited |
||||
required property bool isEncrypted |
||||
required property bool isOnlyEmoji |
||||
required property bool isSender |
||||
required property bool isStateEvent |
||||
required property int notificationlevel |
||||
required property int originalWidth |
||||
required property double proportionalHeight |
||||
required property var reactions |
||||
required property int relatedEventCacheBuster |
||||
required property string replyTo |
||||
required property string roomName |
||||
required property string roomTopic |
||||
required property int status |
||||
required property string threadId |
||||
required property string thumbnailUrl |
||||
required property var timestamp |
||||
required property int trustlevel |
||||
required property int type |
||||
required property string typeString |
||||
required property string url |
||||
required property string userId |
||||
required property string userName |
||||
|
||||
height: row.height + (reactionRow.height > 0 ? reactionRow.height - 2 : 0) + unreadRow.height |
||||
hoverEnabled: true |
||||
|
||||
states: State { |
||||
name: "dragging" |
||||
when: draghandler.active |
||||
} |
||||
transitions: Transition { |
||||
from: "dragging" |
||||
to: "" |
||||
|
||||
PropertyAnimation { |
||||
duration: 100 |
||||
easing.type: Easing.InOutQuad |
||||
properties: "x" |
||||
target: r |
||||
to: 0 |
||||
} |
||||
} |
||||
|
||||
onClicked: { |
||||
let link = contentItem.child.linkAt != undefined && contentItem.child.linkAt(pressX - row.x - msg.x, pressY - row.y - msg.y - contentItem.y); |
||||
if (link) { |
||||
Nheko.openLink(link); |
||||
} |
||||
} |
||||
onDoubleClicked: room.reply = eventId |
||||
onPressAndHold: messageContextMenu.show(eventId, threadId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) |
||||
|
||||
Rectangle { |
||||
anchors.fill: parent |
||||
color: (Settings.messageHoverHighlight && hovered) ? palette.alternateBase : "transparent" |
||||
|
||||
// this looks better without margins |
||||
TapHandler { |
||||
acceptedButtons: Qt.RightButton |
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad |
||||
gesturePolicy: TapHandler.ReleaseWithinBounds |
||||
|
||||
onSingleTapped: messageContextMenu.show(eventId, threadId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) |
||||
} |
||||
} |
||||
DragHandler { |
||||
id: draghandler |
||||
|
||||
xAxis.maximum: 100 |
||||
xAxis.minimum: -100 |
||||
yAxis.enabled: false |
||||
|
||||
onActiveChanged: { |
||||
if (!active && (x < -70 || x > 70)) |
||||
room.reply = eventId; |
||||
} |
||||
} |
||||
AbstractButton { |
||||
ToolTip.delay: Nheko.tooltipDelay |
||||
ToolTip.text: qsTr("Part of a thread") |
||||
ToolTip.visible: hovered |
||||
anchors.left: parent.left |
||||
anchors.leftMargin: Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8) // align bubble with section header |
||||
height: parent.height |
||||
visible: threadId |
||||
width: 4 |
||||
|
||||
onClicked: room.thread = threadId |
||||
|
||||
Rectangle { |
||||
id: threadLine |
||||
|
||||
anchors.fill: parent |
||||
color: TimelineManager.userColor(threadId, palette.base) |
||||
} |
||||
} |
||||
Rectangle { |
||||
id: row |
||||
|
||||
property color bgColor: palette.base |
||||
property bool bubbleOnRight: isSender && Settings.bubbles |
||||
property int maxWidth: (parent.width - (Settings.smallAvatars || isStateEvent ? 0 : Nheko.avatarSize + 8)) * (Settings.bubbles && !isStateEvent ? 0.9 : 1) |
||||
property color userColor: TimelineManager.userColor(userId, palette.base) |
||||
|
||||
anchors.horizontalCenter: isStateEvent ? parent.horizontalCenter : undefined |
||||
anchors.left: (isStateEvent || bubbleOnRight) ? undefined : parent.left |
||||
anchors.leftMargin: (isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) + (threadId ? 6 : 0) // align bubble with section header |
||||
anchors.right: (isStateEvent || !bubbleOnRight) ? undefined : parent.right |
||||
border.color: Nheko.theme.red |
||||
border.width: r.notificationlevel == MtxEvent.Highlight ? 1 : 0 |
||||
color: (Settings.bubbles && !isStateEvent) ? Qt.tint(bgColor, Qt.hsla(userColor.hslHue, 0.5, userColor.hslLightness, 0.2)) : "#00000000" |
||||
height: msg.height + msg.anchors.margins * 2 |
||||
radius: 4 |
||||
width: Settings.bubbles ? Math.min(maxWidth, Math.max(reply.implicitWidth + 8, contentItem.implicitWidth + metadata.width + 20)) : maxWidth |
||||
|
||||
GridLayout { |
||||
id: msg |
||||
|
||||
columnSpacing: 2 |
||||
columns: Settings.bubbles ? 1 : 2 |
||||
rowSpacing: 0 |
||||
rows: Settings.bubbles ? 3 : 2 |
||||
|
||||
anchors { |
||||
left: parent.left |
||||
leftMargin: 4 |
||||
margins: (Settings.bubbles && !isStateEvent) ? 4 : 2 |
||||
right: parent.right |
||||
rightMargin: 4 |
||||
top: parent.top |
||||
} |
||||
|
||||
// fancy reply, if this is a reply |
||||
Reply { |
||||
id: reply |
||||
|
||||
function fromModel(role) { |
||||
return replyTo != "" ? room.dataById(replyTo, role, r.eventId) : null; |
||||
} |
||||
|
||||
Layout.bottomMargin: visible ? 2 : 0 |
||||
Layout.column: 0 |
||||
Layout.fillWidth: true |
||||
Layout.maximumWidth: Settings.bubbles ? Number.MAX_VALUE : implicitWidth |
||||
Layout.preferredHeight: height |
||||
Layout.row: 0 |
||||
blurhash: r.relatedEventCacheBuster, fromModel(Room.Blurhash) ?? "" |
||||
body: r.relatedEventCacheBuster, fromModel(Room.Body) ?? "" |
||||
callType: r.relatedEventCacheBuster, fromModel(Room.Voip) ?? "" |
||||
duration: r.relatedEventCacheBuster, fromModel(Room.Duration) ?? 0 |
||||
encryptionError: r.relatedEventCacheBuster, fromModel(Room.EncryptionError) ?? 0 |
||||
eventId: fromModel(Room.EventId) ?? "" |
||||
filename: r.relatedEventCacheBuster, fromModel(Room.Filename) ?? "" |
||||
filesize: r.relatedEventCacheBuster, fromModel(Room.Filesize) ?? "" |
||||
formattedBody: r.relatedEventCacheBuster, fromModel(Room.FormattedBody) ?? "" |
||||
isOnlyEmoji: r.relatedEventCacheBuster, fromModel(Room.IsOnlyEmoji) ?? false |
||||
isStateEvent: r.relatedEventCacheBuster, fromModel(Room.IsStateEvent) ?? false |
||||
originalWidth: r.relatedEventCacheBuster, fromModel(Room.OriginalWidth) ?? 0 |
||||
proportionalHeight: r.relatedEventCacheBuster, fromModel(Room.ProportionalHeight) ?? 1 |
||||
relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0 |
||||
roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? "" |
||||
roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? "" |
||||
thumbnailUrl: r.relatedEventCacheBuster, fromModel(Room.ThumbnailUrl) ?? "" |
||||
type: r.relatedEventCacheBuster, fromModel(Room.Type) ?? MtxEvent.UnknownMessage |
||||
typeString: r.relatedEventCacheBuster, fromModel(Room.TypeString) ?? "" |
||||
url: r.relatedEventCacheBuster, fromModel(Room.Url) ?? "" |
||||
userColor: r.relatedEventCacheBuster, TimelineManager.userColor(userId, palette.base) |
||||
userId: r.relatedEventCacheBuster, fromModel(Room.UserId) ?? "" |
||||
userName: r.relatedEventCacheBuster, fromModel(Room.UserName) ?? "" |
||||
visible: replyTo |
||||
} |
||||
|
||||
// actual message content |
||||
MessageDelegate { |
||||
id: contentItem |
||||
|
||||
Layout.column: 0 |
||||
Layout.fillWidth: true |
||||
Layout.preferredHeight: height |
||||
Layout.row: 1 |
||||
blurhash: r.blurhash |
||||
body: r.body |
||||
callType: r.callType |
||||
duration: r.duration |
||||
encryptionError: r.encryptionError |
||||
eventId: r.eventId |
||||
filename: r.filename |
||||
filesize: r.filesize |
||||
formattedBody: r.formattedBody |
||||
isOnlyEmoji: r.isOnlyEmoji |
||||
isReply: false |
||||
isStateEvent: r.isStateEvent |
||||
metadataWidth: metadata.width |
||||
originalWidth: r.originalWidth |
||||
proportionalHeight: r.proportionalHeight |
||||
relatedEventCacheBuster: r.relatedEventCacheBuster |
||||
roomName: r.roomName |
||||
roomTopic: r.roomTopic |
||||
thumbnailUrl: r.thumbnailUrl |
||||
type: r.type |
||||
typeString: r.typeString ?? "" |
||||
url: r.url |
||||
userId: r.userId |
||||
userName: r.userName |
||||
} |
||||
Row { |
||||
id: metadata |
||||
|
||||
property int iconSize: Math.floor(fontMetrics.ascent * scaling) |
||||
property double scaling: Settings.bubbles ? 0.75 : 1 |
||||
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignRight |
||||
Layout.bottomMargin: -2 |
||||
Layout.column: Settings.bubbles ? 0 : 1 |
||||
Layout.preferredWidth: implicitWidth |
||||
Layout.row: Settings.bubbles ? 2 : 0 |
||||
Layout.rowSpan: Settings.bubbles ? 1 : 2 |
||||
Layout.topMargin: (contentItem.fitsMetadata && Settings.bubbles) ? -height - Layout.bottomMargin : 0 |
||||
spacing: 2 |
||||
visible: !isStateEvent |
||||
|
||||
StatusIndicator { |
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop |
||||
anchors.verticalCenter: ts.verticalCenter |
||||
eventId: r.eventId |
||||
height: parent.iconSize |
||||
status: r.status |
||||
width: parent.iconSize |
||||
} |
||||
Image { |
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop |
||||
ToolTip.delay: Nheko.tooltipDelay |
||||
ToolTip.text: qsTr("Edited") |
||||
ToolTip.visible: editHovered.hovered |
||||
anchors.verticalCenter: ts.verticalCenter |
||||
height: parent.iconSize |
||||
source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((eventId == room.edit) ? palette.highlight : palette.buttonText) |
||||
sourceSize.height: parent.iconSize * Screen.devicePixelRatio |
||||
sourceSize.width: parent.iconSize * Screen.devicePixelRatio |
||||
visible: isEdited || eventId == room.edit |
||||
width: parent.iconSize |
||||
|
||||
HoverHandler { |
||||
id: editHovered |
||||
|
||||
} |
||||
} |
||||
ImageButton { |
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop |
||||
ToolTip.delay: Nheko.tooltipDelay |
||||
ToolTip.text: qsTr("Part of a thread") |
||||
ToolTip.visible: hovered |
||||
anchors.verticalCenter: ts.verticalCenter |
||||
buttonTextColor: TimelineManager.userColor(threadId, palette.base) |
||||
height: parent.iconSize |
||||
image: ":/icons/icons/ui/thread.svg" |
||||
visible: threadId |
||||
width: parent.iconSize |
||||
|
||||
onClicked: room.thread = threadId |
||||
} |
||||
EncryptionIndicator { |
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop |
||||
anchors.verticalCenter: ts.verticalCenter |
||||
encrypted: isEncrypted |
||||
height: parent.iconSize |
||||
sourceSize.height: parent.iconSize * Screen.devicePixelRatio |
||||
sourceSize.width: parent.iconSize * Screen.devicePixelRatio |
||||
trust: trustlevel |
||||
visible: room.isEncrypted |
||||
width: parent.iconSize |
||||
} |
||||
Label { |
||||
id: ts |
||||
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop |
||||
Layout.preferredWidth: implicitWidth |
||||
ToolTip.delay: Nheko.tooltipDelay |
||||
ToolTip.text: Qt.formatDateTime(timestamp, Qt.DefaultLocaleLongDate) |
||||
ToolTip.visible: ma.hovered |
||||
color: palette.inactive.text |
||||
font.pointSize: fontMetrics.font.pointSize * parent.scaling |
||||
text: timestamp.toLocaleTimeString(Locale.ShortFormat) |
||||
|
||||
HoverHandler { |
||||
id: ma |
||||
|
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
Reactions { |
||||
id: reactionRow |
||||
|
||||
eventId: r.eventId |
||||
layoutDirection: row.bubbleOnRight ? Qt.RightToLeft : Qt.LeftToRight |
||||
reactions: r.reactions |
||||
width: row.maxWidth |
||||
|
||||
anchors { |
||||
left: row.bubbleOnRight ? undefined : row.left |
||||
right: row.bubbleOnRight ? row.right : undefined |
||||
top: row.bottom |
||||
topMargin: -4 |
||||
} |
||||
} |
||||
Rectangle { |
||||
id: unreadRow |
||||
|
||||
color: palette.highlight |
||||
height: visible ? 3 : 0 |
||||
visible: (r.index > 0 && (room.fullyReadEventId == r.eventId)) |
||||
|
||||
anchors { |
||||
left: parent.left |
||||
right: parent.right |
||||
top: reactionRow.bottom |
||||
topMargin: 5 |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,164 @@ |
||||
// SPDX-FileCopyrightText: Nheko Contributors |
||||
// |
||||
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
||||
import Qt.labs.platform 1.1 as Platform |
||||
import QtQuick 2.15 |
||||
import QtQuick.Controls 2.15 |
||||
import QtQuick.Layouts 1.2 |
||||
import QtQuick.Window 2.13 |
||||
import im.nheko 1.0 |
||||
|
||||
Column { |
||||
|
||||
required property var day |
||||
required property bool isSender |
||||
required property bool isStateEvent |
||||
required property int parentWidth |
||||
required property var previousMessageDay |
||||
required property bool previousMessageIsStateEvent |
||||
required property string previousMessageUserId |
||||
required property date timestamp |
||||
required property string userId |
||||
required property string userName |
||||
required property string userPowerlevel |
||||
|
||||
bottomPadding: Settings.bubbles ? (isSender && previousMessageDay == day ? 0 : 2) : 3 |
||||
spacing: 8 |
||||
topPadding: userName_.visible ? 4 : 0 |
||||
visible: (previousMessageUserId !== userId || previousMessageDay !== day || isStateEvent !== previousMessageIsStateEvent) |
||||
width: parentWidth |
||||
|
||||
Label { |
||||
id: dateBubble |
||||
|
||||
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined |
||||
color: palette.text |
||||
height: Math.round(fontMetrics.height * 1.4) |
||||
horizontalAlignment: Text.AlignHCenter |
||||
text: room ? room.formatDateSeparator(timestamp) : "" |
||||
verticalAlignment: Text.AlignVCenter |
||||
visible: room && previousMessageDay !== day |
||||
width: contentWidth * 1.2 |
||||
|
||||
background: Rectangle { |
||||
color: palette.window |
||||
radius: parent.height / 2 |
||||
} |
||||
} |
||||
Row { |
||||
id: userInfo |
||||
|
||||
property int remainingWidth: chat.delegateMaxWidth - spacing - messageUserAvatar.width |
||||
|
||||
height: userName_.height |
||||
spacing: 8 |
||||
visible: !isStateEvent && (!isSender || !Settings.bubbles) |
||||
|
||||
Avatar { |
||||
id: messageUserAvatar |
||||
|
||||
ToolTip.delay: Nheko.tooltipDelay |
||||
ToolTip.text: userid |
||||
ToolTip.visible: messageUserAvatar.hovered |
||||
displayName: userName |
||||
height: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1) |
||||
url: !room ? "" : room.avatarUrl(userId).replace("mxc://", "image://MxcImage/") |
||||
userid: userId |
||||
width: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1) |
||||
|
||||
onClicked: room.openUserProfile(userId) |
||||
} |
||||
Connections { |
||||
function onRoomAvatarUrlChanged() { |
||||
messageUserAvatar.url = room.avatarUrl(userId).replace("mxc://", "image://MxcImage/"); |
||||
} |
||||
function onScrollToIndex(index) { |
||||
chat.positionViewAtIndex(index, ListView.Center); |
||||
} |
||||
|
||||
target: room |
||||
} |
||||
|
||||
AbstractButton { |
||||
id: userNameButton |
||||
|
||||
PowerlevelIndicator { |
||||
id: powerlevelIndicator |
||||
anchors.left: parent.left |
||||
anchors.verticalCenter: parent.verticalCenter |
||||
|
||||
powerlevel: userPowerlevel |
||||
height: fontMetrics.ascent |
||||
width: height |
||||
|
||||
sourceSize.width: fontMetrics.lineSpacing |
||||
sourceSize.height: fontMetrics.lineSpacing |
||||
|
||||
permissions: room ? room.permissions : null |
||||
visible: isAdmin || isModerator |
||||
} |
||||
|
||||
ToolTip.delay: Nheko.tooltipDelay |
||||
ToolTip.text: userId |
||||
ToolTip.visible: hovered |
||||
leftPadding: powerlevelIndicator.visible ? 16 : 0 |
||||
leftInset: 0 |
||||
rightInset: 0 |
||||
rightPadding: 0 |
||||
|
||||
contentItem: Label { |
||||
id: userName_ |
||||
|
||||
color: TimelineManager.userColor(userId, palette.base) |
||||
text: TimelineManager.escapeEmoji(userNameTextMetrics.elidedText) |
||||
textFormat: Text.RichText |
||||
} |
||||
|
||||
onClicked: room.openUserProfile(userId) |
||||
|
||||
TextMetrics { |
||||
id: userNameTextMetrics |
||||
|
||||
elide: Text.ElideRight |
||||
elideWidth: userInfo.remainingWidth - Math.min(statusMsg.implicitWidth, userInfo.remainingWidth / 3) |
||||
text: userName |
||||
} |
||||
NhekoCursorShape { |
||||
anchors.fill: parent |
||||
cursorShape: Qt.PointingHandCursor |
||||
} |
||||
} |
||||
Label { |
||||
id: statusMsg |
||||
|
||||
property string userStatus: Presence.userStatus(userId) |
||||
|
||||
ToolTip.delay: Nheko.tooltipDelay |
||||
ToolTip.text: qsTr("%1's status message").arg(userName) |
||||
ToolTip.visible: statusMsgHoverHandler.hovered |
||||
anchors.baseline: userNameButton.baseline |
||||
color: palette.buttonText |
||||
elide: Text.ElideRight |
||||
font.italic: true |
||||
font.pointSize: Math.floor(fontMetrics.font.pointSize * 0.8) |
||||
text: userStatus.replace(/\n/g, " ") |
||||
textFormat: Text.PlainText |
||||
width: Math.min(implicitWidth, userInfo.remainingWidth - userName_.width - parent.spacing) |
||||
|
||||
HoverHandler { |
||||
id: statusMsgHoverHandler |
||||
|
||||
} |
||||
Connections { |
||||
function onPresenceChanged(id) { |
||||
if (id == userId) |
||||
statusMsg.userStatus = Presence.userStatus(userId); |
||||
} |
||||
|
||||
target: Presence |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
@ -1,779 +0,0 @@ |
||||
// SPDX-FileCopyrightText: Nheko Contributors |
||||
// |
||||
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
||||
import QtQuick 2.6 |
||||
import QtQuick.Controls 2.1 |
||||
import QtQuick.Layouts 1.2 |
||||
import im.nheko 1.0 |
||||
|
||||
Item { |
||||
id: d |
||||
|
||||
required property bool isReply |
||||
property bool keepFullText: !isReply |
||||
property alias child: chooser.child |
||||
//implicitWidth: chooser.child?.implicitWidth ?? 0 |
||||
required property double proportionalHeight |
||||
required property int type |
||||
required property string typeString |
||||
required property int originalWidth |
||||
required property int duration |
||||
required property string blurhash |
||||
required property string body |
||||
required property string formattedBody |
||||
required property string eventId |
||||
required property string filename |
||||
required property string filesize |
||||
required property string url |
||||
required property string thumbnailUrl |
||||
required property bool isOnlyEmoji |
||||
required property bool isStateEvent |
||||
required property string userId |
||||
required property string userName |
||||
required property string roomTopic |
||||
required property string roomName |
||||
required property string callType |
||||
required property int encryptionError |
||||
required property int relatedEventCacheBuster |
||||
property bool fitsMetadata: (chooser.child && chooser.child.fitsMetadata) ? chooser.child.fitsMetadata : false |
||||
property int metadataWidth |
||||
|
||||
implicitWidth: chooser.child?.implicitWidth |
||||
|
||||
height: chooser.child ? chooser.child.height : Nheko.paddingLarge |
||||
|
||||
DelegateChooser { |
||||
id: chooser |
||||
|
||||
//role: "type" //< not supported in our custom implementation, have to use roleValue |
||||
roleValue: type |
||||
//anchors.fill: parent |
||||
|
||||
width: parent?.width ?? 0 // this should get rid of "cannot read property 'width' of null" |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.UnknownEvent |
||||
|
||||
Placeholder { |
||||
typeString: d.typeString |
||||
text: "Unretrieved event" |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.Tombstone |
||||
|
||||
|
||||
ColumnLayout { |
||||
width: parent.width |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
Layout.fillWidth: true |
||||
formatted: qsTr("This room was replaced for the following reason: %1").arg(d.body) |
||||
} |
||||
|
||||
Button { |
||||
Layout.alignment: Qt.AlignHCenter |
||||
text: qsTr("Go to replacement room") |
||||
onClicked: room.joinReplacementRoom(eventId) |
||||
} |
||||
|
||||
} |
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.TextMessage |
||||
|
||||
TextMessage { |
||||
formatted: d.formattedBody |
||||
body: d.body |
||||
isOnlyEmoji: d.isOnlyEmoji |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
metadataWidth: d.metadataWidth |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.UnknownMessage |
||||
|
||||
TextMessage { |
||||
formatted: d.formattedBody |
||||
body: d.body |
||||
isOnlyEmoji: d.isOnlyEmoji |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
metadataWidth: d.metadataWidth |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.ElementEffectMessage |
||||
|
||||
TextMessage { |
||||
formatted: d.formattedBody |
||||
body: d.body |
||||
isOnlyEmoji: d.isOnlyEmoji |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
metadataWidth: d.metadataWidth |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.NoticeMessage |
||||
|
||||
NoticeMessage { |
||||
formatted: d.formattedBody |
||||
body: d.body |
||||
isOnlyEmoji: d.isOnlyEmoji |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
metadataWidth: d.metadataWidth |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.EmoteMessage |
||||
|
||||
NoticeMessage { |
||||
formatted: TimelineManager.escapeEmoji(d.userName) + " " + d.formattedBody |
||||
color: TimelineManager.userColor(d.userId, palette.base) |
||||
body: d.body |
||||
isOnlyEmoji: d.isOnlyEmoji |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
metadataWidth: d.metadataWidth |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.ImageMessage |
||||
|
||||
ImageMessage { |
||||
type: d.type |
||||
originalWidth: d.originalWidth |
||||
proportionalHeight: d.proportionalHeight |
||||
url: d.url |
||||
blurhash: d.blurhash |
||||
body: d.body |
||||
filename: d.filename |
||||
isReply: d.isReply |
||||
eventId: d.eventId |
||||
metadataWidth: d.metadataWidth |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.Sticker |
||||
|
||||
ImageMessage { |
||||
type: d.type |
||||
originalWidth: d.originalWidth |
||||
proportionalHeight: d.proportionalHeight |
||||
url: d.url |
||||
blurhash: d.blurhash |
||||
body: d.body |
||||
filename: d.filename |
||||
isReply: d.isReply |
||||
eventId: d.eventId |
||||
metadataWidth: d.metadataWidth |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.FileMessage |
||||
|
||||
FileMessage { |
||||
eventId: d.eventId |
||||
filename: d.filename |
||||
filesize: d.filesize |
||||
metadataWidth: d.metadataWidth |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.VideoMessage |
||||
|
||||
PlayableMediaMessage { |
||||
proportionalHeight: d.proportionalHeight |
||||
type: d.type |
||||
originalWidth: d.originalWidth |
||||
thumbnailUrl: d.thumbnailUrl |
||||
eventId: d.eventId |
||||
url: d.url |
||||
body: d.body |
||||
filesize: d.filesize |
||||
duration: d.duration |
||||
metadataWidth: d.metadataWidth |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.AudioMessage |
||||
|
||||
PlayableMediaMessage { |
||||
proportionalHeight: d.proportionalHeight |
||||
type: d.type |
||||
originalWidth: d.originalWidth |
||||
thumbnailUrl: d.thumbnailUrl |
||||
eventId: d.eventId |
||||
url: d.url |
||||
body: d.body |
||||
filesize: d.filesize |
||||
duration: d.duration |
||||
metadataWidth: d.metadataWidth |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.Redacted |
||||
|
||||
Redacted { |
||||
metadataWidth: d.metadataWidth |
||||
} |
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.Redaction |
||||
|
||||
Pill { |
||||
text: qsTr("%1 removed a message").arg(d.userName) |
||||
isStateEvent: d.isStateEvent |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.Encryption |
||||
|
||||
EncryptionEnabled { |
||||
username: d.userName |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.Encrypted |
||||
|
||||
Encrypted { |
||||
encryptionError: d.encryptionError |
||||
eventId: d.eventId |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.ServerAcl |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: qsTr("%1 changed which servers are allowed in this room.").arg(d.userName) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.Name |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: d.roomName ? qsTr("%2 changed the room name to: %1").arg(d.roomName).arg(d.userName) : qsTr("%1 removed the room name").arg(d.userName) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.Topic |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: d.roomTopic ? qsTr("%2 changed the topic to: %1").arg(d.roomTopic).arg(d.userName): qsTr("%1 removed the topic").arg(d.userName) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.Avatar |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: qsTr("%1 changed the room avatar").arg(d.userName) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.PinnedEvents |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: qsTr("%1 changed the pinned messages.").arg(d.userName) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.ImagePackInRoom |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: d.relatedEventCacheBuster, room.formatImagePackEvent(d.eventId) |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.CanonicalAlias |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: qsTr("%1 changed the addresses for this room.").arg(d.userName) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.SpaceParent |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: qsTr("%1 changed the parent communities for this room.").arg(d.userName) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.RoomCreate |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: qsTr("%1 created and configured room: %2").arg(d.userName).arg(room.roomId) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.CallInvite |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: { |
||||
switch (d.callType) { |
||||
case "voice": |
||||
return qsTr("%1 placed a voice call.").arg(d.userName); |
||||
case "video": |
||||
return qsTr("%1 placed a video call.").arg(d.userName); |
||||
default: |
||||
return qsTr("%1 placed a call.").arg(d.userName); |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.CallAnswer |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: qsTr("%1 answered the call.").arg(d.userName) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.CallReject |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: qsTr("%1 rejected the call.").arg(d.userName) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.CallSelectAnswer |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: qsTr("%1 select answer").arg(d.userName) |
||||
// formatted: qsTr("Call answered elsewhere") |
||||
} |
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.CallHangUp |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: qsTr("%1 ended the call.").arg(d.userName) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.CallCandidates |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: qsTr("%1 is negotiating the call...").arg(d.userName) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.CallNegotiate |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: qsTr("%1 is negotiating the call...").arg(d.userName) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.PowerLevels |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: d.relatedEventCacheBuster, room.formatPowerLevelEvent(d.eventId) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.PolicyRuleUser |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: d.relatedEventCacheBuster, room.formatPolicyRule(d.eventId) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.PolicyRuleRoom |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: d.relatedEventCacheBuster, room.formatPolicyRule(d.eventId) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.PolicyRuleServer |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: d.relatedEventCacheBuster, room.formatPolicyRule(d.eventId) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.RoomJoinRules |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: d.relatedEventCacheBuster, room.formatJoinRuleEvent(d.eventId) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.RoomHistoryVisibility |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: d.relatedEventCacheBuster, room.formatHistoryVisibilityEvent(d.eventId) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.RoomGuestAccess |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: d.relatedEventCacheBuster, room.formatGuestAccessEvent(d.eventId) |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.Member |
||||
|
||||
ColumnLayout { |
||||
width: parent?.width ?? 100 |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
Layout.fillWidth: true |
||||
formatted: d.relatedEventCacheBuster, room.formatMemberEvent(d.eventId) |
||||
} |
||||
|
||||
Button { |
||||
visible: d.relatedEventCacheBuster, room.showAcceptKnockButton(d.eventId) |
||||
Layout.alignment: Qt.AlignHCenter |
||||
text: qsTr("Allow them in") |
||||
onClicked: room.acceptKnock(eventId) |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.KeyVerificationRequest |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: "KeyVerificationRequest" |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.KeyVerificationStart |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: "KeyVerificationStart" |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.KeyVerificationReady |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: "KeyVerificationReady" |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.KeyVerificationCancel |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: "KeyVerificationCancel" |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.KeyVerificationKey |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: "KeyVerificationKey" |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.KeyVerificationMac |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: "KeyVerificationMac" |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.KeyVerificationDone |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: "KeyVerificationDone" |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.KeyVerificationDone |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: "KeyVerificationDone" |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
roleValue: MtxEvent.KeyVerificationAccept |
||||
|
||||
NoticeMessage { |
||||
body: formatted |
||||
isOnlyEmoji: false |
||||
isReply: d.isReply |
||||
keepFullText: d.keepFullText |
||||
isStateEvent: d.isStateEvent |
||||
formatted: "KeyVerificationAccept" |
||||
} |
||||
|
||||
} |
||||
|
||||
DelegateChoice { |
||||
Placeholder { |
||||
typeString: d.typeString |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,356 @@ |
||||
// SPDX-FileCopyrightText: Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "EventDelegateChooser.h" |
||||
#include "TimelineModel.h" |
||||
|
||||
#include "Logging.h" |
||||
|
||||
#include <QQmlEngine> |
||||
#include <QtGlobal> |
||||
|
||||
#include <ranges> |
||||
|
||||
// privat qt headers to access required properties
|
||||
#include <QtQml/private/qqmlincubator_p.h> |
||||
#include <QtQml/private/qqmlobjectcreator_p.h> |
||||
|
||||
QQmlComponent * |
||||
EventDelegateChoice::delegate() const |
||||
{ |
||||
return delegate_; |
||||
} |
||||
|
||||
void |
||||
EventDelegateChoice::setDelegate(QQmlComponent *delegate) |
||||
{ |
||||
if (delegate != delegate_) { |
||||
delegate_ = delegate; |
||||
emit delegateChanged(); |
||||
emit changed(); |
||||
} |
||||
} |
||||
|
||||
QList<int> |
||||
EventDelegateChoice::roleValues() const |
||||
{ |
||||
return roleValues_; |
||||
} |
||||
|
||||
void |
||||
EventDelegateChoice::setRoleValues(const QList<int> &value) |
||||
{ |
||||
if (value != roleValues_) { |
||||
roleValues_ = value; |
||||
emit roleValuesChanged(); |
||||
emit changed(); |
||||
} |
||||
} |
||||
|
||||
QQmlListProperty<EventDelegateChoice> |
||||
EventDelegateChooser::choices() |
||||
{ |
||||
return QQmlListProperty<EventDelegateChoice>(this, |
||||
this, |
||||
&EventDelegateChooser::appendChoice, |
||||
&EventDelegateChooser::choiceCount, |
||||
&EventDelegateChooser::choice, |
||||
&EventDelegateChooser::clearChoices); |
||||
} |
||||
|
||||
void |
||||
EventDelegateChooser::appendChoice(QQmlListProperty<EventDelegateChoice> *p, EventDelegateChoice *c) |
||||
{ |
||||
EventDelegateChooser *dc = static_cast<EventDelegateChooser *>(p->object); |
||||
dc->choices_.append(c); |
||||
} |
||||
|
||||
qsizetype |
||||
EventDelegateChooser::choiceCount(QQmlListProperty<EventDelegateChoice> *p) |
||||
{ |
||||
return static_cast<EventDelegateChooser *>(p->object)->choices_.count(); |
||||
} |
||||
EventDelegateChoice * |
||||
EventDelegateChooser::choice(QQmlListProperty<EventDelegateChoice> *p, qsizetype index) |
||||
{ |
||||
return static_cast<EventDelegateChooser *>(p->object)->choices_.at(index); |
||||
} |
||||
void |
||||
EventDelegateChooser::clearChoices(QQmlListProperty<EventDelegateChoice> *p) |
||||
{ |
||||
static_cast<EventDelegateChooser *>(p->object)->choices_.clear(); |
||||
} |
||||
|
||||
void |
||||
EventDelegateChooser::componentComplete() |
||||
{ |
||||
QQuickItem::componentComplete(); |
||||
eventIncubator.reset(eventId_); |
||||
replyIncubator.reset(replyId); |
||||
// eventIncubator.forceCompletion();
|
||||
} |
||||
|
||||
void |
||||
EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) |
||||
{ |
||||
auto item = qobject_cast<QQuickItem *>(obj); |
||||
if (!item) |
||||
return; |
||||
|
||||
item->setParentItem(&chooser); |
||||
item->setParent(&chooser); |
||||
|
||||
auto roleNames = chooser.room_->roleNames(); |
||||
QHash<QByteArray, int> nameToRole; |
||||
for (const auto &[k, v] : roleNames.asKeyValueRange()) { |
||||
nameToRole.insert(v, k); |
||||
} |
||||
|
||||
QHash<int, int> roleToPropIdx; |
||||
std::vector<QModelRoleData> roles; |
||||
// Workaround for https://bugreports.qt.io/browse/QTBUG-98846
|
||||
QHash<QString, RequiredPropertyKey> requiredProperties; |
||||
for (const auto &[propKey, prop] : |
||||
QQmlIncubatorPrivate::get(this)->requiredProperties()->asKeyValueRange()) { |
||||
requiredProperties.insert(prop.propertyName, propKey); |
||||
} |
||||
|
||||
// collect required properties
|
||||
auto mo = obj->metaObject(); |
||||
for (int i = 0; i < mo->propertyCount(); i++) { |
||||
auto prop = mo->property(i); |
||||
// nhlog::ui()->critical("Found prop {}", prop.name());
|
||||
// See https://bugreports.qt.io/browse/QTBUG-98846
|
||||
if (!prop.isRequired() && !requiredProperties.contains(prop.name())) |
||||
continue; |
||||
|
||||
if (auto role = nameToRole.find(prop.name()); role != nameToRole.end()) { |
||||
roleToPropIdx.insert(*role, i); |
||||
roles.emplace_back(*role); |
||||
|
||||
// nhlog::ui()->critical("Found prop {}, idx {}, role {}", prop.name(), i, *role);
|
||||
} else { |
||||
nhlog::ui()->critical("Required property {} not found in model!", prop.name()); |
||||
} |
||||
} |
||||
|
||||
// nhlog::ui()->debug("Querying data for id {}", currentId.toStdString());
|
||||
chooser.room_->multiData(currentId, forReply ? chooser.eventId_ : QString(), roles); |
||||
|
||||
Qt::beginPropertyUpdateGroup(); |
||||
auto attached = qobject_cast<EventDelegateChooserAttachedType *>( |
||||
qmlAttachedPropertiesObject<EventDelegateChooser>(obj)); |
||||
Q_ASSERT(attached != nullptr); |
||||
attached->setIsReply(this->forReply); |
||||
|
||||
for (const auto &role : roles) { |
||||
const auto &roleName = roleNames[role.role()]; |
||||
// nhlog::ui()->critical("Setting role {}, {} to {}",
|
||||
// role.role(),
|
||||
// roleName.toStdString(),
|
||||
// role.data().toString().toStdString());
|
||||
|
||||
// nhlog::ui()->critical("Setting {}", mo->property(roleToPropIdx[role.role()]).name());
|
||||
mo->property(roleToPropIdx[role.role()]).write(obj, role.data()); |
||||
|
||||
if (const auto &req = requiredProperties.find(roleName); req != requiredProperties.end()) |
||||
QQmlIncubatorPrivate::get(this)->requiredProperties()->remove(*req); |
||||
} |
||||
|
||||
Qt::endPropertyUpdateGroup(); |
||||
|
||||
// setInitialProperties(rolesToSet);
|
||||
|
||||
auto update = |
||||
[this, obj, roleToPropIdx = std::move(roleToPropIdx)](const QList<int> &changedRoles) { |
||||
if (changedRoles.empty() || changedRoles.contains(TimelineModel::Roles::Type)) { |
||||
int type = chooser.room_ |
||||
->dataById(currentId, |
||||
TimelineModel::Roles::Type, |
||||
forReply ? chooser.eventId_ : QString()) |
||||
.toInt(); |
||||
if (type != oldType) { |
||||
// nhlog::ui()->debug("Type changed!");
|
||||
reset(currentId); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
std::vector<QModelRoleData> rolesToRequest; |
||||
|
||||
if (changedRoles.empty()) { |
||||
for (const auto role : |
||||
std::ranges::subrange(roleToPropIdx.keyBegin(), roleToPropIdx.keyEnd())) |
||||
rolesToRequest.emplace_back(role); |
||||
} else { |
||||
for (auto role : changedRoles) { |
||||
if (roleToPropIdx.contains(role)) { |
||||
rolesToRequest.emplace_back(role); |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (rolesToRequest.empty()) |
||||
return; |
||||
|
||||
auto mo = obj->metaObject(); |
||||
chooser.room_->multiData( |
||||
currentId, forReply ? chooser.eventId_ : QString(), rolesToRequest); |
||||
|
||||
Qt::beginPropertyUpdateGroup(); |
||||
for (const auto &role : rolesToRequest) { |
||||
mo->property(roleToPropIdx[role.role()]).write(obj, role.data()); |
||||
} |
||||
Qt::endPropertyUpdateGroup(); |
||||
}; |
||||
|
||||
if (!forReply) { |
||||
auto row = chooser.room_->idToIndex(currentId); |
||||
auto connection = connect( |
||||
chooser.room_, |
||||
&QAbstractItemModel::dataChanged, |
||||
obj, |
||||
[row, update](const QModelIndex &topLeft, |
||||
const QModelIndex &bottomRight, |
||||
const QList<int> &changedRoles) { |
||||
if (row < topLeft.row() || row > bottomRight.row()) |
||||
return; |
||||
|
||||
update(changedRoles); |
||||
}, |
||||
Qt::QueuedConnection); |
||||
connect(&this->chooser, &EventDelegateChooser::destroyed, obj, [connection]() { |
||||
QObject::disconnect(connection); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
void |
||||
EventDelegateChooser::DelegateIncubator::reset(QString id) |
||||
{ |
||||
if (!chooser.room_ || id.isEmpty()) |
||||
return; |
||||
|
||||
// nhlog::ui()->debug("Reset with id {}, reply {}", id.toStdString(), forReply);
|
||||
|
||||
this->currentId = id; |
||||
|
||||
auto role = |
||||
chooser.room_ |
||||
->dataById(id, TimelineModel::Roles::Type, forReply ? chooser.eventId_ : QString()) |
||||
.toInt(); |
||||
this->oldType = role; |
||||
|
||||
for (const auto choice : qAsConst(chooser.choices_)) { |
||||
const auto &choiceValue = choice->roleValues(); |
||||
if (choiceValue.contains(role) || choiceValue.empty()) { |
||||
// nhlog::ui()->debug(
|
||||
// "Instantiating type: {}, c {}", (int)role, choiceValue.contains(role));
|
||||
|
||||
if (auto child = qobject_cast<QQuickItem *>(object())) { |
||||
child->setParentItem(nullptr); |
||||
} |
||||
|
||||
choice->delegate()->create(*this, QQmlEngine::contextForObject(&chooser)); |
||||
return; |
||||
} |
||||
} |
||||
} |
||||
|
||||
void |
||||
EventDelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status) |
||||
{ |
||||
if (status == QQmlIncubator::Ready) { |
||||
auto child = qobject_cast<QQuickItem *>(object()); |
||||
if (child == nullptr) { |
||||
nhlog::ui()->error("Delegate has to be derived of Item!"); |
||||
return; |
||||
} |
||||
|
||||
child->setParentItem(&chooser); |
||||
QQmlEngine::setObjectOwnership(child, QQmlEngine::ObjectOwnership::JavaScriptOwnership); |
||||
|
||||
// connect(child, &QQuickItem::parentChanged, child, [child](QQuickItem *) {
|
||||
// // QTBUG-115687
|
||||
// if (child->flags().testFlag(QQuickItem::ItemObservesViewport)) {
|
||||
// nhlog::ui()->critical("SETTING OBSERVES VIEWPORT");
|
||||
// // Re-trigger the parent traversal to get subtreeTransformChangedEnabled turned
|
||||
// on child->setFlag(QQuickItem::ItemObservesViewport);
|
||||
// }
|
||||
// });
|
||||
|
||||
if (forReply) |
||||
emit chooser.replyChanged(); |
||||
else |
||||
emit chooser.mainChanged(); |
||||
|
||||
chooser.polish(); |
||||
} else if (status == QQmlIncubator::Error) { |
||||
auto errors_ = errors(); |
||||
for (const auto &e : qAsConst(errors_)) |
||||
nhlog::ui()->error("Error instantiating delegate: {}", e.toString().toStdString()); |
||||
} |
||||
} |
||||
|
||||
void |
||||
EventDelegateChooser::updatePolish() |
||||
{ |
||||
auto mainChild = qobject_cast<QQuickItem *>(eventIncubator.object()); |
||||
auto replyChild = qobject_cast<QQuickItem *>(replyIncubator.object()); |
||||
|
||||
// nhlog::ui()->trace("POLISHING {}", (void *)this);
|
||||
|
||||
auto layoutItem = [this](QQuickItem *item, int inset) { |
||||
if (item) { |
||||
auto attached = qobject_cast<EventDelegateChooserAttachedType *>( |
||||
qmlAttachedPropertiesObject<EventDelegateChooser>(item)); |
||||
Q_ASSERT(attached != nullptr); |
||||
|
||||
int maxWidth = maxWidth_ - inset; |
||||
|
||||
// in theory we could also reset the width, but that doesn't seem to work nicely for
|
||||
// text areas because of how they cache it.
|
||||
if (attached->maxWidth() > 0) |
||||
item->setWidth(attached->maxWidth()); |
||||
else |
||||
item->setWidth(maxWidth); |
||||
item->ensurePolished(); |
||||
auto width = item->implicitWidth(); |
||||
|
||||
if (width < 1 || width > maxWidth) |
||||
width = maxWidth; |
||||
|
||||
if (attached->maxWidth() > 0 && width > attached->maxWidth()) |
||||
width = attached->maxWidth(); |
||||
|
||||
if (attached->keepAspectRatio()) { |
||||
auto height = width * attached->aspectRatio(); |
||||
if (attached->maxHeight() && height > attached->maxHeight()) { |
||||
height = attached->maxHeight(); |
||||
width = height / attached->aspectRatio(); |
||||
} |
||||
|
||||
item->setHeight(height); |
||||
} |
||||
|
||||
item->setWidth(width); |
||||
item->ensurePolished(); |
||||
} |
||||
}; |
||||
|
||||
layoutItem(mainChild, mainInset_); |
||||
layoutItem(replyChild, replyInset_); |
||||
} |
||||
|
||||
void |
||||
EventDelegateChooserAttachedType::polishChooser() |
||||
{ |
||||
auto p = parent(); |
||||
if (p) { |
||||
auto chooser = qobject_cast<EventDelegateChooser *>(p->parent()); |
||||
if (chooser) { |
||||
chooser->polish(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,276 @@ |
||||
// SPDX-FileCopyrightText: Nheko Contributors
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once |
||||
|
||||
#include <QAbstractItemModel> |
||||
#include <QQmlComponent> |
||||
#include <QQmlIncubator> |
||||
#include <QQmlListProperty> |
||||
#include <QQuickItem> |
||||
#include <QtCore/QObject> |
||||
#include <QtCore/QVariant> |
||||
|
||||
#include "TimelineModel.h" |
||||
|
||||
class EventDelegateChooserAttachedType : public QObject |
||||
{ |
||||
Q_OBJECT |
||||
Q_PROPERTY(bool keepAspectRatio READ keepAspectRatio WRITE setKeepAspectRatio NOTIFY |
||||
keepAspectRatioChanged) |
||||
Q_PROPERTY(double aspectRatio READ aspectRatio WRITE setAspectRatio NOTIFY aspectRatioChanged) |
||||
Q_PROPERTY(int maxWidth READ maxWidth WRITE setMaxWidth NOTIFY maxWidthChanged) |
||||
Q_PROPERTY(int maxHeight READ maxHeight WRITE setMaxHeight NOTIFY maxHeightChanged) |
||||
Q_PROPERTY(bool isReply READ isReply WRITE setIsReply NOTIFY isReplyChanged) |
||||
|
||||
QML_ANONYMOUS |
||||
public: |
||||
EventDelegateChooserAttachedType(QObject *parent) |
||||
: QObject(parent) |
||||
{ |
||||
} |
||||
|
||||
bool keepAspectRatio() const { return keepAspectRatio_; } |
||||
void setKeepAspectRatio(bool fill) |
||||
{ |
||||
if (fill != keepAspectRatio_) { |
||||
keepAspectRatio_ = fill; |
||||
emit keepAspectRatioChanged(); |
||||
polishChooser(); |
||||
} |
||||
} |
||||
|
||||
double aspectRatio() const { return aspectRatio_; } |
||||
void setAspectRatio(double fill) |
||||
{ |
||||
aspectRatio_ = fill; |
||||
emit aspectRatioChanged(); |
||||
polishChooser(); |
||||
} |
||||
|
||||
int maxWidth() const { return maxWidth_; } |
||||
void setMaxWidth(int fill) |
||||
{ |
||||
maxWidth_ = fill; |
||||
emit maxWidthChanged(); |
||||
polishChooser(); |
||||
} |
||||
|
||||
int maxHeight() const { return maxHeight_; } |
||||
void setMaxHeight(int fill) |
||||
{ |
||||
maxHeight_ = fill; |
||||
emit maxHeightChanged(); |
||||
} |
||||
|
||||
bool isReply() const { return isReply_; } |
||||
void setIsReply(bool fill) |
||||
{ |
||||
if (fill != isReply_) { |
||||
isReply_ = fill; |
||||
emit isReplyChanged(); |
||||
polishChooser(); |
||||
} |
||||
} |
||||
|
||||
signals: |
||||
void keepAspectRatioChanged(); |
||||
void aspectRatioChanged(); |
||||
void maxWidthChanged(); |
||||
void maxHeightChanged(); |
||||
void isReplyChanged(); |
||||
|
||||
private: |
||||
void polishChooser(); |
||||
|
||||
double aspectRatio_ = 1.; |
||||
int maxWidth_ = -1; |
||||
int maxHeight_ = -1; |
||||
bool keepAspectRatio_ = false; |
||||
bool isReply_ = false; |
||||
}; |
||||
|
||||
class EventDelegateChoice : public QObject |
||||
{ |
||||
Q_OBJECT |
||||
QML_ELEMENT |
||||
Q_CLASSINFO("DefaultProperty", "delegate") |
||||
|
||||
public: |
||||
Q_PROPERTY(QList<int> roleValues READ roleValues WRITE setRoleValues NOTIFY roleValuesChanged |
||||
REQUIRED FINAL) |
||||
Q_PROPERTY( |
||||
QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged REQUIRED FINAL) |
||||
|
||||
[[nodiscard]] QQmlComponent *delegate() const; |
||||
void setDelegate(QQmlComponent *delegate); |
||||
|
||||
[[nodiscard]] QList<int> roleValues() const; |
||||
void setRoleValues(const QList<int> &value); |
||||
|
||||
signals: |
||||
void delegateChanged(); |
||||
void roleValuesChanged(); |
||||
void changed(); |
||||
|
||||
private: |
||||
QList<int> roleValues_; |
||||
QQmlComponent *delegate_ = nullptr; |
||||
}; |
||||
|
||||
class EventDelegateChooser : public QQuickItem |
||||
{ |
||||
Q_OBJECT |
||||
QML_ELEMENT |
||||
Q_CLASSINFO("DefaultProperty", "choices") |
||||
|
||||
QML_ATTACHED(EventDelegateChooserAttachedType) |
||||
|
||||
Q_PROPERTY(QQmlListProperty<EventDelegateChoice> choices READ choices CONSTANT FINAL) |
||||
Q_PROPERTY(QQuickItem *main READ main NOTIFY mainChanged FINAL) |
||||
Q_PROPERTY(QQuickItem *reply READ reply NOTIFY replyChanged FINAL) |
||||
Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged REQUIRED FINAL) |
||||
Q_PROPERTY(QString replyTo READ replyTo WRITE setReplyTo NOTIFY replyToChanged REQUIRED FINAL) |
||||
Q_PROPERTY(TimelineModel *room READ room WRITE setRoom NOTIFY roomChanged REQUIRED FINAL) |
||||
Q_PROPERTY(bool sameWidth READ sameWidth WRITE setSameWidth NOTIFY sameWidthChanged) |
||||
Q_PROPERTY(int maxWidth READ maxWidth WRITE setMaxWidth NOTIFY maxWidthChanged) |
||||
Q_PROPERTY(int replyInset READ replyInset WRITE setReplyInset NOTIFY replyInsetChanged) |
||||
Q_PROPERTY(int mainInset READ mainInset WRITE setMainInset NOTIFY mainInsetChanged) |
||||
|
||||
public: |
||||
QQmlListProperty<EventDelegateChoice> choices(); |
||||
|
||||
[[nodiscard]] QQuickItem *main() const |
||||
{ |
||||
return qobject_cast<QQuickItem *>(eventIncubator.object()); |
||||
} |
||||
[[nodiscard]] QQuickItem *reply() const |
||||
{ |
||||
return qobject_cast<QQuickItem *>(replyIncubator.object()); |
||||
} |
||||
|
||||
bool sameWidth() const { return sameWidth_; } |
||||
void setSameWidth(bool width) |
||||
{ |
||||
sameWidth_ = width; |
||||
emit sameWidthChanged(); |
||||
} |
||||
int maxWidth() const { return maxWidth_; } |
||||
void setMaxWidth(int width) |
||||
{ |
||||
maxWidth_ = width; |
||||
emit maxWidthChanged(); |
||||
polish(); |
||||
} |
||||
|
||||
int replyInset() const { return replyInset_; } |
||||
void setReplyInset(int width) |
||||
{ |
||||
replyInset_ = width; |
||||
emit replyInsetChanged(); |
||||
polish(); |
||||
} |
||||
|
||||
int mainInset() const { return mainInset_; } |
||||
void setMainInset(int width) |
||||
{ |
||||
mainInset_ = width; |
||||
emit mainInsetChanged(); |
||||
polish(); |
||||
} |
||||
|
||||
void setRoom(TimelineModel *m) |
||||
{ |
||||
if (m != room_) { |
||||
room_ = m; |
||||
emit roomChanged(); |
||||
|
||||
if (isComponentComplete()) { |
||||
eventIncubator.reset(eventId_); |
||||
replyIncubator.reset(replyId); |
||||
} |
||||
} |
||||
} |
||||
[[nodiscard]] TimelineModel *room() { return room_; } |
||||
|
||||
void setEventId(QString idx) |
||||
{ |
||||
eventId_ = idx; |
||||
emit eventIdChanged(); |
||||
|
||||
if (isComponentComplete()) |
||||
eventIncubator.reset(eventId_); |
||||
} |
||||
[[nodiscard]] QString eventId() const { return eventId_; } |
||||
void setReplyTo(QString id) |
||||
{ |
||||
replyId = id; |
||||
emit replyToChanged(); |
||||
|
||||
if (isComponentComplete()) |
||||
replyIncubator.reset(replyId); |
||||
} |
||||
[[nodiscard]] QString replyTo() const { return replyId; } |
||||
|
||||
void componentComplete() override; |
||||
|
||||
static EventDelegateChooserAttachedType *qmlAttachedProperties(QObject *object) |
||||
{ |
||||
return new EventDelegateChooserAttachedType(object); |
||||
} |
||||
|
||||
void updatePolish() override; |
||||
|
||||
signals: |
||||
void mainChanged(); |
||||
void replyChanged(); |
||||
void roomChanged(); |
||||
void eventIdChanged(); |
||||
void replyToChanged(); |
||||
void sameWidthChanged(); |
||||
void maxWidthChanged(); |
||||
void replyInsetChanged(); |
||||
void mainInsetChanged(); |
||||
|
||||
private: |
||||
struct DelegateIncubator final : public QQmlIncubator |
||||
{ |
||||
DelegateIncubator(EventDelegateChooser &parent, bool forReply) |
||||
: QQmlIncubator(QQmlIncubator::AsynchronousIfNested) |
||||
, chooser(parent) |
||||
, forReply(forReply) |
||||
{ |
||||
} |
||||
void setInitialState(QObject *object) override; |
||||
void statusChanged(QQmlIncubator::Status status) override; |
||||
|
||||
void reset(QString id); |
||||
|
||||
EventDelegateChooser &chooser; |
||||
bool forReply; |
||||
QString currentId; |
||||
|
||||
QString instantiatedId; |
||||
int instantiatedRole = -1; |
||||
QAbstractItemModel *instantiatedModel = nullptr; |
||||
int oldType = -1; |
||||
}; |
||||
|
||||
QVariant roleValue_; |
||||
QList<EventDelegateChoice *> choices_; |
||||
DelegateIncubator eventIncubator{*this, false}; |
||||
DelegateIncubator replyIncubator{*this, true}; |
||||
TimelineModel *room_{nullptr}; |
||||
QString eventId_; |
||||
QString replyId; |
||||
bool sameWidth_ = false; |
||||
int maxWidth_ = 400; |
||||
int replyInset_ = 0; |
||||
int mainInset_ = 0; |
||||
|
||||
static void appendChoice(QQmlListProperty<EventDelegateChoice> *, EventDelegateChoice *); |
||||
static qsizetype choiceCount(QQmlListProperty<EventDelegateChoice> *); |
||||
static EventDelegateChoice *choice(QQmlListProperty<EventDelegateChoice> *, qsizetype index); |
||||
static void clearChoices(QQmlListProperty<EventDelegateChoice> *); |
||||
}; |
Loading…
Reference in new issue