Basic bubble style

This commit is contained in:
Nicolas Werner 2023-10-09 21:28:39 +02:00
parent fc7a1bdfba
commit a86e364d1a
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
5 changed files with 445 additions and 71 deletions

View file

@ -703,6 +703,8 @@ set(QML_SOURCES
resources/qml/RoomList.qml resources/qml/RoomList.qml
resources/qml/TimelineSectionHeader.qml resources/qml/TimelineSectionHeader.qml
resources/qml/TimelineDefaultMessageStyle.qml resources/qml/TimelineDefaultMessageStyle.qml
resources/qml/TimelineBubbleMessageStyle.qml
resources/qml/TimelineMetadata.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

View file

@ -68,8 +68,17 @@ Item {
scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
} }
} }
Component {
id: bubbleMessageStyle
delegate: defaultMessageStyle TimelineBubbleMessageStyle {
messageActions: messageActionsC
messageContextMenu: messageContextMenuC
scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
}
}
delegate: Settings.bubbles ? bubbleMessageStyle : defaultMessageStyle
footer: Item { footer: Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.margins: Nheko.paddingLarge anchors.margins: Nheko.paddingLarge

View file

@ -0,0 +1,323 @@
// 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 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
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()
}
}
}
},
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 int metadataWidth: 100
property int metadataHeight: 20
property bool fitsMetadata: ((wrapper.main?.width ?? 0) + wrapper.mainInset + metadata.width) < wrapper.maxWidth
implicitWidth: Math.max((wrapper.reply?.width ?? 0) + wrapper.replyInset, (wrapper.main?.width ?? 0) + wrapper.mainInset + (fitsMetadata ? metadata.width : 0))
implicitHeight: contentColumn.implicitHeight + (fitsMetadata ? 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 / 5) + 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)
}
}
}
data: [replyRow, wrapper.main]
}
}
padding: 4
background: Rectangle {
color: !wrapper.isStateEvent ? Qt.tint(palette.base, Qt.hsla(messageBubble.userColor.hslHue, 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: 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
}
}
]
}

View file

@ -51,7 +51,7 @@ TimelineEvent {
property alias hovered: messageHover.hovered property alias hovered: messageHover.hovered
property bool scrolledToThis: false property bool scrolledToThis: false
mainInset: (threadId ? (4 + Nheko.paddingSmall) : 0) + 4 mainInset: (threadId ? (4 + Nheko.paddingSmall) : 0)
replyInset: mainInset + 4 + Nheko.paddingSmall replyInset: mainInset + 4 + Nheko.paddingSmall
maxWidth: chat.delegateMaxWidth - avatarMargin - metadata.width maxWidth: chat.delegateMaxWidth - avatarMargin - metadata.width
@ -269,82 +269,24 @@ TimelineEvent {
} }
}, },
RowLayout { TimelineMetadata {
id: metadata id: metadata
property int iconSize: Math.floor(fontMetrics.ascent * scaling) scaling: 1
property double scaling: Settings.bubbles ? 0.75 : 1
anchors.right: parent.right anchors.right: parent.right
y: section.visible && section.active ? section.y + section.height : 0 y: section.visible && section.active ? section.y + section.height : 0
spacing: 2 visible: !wrapper.isStateEvent
visible: !isStateEvent
StatusIndicator { eventId: wrapper.eventId
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter status: wrapper.status
eventId: wrapper.eventId trustlevel: wrapper.trustlevel
height: parent.iconSize isEdited: wrapper.isEdited
status: wrapper.status isEncrypted: wrapper.isEncrypted
width: parent.iconSize threadId: wrapper.threadId
} timestamp: wrapper.timestamp
Image { room: wrapper.room
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 { Reactions {
id: reactionRow id: reactionRow

View file

@ -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
}
}
}