mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-22 11:00:48 +03:00
Move message styles to their own files and work around hover not propagating to siblings
This commit is contained in:
parent
43c8e64ed3
commit
184806bf71
6 changed files with 530 additions and 467 deletions
|
@ -701,6 +701,8 @@ set(QML_SOURCES
|
||||||
resources/qml/ChatPage.qml
|
resources/qml/ChatPage.qml
|
||||||
resources/qml/CommunitiesList.qml
|
resources/qml/CommunitiesList.qml
|
||||||
resources/qml/RoomList.qml
|
resources/qml/RoomList.qml
|
||||||
|
resources/qml/TimelineSectionHeader.qml
|
||||||
|
resources/qml/TimelineDefaultMessageStyle.qml
|
||||||
resources/qml/TimelineView.qml
|
resources/qml/TimelineView.qml
|
||||||
resources/qml/Avatar.qml
|
resources/qml/Avatar.qml
|
||||||
resources/qml/Completer.qml
|
resources/qml/Completer.qml
|
||||||
|
|
|
@ -25,7 +25,7 @@ Item {
|
||||||
// HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
|
// HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
|
||||||
Connections {
|
Connections {
|
||||||
function onHideMenu() {
|
function onHideMenu() {
|
||||||
messageContextMenu.close();
|
messageContextMenuC.close();
|
||||||
replyContextMenu.close();
|
replyContextMenu.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,293 +59,9 @@ Item {
|
||||||
spacing: 2
|
spacing: 2
|
||||||
verticalLayoutDirection: ListView.BottomToTop
|
verticalLayoutDirection: ListView.BottomToTop
|
||||||
|
|
||||||
delegate: TimelineEvent {
|
delegate: TimelineDefaultMessageStyle {
|
||||||
id: wrapper
|
messageActions: messageActionsC
|
||||||
ListView.delayRemove: true
|
messageContextMenu: messageContextMenuC
|
||||||
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 type
|
|
||||||
required property bool isEditable
|
|
||||||
|
|
||||||
property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) // align bubble with section header
|
|
||||||
|
|
||||||
property alias hovered: messageHover.hovered
|
|
||||||
|
|
||||||
data: [
|
|
||||||
Loader {
|
|
||||||
id: section
|
|
||||||
|
|
||||||
property var day: wrapper.day
|
|
||||||
property bool isSender: wrapper.isSender
|
|
||||||
property bool isStateEvent: wrapper.isStateEvent
|
|
||||||
property int parentWidth: wrapper.width
|
|
||||||
property var previousMessageDay: wrapper.previousMessageDay
|
|
||||||
property bool previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent
|
|
||||||
property string previousMessageUserId: wrapper.previousMessageUserId
|
|
||||||
property date timestamp: wrapper.timestamp
|
|
||||||
property string userId: wrapper.userId
|
|
||||||
property string userName: wrapper.userName
|
|
||||||
property string userPowerlevel: wrapper.userPowerlevel
|
|
||||||
|
|
||||||
active: previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent !== isStateEvent
|
|
||||||
//asynchronous: true
|
|
||||||
sourceComponent: sectionHeader
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
RowLayout {
|
|
||||||
id: gridContainer
|
|
||||||
|
|
||||||
width: wrapper.width
|
|
||||||
y: section.visible && section.active ? section.y + section.height : 0
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.preferredWidth: wrapper.avatarMargin
|
|
||||||
}
|
|
||||||
|
|
||||||
AbstractButton {
|
|
||||||
ToolTip.delay: Nheko.tooltipDelay
|
|
||||||
ToolTip.text: qsTr("Part of a thread")
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
Layout.fillHeight: true
|
|
||||||
visible: wrapper.threadId
|
|
||||||
Layout.preferredWidth: 4
|
|
||||||
|
|
||||||
onClicked: room.thread = wrapper.threadId
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: threadLine
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
color: TimelineManager.userColor(wrapper.threadId, palette.base)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ColumnLayout {
|
|
||||||
id: contentColumn
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
AbstractButton {
|
|
||||||
id: replyRow
|
|
||||||
visible: wrapper.reply
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.maximumHeight: timelineView.height / 8
|
|
||||||
Layout.preferredWidth: replyRowLay.implicitWidth
|
|
||||||
Layout.preferredHeight: replyRowLay.implicitHeight
|
|
||||||
|
|
||||||
property color userColor: TimelineManager.userColor(wrapper.reply?.userId ?? '', palette.base)
|
|
||||||
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
contentItem: RowLayout {
|
|
||||||
id: replyRowLay
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: replyLine
|
|
||||||
Layout.fillHeight: true
|
|
||||||
color: replyRow.userColor
|
|
||||||
Layout.preferredWidth: 4
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
AbstractButton {
|
|
||||||
id: replyUserButton
|
|
||||||
Layout.fillWidth: true
|
|
||||||
contentItem: ElidedLabel {
|
|
||||||
id: userName_
|
|
||||||
fullText: wrapper.reply?.userName ?? ''
|
|
||||||
color: replyRow.userColor
|
|
||||||
textFormat: Text.RichText
|
|
||||||
width: parent.width
|
|
||||||
elideWidth: width
|
|
||||||
}
|
|
||||||
onClicked: 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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data: [
|
|
||||||
replyRow, wrapper.main,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
// spacer to fill width if needed
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
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.preferredWidth: implicitWidth
|
|
||||||
spacing: 2
|
|
||||||
visible: !isStateEvent
|
|
||||||
|
|
||||||
StatusIndicator {
|
|
||||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
|
||||||
eventId: wrapper.eventId
|
|
||||||
height: parent.iconSize
|
|
||||||
status: wrapper.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?" + ((wrapper.eventId == room.edit) ? palette.highlight : palette.buttonText)
|
|
||||||
sourceSize.height: parent.iconSize * Screen.devicePixelRatio
|
|
||||||
sourceSize.width: parent.iconSize * Screen.devicePixelRatio
|
|
||||||
visible: wrapper.isEdited || wrapper.eventId == 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(wrapper.threadId, palette.base)
|
|
||||||
height: parent.iconSize
|
|
||||||
image: ":/icons/icons/ui/thread.svg"
|
|
||||||
visible: wrapper.threadId
|
|
||||||
width: parent.iconSize
|
|
||||||
|
|
||||||
onClicked: room.thread = threadId
|
|
||||||
}
|
|
||||||
EncryptionIndicator {
|
|
||||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
|
||||||
encrypted: wrapper.isEncrypted
|
|
||||||
height: parent.iconSize
|
|
||||||
sourceSize.height: parent.iconSize * Screen.devicePixelRatio
|
|
||||||
sourceSize.width: parent.iconSize * Screen.devicePixelRatio
|
|
||||||
trust: wrapper.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(wrapper.timestamp, Qt.DefaultLocaleLongDate)
|
|
||||||
ToolTip.visible: ma.hovered
|
|
||||||
color: palette.inactive.text
|
|
||||||
font.pointSize: fontMetrics.font.pointSize * parent.scaling
|
|
||||||
text: wrapper.timestamp.toLocaleTimeString(Locale.ShortFormat)
|
|
||||||
|
|
||||||
HoverHandler {
|
|
||||||
id: ma
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Item {
|
|
||||||
id: messageActionsAnchor
|
|
||||||
anchors.fill: gridContainer
|
|
||||||
property alias hovered: messageHover.hovered
|
|
||||||
HoverHandler {
|
|
||||||
id: messageHover
|
|
||||||
onHoveredChanged: () => {
|
|
||||||
if (!Settings.mobileMode && hovered) {
|
|
||||||
if (!messageActions.hovered) {
|
|
||||||
messageActions.model = wrapper;
|
|
||||||
messageActions.attached = wrapper;
|
|
||||||
messageActions.anchors.bottomMargin = -gridContainer.y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Reactions {
|
|
||||||
id: reactionRow
|
|
||||||
|
|
||||||
eventId: wrapper.eventId
|
|
||||||
layoutDirection: row.bubbleOnRight ? 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 && (room.fullyReadEventId == wrapper.eventId))
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
left: parent.left
|
|
||||||
right: parent.right
|
|
||||||
top: reactionRow.bottom
|
|
||||||
topMargin: 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
footer: Item {
|
footer: Item {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
@ -380,7 +96,7 @@ Item {
|
||||||
source: room
|
source: room
|
||||||
}
|
}
|
||||||
Control {
|
Control {
|
||||||
id: messageActions
|
id: messageActionsC
|
||||||
|
|
||||||
property Item attached: null
|
property Item attached: null
|
||||||
// use comma to update on scroll
|
// use comma to update on scroll
|
||||||
|
@ -405,7 +121,7 @@ Item {
|
||||||
|
|
||||||
property var model
|
property var model
|
||||||
|
|
||||||
spacing: messageActions.padding
|
spacing: messageActionsC.padding
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: Settings.recentReactions
|
model: Settings.recentReactions
|
||||||
|
@ -542,7 +258,7 @@ Item {
|
||||||
image: ":/icons/icons/ui/options.svg"
|
image: ":/icons/icons/ui/options.svg"
|
||||||
width: 16
|
width: 16
|
||||||
|
|
||||||
onClicked: messageContextMenu.show(row.model.eventId, row.model.threadId, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton)
|
onClicked: messageContextMenuC.show(row.model.eventId, row.model.threadId, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -624,152 +340,9 @@ Item {
|
||||||
room.setCurrentIndex(room.currentIndex);
|
room.setCurrentIndex(room.currentIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Component {
|
|
||||||
id: sectionHeader
|
|
||||||
|
|
||||||
Column {
|
|
||||||
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.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
powerlevel: userPowerlevel
|
|
||||||
height: fontMetrics.lineSpacing
|
|
||||||
width: fontMetrics.lineSpacing
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Platform.Menu {
|
Platform.Menu {
|
||||||
id: messageContextMenu
|
id: messageContextMenuC
|
||||||
|
|
||||||
property string eventId
|
property string eventId
|
||||||
property int eventType
|
property int eventType
|
||||||
|
@ -824,22 +397,22 @@ Item {
|
||||||
|
|
||||||
onTriggered: function () {
|
onTriggered: function () {
|
||||||
topBar.searchString = "";
|
topBar.searchString = "";
|
||||||
room.showEvent(messageContextMenu.eventId);
|
room.showEvent(messageContextMenuC.eventId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
enabled: visible
|
enabled: visible
|
||||||
text: qsTr("&Copy")
|
text: qsTr("&Copy")
|
||||||
visible: messageContextMenu.text
|
visible: messageContextMenuC.text
|
||||||
|
|
||||||
onTriggered: Clipboard.text = messageContextMenu.text
|
onTriggered: Clipboard.text = messageContextMenuC.text
|
||||||
}
|
}
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
enabled: visible
|
enabled: visible
|
||||||
text: qsTr("Copy &link location")
|
text: qsTr("Copy &link location")
|
||||||
visible: messageContextMenu.link
|
visible: messageContextMenuC.link
|
||||||
|
|
||||||
onTriggered: Clipboard.text = messageContextMenu.link
|
onTriggered: Clipboard.text = messageContextMenuC.link
|
||||||
}
|
}
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
id: reactionOption
|
id: reactionOption
|
||||||
|
@ -848,7 +421,7 @@ Item {
|
||||||
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
|
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
|
||||||
|
|
||||||
onTriggered: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(null, room.roomId, function (plaintext, markdown) {
|
onTriggered: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(null, room.roomId, function (plaintext, markdown) {
|
||||||
room.input.reaction(messageContextMenu.eventId, plaintext);
|
room.input.reaction(messageContextMenuC.eventId, plaintext);
|
||||||
TimelineManager.focusMessageInput();
|
TimelineManager.focusMessageInput();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -856,41 +429,41 @@ Item {
|
||||||
text: qsTr("Repl&y")
|
text: qsTr("Repl&y")
|
||||||
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
|
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
|
||||||
|
|
||||||
onTriggered: room.reply = (messageContextMenu.eventId)
|
onTriggered: room.reply = (messageContextMenuC.eventId)
|
||||||
}
|
}
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
enabled: visible
|
enabled: visible
|
||||||
text: qsTr("&Edit")
|
text: qsTr("&Edit")
|
||||||
visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
|
visible: messageContextMenuC.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
|
||||||
|
|
||||||
onTriggered: room.edit = (messageContextMenu.eventId)
|
onTriggered: room.edit = (messageContextMenuC.eventId)
|
||||||
}
|
}
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
enabled: visible
|
enabled: visible
|
||||||
text: qsTr("&Thread")
|
text: qsTr("&Thread")
|
||||||
visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
|
visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
|
||||||
|
|
||||||
onTriggered: room.thread = (messageContextMenu.threadId || messageContextMenu.eventId)
|
onTriggered: room.thread = (messageContextMenuC.threadId || messageContextMenuC.eventId)
|
||||||
}
|
}
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
enabled: visible
|
enabled: visible
|
||||||
text: visible && room.pinnedMessages.includes(messageContextMenu.eventId) ? qsTr("Un&pin") : qsTr("&Pin")
|
text: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? qsTr("Un&pin") : qsTr("&Pin")
|
||||||
visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false)
|
visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false)
|
||||||
|
|
||||||
onTriggered: visible && room.pinnedMessages.includes(messageContextMenu.eventId) ? room.unpin(messageContextMenu.eventId) : room.pin(messageContextMenu.eventId)
|
onTriggered: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? room.unpin(messageContextMenuC.eventId) : room.pin(messageContextMenuC.eventId)
|
||||||
}
|
}
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
text: qsTr("&Read receipts")
|
text: qsTr("&Read receipts")
|
||||||
|
|
||||||
onTriggered: room.showReadReceipts(messageContextMenu.eventId)
|
onTriggered: room.showReadReceipts(messageContextMenuC.eventId)
|
||||||
}
|
}
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
text: qsTr("&Forward")
|
text: qsTr("&Forward")
|
||||||
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage
|
visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker || messageContextMenuC.eventType == MtxEvent.TextMessage || messageContextMenuC.eventType == MtxEvent.LocationMessage || messageContextMenuC.eventType == MtxEvent.EmoteMessage || messageContextMenuC.eventType == MtxEvent.NoticeMessage
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
|
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
|
||||||
forwardMess.setMessageEventId(messageContextMenu.eventId);
|
forwardMess.setMessageEventId(messageContextMenuC.eventId);
|
||||||
forwardMess.open();
|
forwardMess.open();
|
||||||
timelineRoot.destroyOnClose(forwardMess);
|
timelineRoot.destroyOnClose(forwardMess);
|
||||||
}
|
}
|
||||||
|
@ -901,23 +474,23 @@ Item {
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
text: qsTr("View raw message")
|
text: qsTr("View raw message")
|
||||||
|
|
||||||
onTriggered: room.viewRawMessage(messageContextMenu.eventId)
|
onTriggered: room.viewRawMessage(messageContextMenuC.eventId)
|
||||||
}
|
}
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
enabled: visible
|
enabled: visible
|
||||||
text: qsTr("View decrypted raw message")
|
text: qsTr("View decrypted raw message")
|
||||||
// TODO(Nico): Fix this still being iterated over, when using keyboard to select options
|
// TODO(Nico): Fix this still being iterated over, when using keyboard to select options
|
||||||
visible: messageContextMenu.isEncrypted
|
visible: messageContextMenuC.isEncrypted
|
||||||
|
|
||||||
onTriggered: room.viewDecryptedRawMessage(messageContextMenu.eventId)
|
onTriggered: room.viewDecryptedRawMessage(messageContextMenuC.eventId)
|
||||||
}
|
}
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
text: qsTr("Remo&ve message")
|
text: qsTr("Remo&ve message")
|
||||||
visible: (room ? room.permissions.canRedact() : false) || messageContextMenu.isSender
|
visible: (room ? room.permissions.canRedact() : false) || messageContextMenuC.isSender
|
||||||
|
|
||||||
onTriggered: function () {
|
onTriggered: function () {
|
||||||
var dialog = removeReason.createObject(timelineRoot);
|
var dialog = removeReason.createObject(timelineRoot);
|
||||||
dialog.eventId = messageContextMenu.eventId;
|
dialog.eventId = messageContextMenuC.eventId;
|
||||||
dialog.show();
|
dialog.show();
|
||||||
dialog.forceActiveFocus();
|
dialog.forceActiveFocus();
|
||||||
timelineRoot.destroyOnClose(dialog);
|
timelineRoot.destroyOnClose(dialog);
|
||||||
|
@ -926,23 +499,23 @@ Item {
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
enabled: visible
|
enabled: visible
|
||||||
text: qsTr("&Save as")
|
text: qsTr("&Save as")
|
||||||
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
|
visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker
|
||||||
|
|
||||||
onTriggered: room.saveMedia(messageContextMenu.eventId)
|
onTriggered: room.saveMedia(messageContextMenuC.eventId)
|
||||||
}
|
}
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
enabled: visible
|
enabled: visible
|
||||||
text: qsTr("&Open in external program")
|
text: qsTr("&Open in external program")
|
||||||
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
|
visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker
|
||||||
|
|
||||||
onTriggered: room.openMedia(messageContextMenu.eventId)
|
onTriggered: room.openMedia(messageContextMenuC.eventId)
|
||||||
}
|
}
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
enabled: visible
|
enabled: visible
|
||||||
text: qsTr("Copy link to eve&nt")
|
text: qsTr("Copy link to eve&nt")
|
||||||
visible: messageContextMenu.eventId
|
visible: messageContextMenuC.eventId
|
||||||
|
|
||||||
onTriggered: room.copyLinkToEvent(messageContextMenu.eventId)
|
onTriggered: room.copyLinkToEvent(messageContextMenuC.eventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Component {
|
Component {
|
||||||
|
|
320
resources/qml/TimelineDefaultMessageStyle.qml
Normal file
320
resources/qml/TimelineDefaultMessageStyle.qml
Normal file
|
@ -0,0 +1,320 @@
|
||||||
|
// 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 type
|
||||||
|
required property bool isEditable
|
||||||
|
|
||||||
|
required property QtObject messageContextMenu
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RowLayout {
|
||||||
|
id: gridContainer
|
||||||
|
|
||||||
|
width: wrapper.width
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.preferredWidth: wrapper.avatarMargin
|
||||||
|
}
|
||||||
|
|
||||||
|
AbstractButton {
|
||||||
|
ToolTip.delay: Nheko.tooltipDelay
|
||||||
|
ToolTip.text: qsTr("Part of a thread")
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
Layout.fillHeight: true
|
||||||
|
visible: wrapper.threadId
|
||||||
|
Layout.preferredWidth: 4
|
||||||
|
|
||||||
|
onClicked: wrapper.room.thread = wrapper.threadId
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: threadLine
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
color: TimelineManager.userColor(wrapper.threadId, palette.base)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ColumnLayout {
|
||||||
|
id: contentColumn
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
AbstractButton {
|
||||||
|
id: replyRow
|
||||||
|
visible: wrapper.reply
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumHeight: timelineView.height / 8
|
||||||
|
Layout.preferredWidth: replyRowLay.implicitWidth
|
||||||
|
Layout.preferredHeight: replyRowLay.implicitHeight
|
||||||
|
|
||||||
|
property color userColor: TimelineManager.userColor(wrapper.reply?.userId ?? '', palette.base)
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
NhekoCursorShape {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: RowLayout {
|
||||||
|
id: replyRowLay
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: replyLine
|
||||||
|
Layout.fillHeight: true
|
||||||
|
color: replyRow.userColor
|
||||||
|
Layout.preferredWidth: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
AbstractButton {
|
||||||
|
id: replyUserButton
|
||||||
|
Layout.fillWidth: true
|
||||||
|
contentItem: ElidedLabel {
|
||||||
|
id: userName_
|
||||||
|
fullText: wrapper.reply?.userName ?? ''
|
||||||
|
color: replyRow.userColor
|
||||||
|
textFormat: Text.RichText
|
||||||
|
width: parent.width
|
||||||
|
elideWidth: width
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data: [
|
||||||
|
replyRow, wrapper.main,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
// spacer to fill width if needed
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
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.preferredWidth: implicitWidth
|
||||||
|
spacing: 2
|
||||||
|
visible: !isStateEvent
|
||||||
|
|
||||||
|
StatusIndicator {
|
||||||
|
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||||
|
eventId: wrapper.eventId
|
||||||
|
height: parent.iconSize
|
||||||
|
status: wrapper.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?" + ((wrapper.eventId == wrapper.room.edit) ? palette.highlight : palette.buttonText)
|
||||||
|
sourceSize.height: parent.iconSize * Screen.devicePixelRatio
|
||||||
|
sourceSize.width: parent.iconSize * Screen.devicePixelRatio
|
||||||
|
visible: wrapper.isEdited || wrapper.eventId == wrapper.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(wrapper.threadId, palette.base)
|
||||||
|
height: parent.iconSize
|
||||||
|
image: ":/icons/icons/ui/thread.svg"
|
||||||
|
visible: wrapper.threadId
|
||||||
|
width: parent.iconSize
|
||||||
|
|
||||||
|
onClicked: wrapper.room.thread = threadId
|
||||||
|
}
|
||||||
|
EncryptionIndicator {
|
||||||
|
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||||
|
encrypted: wrapper.isEncrypted
|
||||||
|
height: parent.iconSize
|
||||||
|
sourceSize.height: parent.iconSize * Screen.devicePixelRatio
|
||||||
|
sourceSize.width: parent.iconSize * Screen.devicePixelRatio
|
||||||
|
trust: wrapper.trustlevel
|
||||||
|
visible: wrapper.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(wrapper.timestamp, Qt.DefaultLocaleLongDate)
|
||||||
|
ToolTip.visible: ma.hovered
|
||||||
|
color: palette.inactive.text
|
||||||
|
font.pointSize: fontMetrics.font.pointSize * parent.scaling
|
||||||
|
text: wrapper.timestamp.toLocaleTimeString(Locale.ShortFormat)
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: ma
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Reactions {
|
||||||
|
id: reactionRow
|
||||||
|
|
||||||
|
eventId: wrapper.eventId
|
||||||
|
layoutDirection: row.bubbleOnRight ? 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
164
resources/qml/TimelineSectionHeader.qml
Normal file
164
resources/qml/TimelineSectionHeader.qml
Normal file
|
@ -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.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
powerlevel: userPowerlevel
|
||||||
|
height: fontMetrics.lineSpacing
|
||||||
|
width: fontMetrics.lineSpacing
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -130,21 +130,23 @@ AbstractButton {
|
||||||
id: mxcimage
|
id: mxcimage
|
||||||
|
|
||||||
visible: loaded
|
visible: loaded
|
||||||
anchors.fill: parent
|
|
||||||
roomm: room
|
roomm: room
|
||||||
play: !Settings.animateImagesOnHover || parent.hovered
|
play: !Settings.animateImagesOnHover || parent.hovered
|
||||||
eventId: parent.eventId
|
eventId: parent.eventId
|
||||||
|
width: parent.implicitWidth
|
||||||
|
height: parent.implicitHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: blurhash_
|
id: blurhash_
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
source: blurhash ? ("image://blurhash/" + blurhash) : ("image://colorimage/:/icons/icons/ui/image-failed.svg?" + palette.buttonText)
|
source: blurhash ? ("image://blurhash/" + blurhash) : ("image://colorimage/:/icons/icons/ui/image-failed.svg?" + palette.buttonText)
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
sourceSize.width: parent.width * Screen.devicePixelRatio
|
sourceSize.width: parent.implicitWidth * Screen.devicePixelRatio
|
||||||
sourceSize.height: parent.height * Screen.devicePixelRatio
|
sourceSize.height: parent.implicitHeight * Screen.devicePixelRatio
|
||||||
|
width: parent.implicitWidth
|
||||||
|
height: parent.implicitHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: Settings.openImageExternal ? room.openMedia(eventId) : TimelineManager.openImageOverlay(room, url, eventId, originalWidth, proportionalHeight);
|
onClicked: Settings.openImageExternal ? room.openMedia(eventId) : TimelineManager.openImageOverlay(room, url, eventId, originalWidth, proportionalHeight);
|
||||||
|
@ -152,7 +154,8 @@ AbstractButton {
|
||||||
Item {
|
Item {
|
||||||
id: overlay
|
id: overlay
|
||||||
|
|
||||||
anchors.fill: parent
|
width: parent.implicitWidth
|
||||||
|
height: parent.implicitHeight
|
||||||
visible: parent.hovered
|
visible: parent.hovered
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|
|
@ -44,7 +44,8 @@ MatrixText {
|
||||||
Layout.maximumHeight: !keepFullText ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : implicitHeight
|
Layout.maximumHeight: !keepFullText ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : implicitHeight
|
||||||
clip: !keepFullText
|
clip: !keepFullText
|
||||||
selectByMouse: !Settings.mobileMode && !isReply
|
selectByMouse: !Settings.mobileMode && !isReply
|
||||||
enabled: !Settings.mobileMode
|
enabled: !Settings.mobileMode && !isReply
|
||||||
|
hoverEnabled: !Settings.mobileMode
|
||||||
font.pointSize: (Settings.enlargeEmojiOnlyMessages && isOnlyEmoji > 0 && isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
|
font.pointSize: (Settings.enlargeEmojiOnlyMessages && isOnlyEmoji > 0 && isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
|
||||||
|
|
||||||
NhekoCursorShape {
|
NhekoCursorShape {
|
||||||
|
|
Loading…
Reference in a new issue