Merge branch 'delegate-rework'

This commit is contained in:
Nicolas Werner 2023-10-10 00:33:39 +02:00
commit 3a0d5788e1
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
36 changed files with 2356 additions and 1897 deletions

View file

@ -106,7 +106,6 @@ build-tw:
"pkgconfig" "pkgconfig"
"spdlog-devel" "spdlog-devel"
"zlib-devel" "zlib-devel"
"libQt5PlatformHeaders-devel"
"cmake(re2)" "cmake(re2)"
"cmake(Qt6Core)" "cmake(Qt6Core)"
"cmake(Qt6DBus)" "cmake(Qt6DBus)"
@ -117,6 +116,7 @@ build-tw:
"cmake(Qt6Svg)" "cmake(Qt6Svg)"
"cmake(Qt6Widgets)" "cmake(Qt6Widgets)"
"cmake(Qt6Gui)" "cmake(Qt6Gui)"
"qt6-qml-private-devel"
"pkgconfig(libcurl)" "pkgconfig(libcurl)"
"pkgconfig(libevent)" "pkgconfig(libevent)"
"pkgconfig(gstreamer-webrtc-1.0)" "pkgconfig(gstreamer-webrtc-1.0)"

View file

@ -357,6 +357,8 @@ set(SRC_FILES
src/timeline/DelegateChooser.h src/timeline/DelegateChooser.h
src/timeline/EventStore.cpp src/timeline/EventStore.cpp
src/timeline/EventStore.h src/timeline/EventStore.h
src/timeline/EventDelegateChooser.cpp
src/timeline/EventDelegateChooser.h
src/timeline/InputBar.cpp src/timeline/InputBar.cpp
src/timeline/InputBar.h src/timeline/InputBar.h
src/timeline/Permissions.cpp src/timeline/Permissions.cpp
@ -693,7 +695,6 @@ 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/TimelineView.qml
resources/qml/Avatar.qml resources/qml/Avatar.qml
resources/qml/Completer.qml resources/qml/Completer.qml
resources/qml/EncryptionIndicator.qml resources/qml/EncryptionIndicator.qml
@ -709,7 +710,12 @@ set(QML_SOURCES
resources/qml/Reactions.qml resources/qml/Reactions.qml
resources/qml/ReplyPopup.qml resources/qml/ReplyPopup.qml
resources/qml/StatusIndicator.qml resources/qml/StatusIndicator.qml
resources/qml/TimelineRow.qml resources/qml/TimelineEvent.qml
resources/qml/TimelineSectionHeader.qml
resources/qml/TimelineDefaultMessageStyle.qml
resources/qml/TimelineBubbleMessageStyle.qml
resources/qml/TimelineMetadata.qml
resources/qml/TimelineView.qml
resources/qml/TopBar.qml resources/qml/TopBar.qml
resources/qml/QuickSwitcher.qml resources/qml/QuickSwitcher.qml
resources/qml/ForwardCompleter.qml resources/qml/ForwardCompleter.qml
@ -731,7 +737,6 @@ set(QML_SOURCES
resources/qml/delegates/Encrypted.qml resources/qml/delegates/Encrypted.qml
resources/qml/delegates/FileMessage.qml resources/qml/delegates/FileMessage.qml
resources/qml/delegates/ImageMessage.qml resources/qml/delegates/ImageMessage.qml
resources/qml/delegates/MessageDelegate.qml
resources/qml/delegates/NoticeMessage.qml resources/qml/delegates/NoticeMessage.qml
resources/qml/delegates/Pill.qml resources/qml/delegates/Pill.qml
resources/qml/delegates/Placeholder.qml resources/qml/delegates/Placeholder.qml
@ -874,6 +879,7 @@ target_link_libraries(nheko PRIVATE
Qt::Gui Qt::Gui
Qt::Multimedia Qt::Multimedia
Qt::Qml Qt::Qml
Qt::QmlPrivate
Qt::QuickControls2 Qt::QuickControls2
qt6keychain qt6keychain
nlohmann_json::nlohmann_json nlohmann_json::nlohmann_json
@ -966,3 +972,4 @@ if(UNIX AND NOT APPLE)
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake) COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake)
endif() endif()
endif() endif()
# vim: tabstop=4 shiftwidth=4 expandtab

View file

@ -145,7 +145,6 @@ Control {
roleValue: "user" roleValue: "user"
RowLayout { RowLayout {
anchors.centerIn: centerRowContent ? parent : undefined anchors.centerIn: centerRowContent ? parent : undefined
spacing: rowSpacing spacing: rowSpacing
@ -171,7 +170,6 @@ Control {
roleValue: "emoji" roleValue: "emoji"
RowLayout { RowLayout {
anchors.centerIn: parent anchors.centerIn: parent
spacing: rowSpacing spacing: rowSpacing
@ -207,7 +205,6 @@ Control {
roleValue: "command" roleValue: "command"
RowLayout { RowLayout {
anchors.centerIn: parent anchors.centerIn: parent
spacing: rowSpacing spacing: rowSpacing
@ -226,7 +223,6 @@ Control {
roleValue: "room" roleValue: "room"
RowLayout { RowLayout {
anchors.centerIn: centerRowContent ? parent : undefined anchors.centerIn: centerRowContent ? parent : undefined
spacing: rowSpacing spacing: rowSpacing
@ -251,7 +247,6 @@ Control {
roleValue: "roomAliases" roleValue: "roomAliases"
RowLayout { RowLayout {
anchors.centerIn: parent anchors.centerIn: parent
spacing: rowSpacing spacing: rowSpacing

View file

@ -54,25 +54,9 @@ Popup {
Reply { Reply {
id: replyPreview id: replyPreview
property var modelData: room ? room.getDump(mid, "") : {} eventId: mid
blurhash: modelData.blurhash ?? ""
body: modelData.body ?? ""
encryptionError: modelData.encryptionError ?? ""
eventId: modelData.eventId ?? ""
filename: modelData.filename ?? ""
filesize: modelData.filesize ?? ""
formattedBody: modelData.formattedBody ?? ""
isOnlyEmoji: modelData.isOnlyEmoji ?? false
originalWidth: modelData.originalWidth ?? 0
proportionalHeight: modelData.proportionalHeight ?? 1
type: modelData.type ?? MtxEvent.UnknownMessage
typeString: modelData.typeString ?? ""
url: modelData.url ?? ""
userColor: TimelineManager.userColor(modelData.userId, palette.window) userColor: TimelineManager.userColor(modelData.userId, palette.window)
userId: modelData.userId ?? "" maxWidth: parent.width
userName: modelData.userName ?? ""
width: parent.width
} }
MatrixTextField { MatrixTextField {
id: roomTextInput id: roomTextInput

View file

@ -4,33 +4,33 @@
// TODO: using any Qt 6 API version will screw up the reply text color. We need to // TODO: using any Qt 6 API version will screw up the reply text color. We need to
// figure out a more permanent fix than just importing the old version. // figure out a more permanent fix than just importing the old version.
import QtQuick 2.15 //import QtQuick 2.15
import QtQuick
import QtQuick.Controls import QtQuick.Controls
import im.nheko import im.nheko
TextEdit { TextArea {
id: r id: r
property alias cursorShape: cs.cursorShape property alias cursorShape: cs.cursorShape
//leftInset: 0
//bottomInset: 0
//rightInset: 0
//topInset: 0
//leftPadding: 0
//bottomPadding: 0
//rightPadding: 0
//topPadding: 0
//background: null
ToolTip.text: hoveredLink ToolTip.text: hoveredLink
ToolTip.visible: hoveredLink || false ToolTip.visible: hoveredLink || false
background: null
bottomInset: 0
bottomPadding: 0
// this always has to be enabled, otherwise you can't click links anymore! // this always has to be enabled, otherwise you can't click links anymore!
//enabled: selectByMouse //enabled: selectByMouse
color: palette.text color: palette.text
focus: false focus: false
leftInset: 0
leftPadding: 0
readOnly: true readOnly: true
rightInset: 0
rightPadding: 0
textFormat: TextEdit.RichText textFormat: TextEdit.RichText
topInset: 0
topPadding: 0
wrapMode: Text.Wrap wrapMode: Text.Wrap
// Setting a tooltip delay makes the hover text empty .-. // Setting a tooltip delay makes the hover text empty .-.
@ -40,9 +40,9 @@ TextEdit {
} }
onLinkActivated: Nheko.openLink(link) onLinkActivated: Nheko.openLink(link)
//// propagate events up // propagate events up
//onPressAndHold: (event) => event.accepted = false onPressAndHold: event => event.accepted = false
//onPressed: (event) => event.accepted = (event.button == Qt.LeftButton) onPressed: event => event.accepted = (event.button == Qt.LeftButton)
NhekoCursorShape { NhekoCursorShape {
id: cs id: cs

View file

@ -20,12 +20,13 @@ Item {
property int availableWidth: width property int availableWidth: width
property int padding: Nheko.paddingMedium property int padding: Nheko.paddingMedium
property string searchString: "" property string searchString: ""
property Room roommodel: room
// 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(); replyContextMenuC.close();
} }
target: MainWindow target: MainWindow
@ -51,182 +52,35 @@ Item {
//onModelChanged: if (room) room.sendReset() //onModelChanged: if (room) room.sendReset()
//reuseItems: true //reuseItems: true
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
displayMarginBeginning: height / 2 displayMarginBeginning: height / 4
displayMarginEnd: height / 2 displayMarginEnd: height / 4
model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room
//pixelAligned: true //pixelAligned: true
spacing: 2 spacing: 2
verticalLayoutDirection: ListView.BottomToTop verticalLayoutDirection: ListView.BottomToTop
delegate: Item { Component {
id: wrapper id: defaultMessageStyle
required property string blurhash TimelineDefaultMessageStyle {
required property string body messageActions: messageActionsC
required property string callType messageContextMenu: messageContextMenuC
required property var day replyContextMenu: replyContextMenuC
required property string duration scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
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
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 double proportionalHeight
required property var reactions
required property int relatedEventCacheBuster
required property string replyTo
required property string roomName
required property string roomTopic
property bool scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
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
required property int userPowerlevel
ListView.delayRemove: true
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
height: (section.item?.height ?? 0) + timelinerow.height
width: chat.delegateMaxWidth
Loader {
id: section
property var day: wrapper.day
property bool isSender: wrapper.isSender
property bool isStateEvent: wrapper.isStateEvent
property int parentWidth: parent.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 int userPowerlevel: wrapper.userPowerlevel
active: previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent !== isStateEvent
//asynchronous: true
sourceComponent: sectionHeader
visible: status == Loader.Ready
z: 4
}
TimelineRow {
id: timelinerow
blurhash: wrapper.blurhash
body: wrapper.body
callType: wrapper.callType
duration: wrapper.duration
encryptionError: wrapper.encryptionError
eventId: chat.model, wrapper.eventId
filename: wrapper.filename
filesize: wrapper.filesize
formattedBody: wrapper.formattedBody
index: wrapper.index
isEditable: wrapper.isEditable
isEdited: wrapper.isEdited
isEncrypted: wrapper.isEncrypted
isOnlyEmoji: wrapper.isOnlyEmoji
isSender: wrapper.isSender
isStateEvent: wrapper.isStateEvent
notificationlevel: wrapper.notificationlevel
originalWidth: wrapper.originalWidth
proportionalHeight: wrapper.proportionalHeight
reactions: wrapper.reactions
relatedEventCacheBuster: wrapper.relatedEventCacheBuster
replyTo: wrapper.replyTo
roomName: wrapper.roomName
roomTopic: wrapper.roomTopic
status: wrapper.status
threadId: wrapper.threadId
thumbnailUrl: wrapper.thumbnailUrl
timestamp: wrapper.timestamp
trustlevel: wrapper.trustlevel
type: chat.model, wrapper.type
typeString: wrapper.typeString
url: wrapper.url
userId: wrapper.userId
userName: wrapper.userName
width: wrapper.width
y: section.visible && section.active ? section.y + section.height : 0
background: Rectangle {
id: scrollHighlight
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: room.eventShown()
}
}
}
}
onHoveredChanged: {
if (!Settings.mobileMode && hovered) {
if (!messageActions.hovered) {
messageActions.attached = timelinerow;
messageActions.model = timelinerow;
}
}
}
}
Connections {
function onMovementEnded() {
if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
chat.model.currentIndex = index;
}
target: chat
} }
} }
Component {
id: bubbleMessageStyle
TimelineBubbleMessageStyle {
messageActions: messageActionsC
messageContextMenu: messageContextMenuC
replyContextMenu: replyContextMenuC
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
@ -260,19 +114,19 @@ 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
property var attachedPos: chat.contentY, attached ? chat.mapFromItem(attached, attached ? attached.width - width : 0, -height) : null
property alias model: row.model property alias model: row.model
hoverEnabled: true hoverEnabled: true
padding: Nheko.paddingSmall padding: Nheko.paddingSmall
visible: Settings.buttonsInTimeline && !!attached && (attached.hovered || hovered) visible: Settings.buttonsInTimeline && !!attached && (attached.hovered || hovered)
x: attached ? attachedPos.x : 0
y: attached ? attachedPos.y + Nheko.paddingSmall : 0
z: 10 z: 10
parent: chat.contentItem
anchors.bottom: attached?.top
anchors.right: attached?.right
background: Rectangle { background: Rectangle {
border.color: palette.buttonText border.color: palette.buttonText
@ -285,7 +139,7 @@ Item {
property var model property var model
spacing: messageActions.padding spacing: messageActionsC.padding
Repeater { Repeater {
model: Settings.recentReactions model: Settings.recentReactions
@ -422,7 +276,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)
} }
} }
} }
@ -504,148 +358,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.verticalCenter: parent.verticalCenter
height: fontMetrics.ascent
width: height
powerlevel: userPowerlevel
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
@ -700,22 +415,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
@ -724,7 +439,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();
}) })
} }
@ -732,41 +447,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);
} }
@ -777,23 +492,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);
@ -802,23 +517,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 {
@ -828,7 +543,7 @@ Item {
} }
} }
Platform.Menu { Platform.Menu {
id: replyContextMenu id: replyContextMenuC
property string eventId property string eventId
property string link property string link
@ -844,23 +559,23 @@ Item {
Platform.MenuItem { Platform.MenuItem {
enabled: visible enabled: visible
text: qsTr("&Copy") text: qsTr("&Copy")
visible: replyContextMenu.text visible: replyContextMenuC.text
onTriggered: Clipboard.text = replyContextMenu.text onTriggered: Clipboard.text = replyContextMenuC.text
} }
Platform.MenuItem { Platform.MenuItem {
enabled: visible enabled: visible
text: qsTr("Copy &link location") text: qsTr("Copy &link location")
visible: replyContextMenu.link visible: replyContextMenuC.link
onTriggered: Clipboard.text = replyContextMenu.link onTriggered: Clipboard.text = replyContextMenuC.link
} }
Platform.MenuItem { Platform.MenuItem {
enabled: visible enabled: visible
text: qsTr("&Go to quoted message") text: qsTr("&Go to quoted message")
visible: true visible: true
onTriggered: room.showEvent(replyContextMenu.eventId) onTriggered: room.showEvent(replyContextMenuC.eventId)
} }
} }
RoundButton { RoundButton {

View file

@ -74,10 +74,10 @@ Flow {
anchors.verticalCenter: divider.verticalCenter anchors.verticalCenter: divider.verticalCenter
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
height: textMetrics.height height: textMetrics.height
mipmap: true
source: modelData.key.startsWith("mxc://") ? (modelData.key.replace("mxc://", "image://MxcImage/") + "?scale") : "" source: modelData.key.startsWith("mxc://") ? (modelData.key.replace("mxc://", "image://MxcImage/") + "?scale") : ""
visible: modelData.key.startsWith("mxc://") visible: modelData.key.startsWith("mxc://")
width: textMetrics.height width: textMetrics.height
mipmap: true
} }
Rectangle { Rectangle {
id: divider id: divider

View file

@ -29,24 +29,10 @@ Rectangle {
anchors.rightMargin: replyPopup.width < 450 ? 2 * (22 + 16) : 3 * (22 + 16) anchors.rightMargin: replyPopup.width < 450 ? 2 * (22 + 16) : 3 * (22 + 16)
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: Nheko.paddingSmall anchors.topMargin: Nheko.paddingSmall
blurhash: modelData.blurhash ?? "" eventId: room.reply ?? ""
body: modelData.body ?? ""
encryptionError: modelData.encryptionError ?? 0
eventId: modelData.eventId ?? ""
filename: modelData.filename ?? ""
filesize: modelData.filesize ?? ""
formattedBody: modelData.formattedBody ?? ""
isOnlyEmoji: modelData.isOnlyEmoji ?? false
originalWidth: modelData.originalWidth ?? 0
proportionalHeight: modelData.proportionalHeight ?? 1
type: modelData.type ?? MtxEvent.UnknownMessage
typeString: modelData.typeString ?? ""
url: modelData.url ?? ""
userColor: TimelineManager.userColor(modelData.userId, palette.window) userColor: TimelineManager.userColor(modelData.userId, palette.window)
userId: modelData.userId ?? ""
userName: modelData.userName ?? ""
visible: room && room.reply visible: room && room.reply
width: parent.width maxWidth: parent.width - anchors.leftMargin - anchors.rightMargin
} }
ImageButton { ImageButton {
id: closeReplyButton id: closeReplyButton

View file

@ -728,9 +728,9 @@ Page {
} }
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Mark as read") text: qsTr("Mark as read")
onTriggered: Rooms.getRoomById(roomContextMenu.roomid).markRoomAsRead() onTriggered: Rooms.getRoomById(roomContextMenu.roomid).markRoomAsRead()
} }
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Room settings") text: qsTr("Room settings")

View file

@ -355,7 +355,6 @@ Pane {
onAccepted: UIA.continue3pidReceived() onAccepted: UIA.continue3pidReceived()
} }
Connections { Connections {
function onConfirm3pidToken() { function onConfirm3pidToken() {
uiaConfirmationLinkDialog.open(); uiaConfirmationLinkDialog.open();
@ -363,6 +362,18 @@ Pane {
function onEmail() { function onEmail() {
uiaEmailPrompt.show(); uiaEmailPrompt.show();
} }
function onFallbackAuth(fallback) {
var component = Qt.createComponent("qrc:/resources/qml/dialogs/FallbackAuthDialog.qml");
if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot, {
"fallback": fallback
});
dialog.show();
destroyOnClose(dialog);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
function onPassword() { function onPassword() {
console.log("UIA: password needed"); console.log("UIA: password needed");
uiaPassPrompt.show(); uiaPassPrompt.show();
@ -385,18 +396,6 @@ Pane {
console.error("Failed to create component: " + component.errorString()); console.error("Failed to create component: " + component.errorString());
} }
} }
function onFallbackAuth(fallback) {
var component = Qt.createComponent("qrc:/resources/qml/dialogs/FallbackAuthDialog.qml");
if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot, {
"fallback": fallback
});
dialog.show();
destroyOnClose(dialog);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
target: UIA target: UIA
} }

View file

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

View file

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

View file

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

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

View file

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

View 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.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
}
}
}
}

View file

@ -285,25 +285,10 @@ Pane {
property var e: room ? room.getDump(modelData, "pins") : {} property var e: room ? room.getDump(modelData, "pins") : {}
Layout.fillWidth: true maxWidth: pinnedMessages.width
Layout.preferredHeight: height //Layout.preferredHeight: height
blurhash: e.blurhash ?? ""
body: e.body ?? ""
encryptionError: e.encryptionError ?? 0
eventId: e.eventId ?? "" eventId: e.eventId ?? ""
filename: e.filename ?? ""
filesize: e.filesize ?? ""
formattedBody: e.formattedBody ?? ""
isOnlyEmoji: e.isOnlyEmoji ?? false
keepFullText: true
originalWidth: e.originalWidth ?? 0
proportionalHeight: e.proportionalHeight ?? 1
type: e.type ?? MtxEvent.UnknownMessage
typeString: e.typeString ?? ""
url: e.url ?? ""
userColor: TimelineManager.userColor(e.userId, palette.window) userColor: TimelineManager.userColor(e.userId, palette.window)
userId: e.userId ?? ""
userName: e.userName ?? ""
Connections { Connections {
function onPinnedMessagesChanged() { function onPinnedMessagesChanged() {

View file

@ -8,37 +8,34 @@ import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import im.nheko 1.0 import im.nheko 1.0
Rectangle { Control {
id: r id: r
required property int encryptionError required property int encryptionError
required property string eventId required property string eventId
radius: fontMetrics.lineSpacing / 2 + Nheko.paddingMedium padding: Nheko.paddingMedium
width: parent.width? parent.width : 0 implicitHeight: contents.implicitHeight + Nheko.paddingMedium * 2
implicitWidth: encryptedText.implicitWidth+24+Nheko.paddingMedium*3 // Column doesn't provide a useful implicitWidth, should be replaced by ColumnLayout Layout.maximumWidth: contents.Layout.maximumWidth + padding * 2
height: contents.implicitHeight + Nheko.paddingMedium * 2 Layout.fillWidth: true
color: palette.alternateBase
RowLayout { contentItem: RowLayout {
id: contents id: contents
anchors.fill: parent
anchors.margins: Nheko.paddingMedium
spacing: Nheko.paddingMedium spacing: Nheko.paddingMedium
Image { Image {
source: "image://colorimage/:/icons/icons/ui/shield-filled-cross.svg?" + Nheko.theme.error source: "image://colorimage/:/icons/icons/ui/shield-filled-cross.svg?" + Nheko.theme.error
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
width: 24 Layout.preferredWidth: 24
height: width Layout.preferredHeight: 24
} }
Column { ColumnLayout {
spacing: Nheko.paddingSmall spacing: Nheko.paddingSmall
Layout.fillWidth: true Layout.fillWidth: true
MatrixText { Label {
id: encryptedText id: encryptedText
text: { text: {
switch (encryptionError) { switch (encryptionError) {
@ -58,8 +55,11 @@ Rectangle {
return qsTr("Unknown decryption error"); return qsTr("Unknown decryption error");
} }
} }
textFormat: Text.PlainText
wrapMode: Label.WordWrap
color: palette.text color: palette.text
width: parent.width Layout.fillWidth: true
Layout.maximumWidth: implicitWidth + 1
} }
Button { Button {
@ -72,4 +72,9 @@ Rectangle {
} }
background: Rectangle {
color: palette.alternateBase
radius: fontMetrics.lineSpacing / 2 + 2 * Nheko.paddingMedium
visible: !Settings.bubbles // the bubble in a bubble looks odd
}
} }

View file

@ -3,27 +3,24 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import ".." import ".."
import QtQuick 2.15 import QtQuick
import QtQuick.Layouts 1.15 import QtQuick.Controls
import im.nheko 1.0 import QtQuick.Layouts
import im.nheko
Rectangle { Control {
id: r id: r
required property string username required property string userName
radius: fontMetrics.lineSpacing / 2 + Nheko.paddingMedium padding: Nheko.paddingMedium
width: parent.width ? Math.min(parent.width, 700) : 0 //implicitHeight: contents.implicitHeight + padd * 2
height: contents.implicitHeight + Nheko.paddingMedium * 2 Layout.maximumWidth: contents.Layout.maximumWidth + padding * 2
color: palette.alternateBase Layout.fillWidth: true
border.color: Nheko.theme.green
border.width: 2
RowLayout { contentItem: RowLayout {
id: contents id: contents
anchors.fill: parent
anchors.margins: Nheko.paddingMedium
spacing: Nheko.paddingMedium spacing: Nheko.paddingMedium
Image { Image {
@ -33,26 +30,36 @@ Rectangle {
Layout.preferredHeight: 24 Layout.preferredHeight: 24
} }
Column { ColumnLayout {
spacing: Nheko.paddingSmall spacing: Nheko.paddingSmall
Layout.fillWidth: true Layout.fillWidth: true
MatrixText { MatrixText {
text: qsTr("%1 enabled end-to-end encryption").arg(r.username) text: qsTr("%1 enabled end-to-end encryption").arg(r.userName)
font.bold: true font.bold: true
font.pointSize: 14 font.pointSize: 14
color: palette.text color: palette.text
width: parent.width Layout.fillWidth: true
Layout.maximumWidth: implicitWidth + 1
} }
MatrixText { Label {
text: qsTr("Encryption keeps your messages safe by only allowing the people you sent the message to to read it. For extra security, if you want to make sure you are talking to the right people, you can verify them in real life.") text: qsTr("Encryption keeps your messages safe by only allowing the people you sent the message to to read it. For extra security, if you want to make sure you are talking to the right people, you can verify them in real life.")
color: palette.text textFormat: Text.PlainText
width: parent.width wrapMode: Label.WordWrap
Layout.fillWidth: true
Layout.maximumWidth: implicitWidth + 1
} }
} }
} }
background: Rectangle {
radius: fontMetrics.lineSpacing / 2 + Nheko.paddingMedium
height: contents.implicitHeight + Nheko.paddingMedium * 2
color: palette.alternateBase
border.color: Nheko.theme.green
border.width: 2
}
} }

View file

@ -2,26 +2,30 @@
// //
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.12 import QtQuick
import QtQuick.Layouts 1.2 import QtQuick.Layouts
import im.nheko 1.0 import QtQuick.Controls
import im.nheko
Control {
id: evRoot
Item {
required property string eventId required property string eventId
required property string filename required property string filename
required property string filesize required property string filesize
height: rowa.height + (Settings.bubbles? 16: 24) padding: Settings.bubbles? 8 : 12
implicitWidth: rowa.implicitWidth + metadataWidth //Layout.preferredHeight: rowa.implicitHeight + padding
property int metadataWidth //Layout.maximumWidth: rowa.Layout.maximumWidth + metadataWidth + padding
property bool fitsMetadata: true property int metadataWidth: 0
property bool fitsMetadata: false
RowLayout { Layout.maximumWidth: rowa.Layout.maximumWidth + padding * 2
contentItem: RowLayout {
id: rowa id: rowa
anchors.centerIn: parent spacing: 16
width: parent.width - (Settings.bubbles? 16 : 24)
spacing: 15
Rectangle { Rectangle {
id: button id: button
@ -63,6 +67,7 @@ Item {
id: filename_ id: filename_
Layout.fillWidth: true Layout.fillWidth: true
Layout.maximumWidth: implicitWidth + 1
text: filename text: filename
textFormat: Text.PlainText textFormat: Text.PlainText
elide: Text.ElideRight elide: Text.ElideRight
@ -73,6 +78,7 @@ Item {
id: filesize_ id: filesize_
Layout.fillWidth: true Layout.fillWidth: true
Layout.maximumWidth: implicitWidth + 1
text: filesize text: filesize
textFormat: Text.PlainText textFormat: Text.PlainText
elide: Text.ElideRight elide: Text.ElideRight
@ -83,11 +89,9 @@ Item {
} }
Rectangle { background: Rectangle {
color: palette.alternateBase color: palette.alternateBase
z: -1 radius: fontMetrics.lineSpacing / 2 + 2 * Nheko.paddingSmall
radius: 10
anchors.fill: parent
visible: !Settings.bubbles // the bubble in a bubble looks odd visible: !Settings.bubbles // the bubble in a bubble looks odd
} }

View file

@ -2,29 +2,31 @@
// //
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.15 import QtQuick
import QtQuick.Window 2.15 import QtQuick.Window
import QtQuick.Controls 2.3 import QtQuick.Controls
import im.nheko 1.0 import im.nheko
AbstractButton { AbstractButton {
required property int type required property int type
required property int originalWidth required property int originalWidth
required property int originalHeight
required property double proportionalHeight required property double proportionalHeight
required property string url required property string url
required property string blurhash required property string blurhash
required property string body required property string body
required property string filename required property string filename
required property bool isReply
required property string eventId required property string eventId
property double divisor: isReply ? 5 : 3 required property int containerHeight
property double divisor: EventDelegateChooser.isReply ? 10 : 4
property int tempWidth: originalWidth < 1? 400: originalWidth EventDelegateChooser.keepAspectRatio: true
EventDelegateChooser.maxWidth: originalWidth
EventDelegateChooser.maxHeight: containerHeight / divisor
EventDelegateChooser.aspectRatio: proportionalHeight
implicitWidth: Math.round(tempWidth*Math.min((timelineView.height/divisor)/(tempWidth*proportionalHeight), 1))
width: Math.min(parent?.width ?? 2000,implicitWidth)
height: width*proportionalHeight
hoverEnabled: true hoverEnabled: true
enabled: !EventDelegateChooser.isReply
state: (img.status != Image.Ready || timeline.privacyScreen.active) ? "BlurhashVisible" : "ImageVisible" state: (img.status != Image.Ready || timeline.privacyScreen.active) ? "BlurhashVisible" : "ImageVisible"
states: [ states: [
@ -116,6 +118,7 @@ AbstractButton {
source: url != "" ? (url.replace("mxc://", "image://MxcImage/") + "?scale") : "" source: url != "" ? (url.replace("mxc://", "image://MxcImage/") + "?scale") : ""
asynchronous: true asynchronous: true
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
horizontalAlignment: Image.AlignLeft
smooth: true smooth: true
mipmap: true mipmap: true
@ -127,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
anchors.fill: parent
} }
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.width * Screen.devicePixelRatio
sourceSize.height: parent.height * Screen.devicePixelRatio sourceSize.height: parent.height * Screen.devicePixelRatio
anchors.fill: parent
} }
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);
@ -150,6 +155,7 @@ AbstractButton {
id: overlay id: overlay
anchors.fill: parent anchors.fill: parent
visible: parent.hovered visible: parent.hovered
Rectangle { Rectangle {

View file

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

View file

@ -22,7 +22,7 @@ Item {
required property string url required property string url
required property string body required property string body
required property string filesize required property string filesize
property double divisor: isReply ? 4 : 2 property double divisor: EventDelegateChooser.isReply ? 10 : 4
property int tempWidth: originalWidth < 1? 400: originalWidth property int tempWidth: originalWidth < 1? 400: originalWidth
implicitWidth: type == MtxEvent.VideoMessage ? Math.round(tempWidth*Math.min((timelineView.height/divisor)/(tempWidth*proportionalHeight), 1)) : 500 implicitWidth: type == MtxEvent.VideoMessage ? Math.round(tempWidth*Math.min((timelineView.height/divisor)/(tempWidth*proportionalHeight), 1)) : 500
width: Math.min(parent?.width ?? implicitWidth, implicitWidth) width: Math.min(parent?.width ?? implicitWidth, implicitWidth)

View file

@ -2,25 +2,22 @@
// //
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.15 import QtQuick
import QtQuick.Controls 2.15 import QtQuick.Controls
import QtQuick.Layouts 1.15 import QtQuick.Layouts
import im.nheko 1.0 import im.nheko
Rectangle{ Control {
id: msgRoot
height: redactedLayout.implicitHeight + Nheko.paddingSmall property int metadataWidth: 0
implicitWidth: redactedLayout.implicitWidth + 2 * Nheko.paddingMedium property bool fitsMetadata: false //parent.width - redactedLayout.width > metadataWidth + 4
width: Math.min(parent.width,implicitWidth+1)
radius: fontMetrics.lineSpacing / 2 + 2 * Nheko.paddingSmall
color: palette.alternateBase
property int metadataWidth
property bool fitsMetadata: parent.width - redactedLayout.width > metadataWidth + 4
RowLayout { required property string eventId
required property Room room
contentItem: RowLayout {
id: redactedLayout id: redactedLayout
anchors.centerIn: parent
width: parent.width - 2 * Nheko.paddingMedium
spacing: Nheko.paddingSmall spacing: Nheko.paddingSmall
Image { Image {
@ -34,12 +31,11 @@ Rectangle{
id: redactedLabel id: redactedLabel
Layout.margins: 0 Layout.margins: 0
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
Layout.preferredWidth: implicitWidth Layout.maximumWidth: implicitWidth + 1
Layout.fillWidth: true Layout.fillWidth: true
property var redactedPair: room.formatRedactedEvent(eventId) property var redactedPair: room.formatRedactedEvent(msgRoot.eventId)
text: redactedPair["first"] text: redactedPair["first"]
wrapMode: Label.WordWrap wrapMode: Label.WordWrap
color: palette.text
ToolTip.text: redactedPair["second"] ToolTip.text: redactedPair["second"]
ToolTip.visible: hh.hovered ToolTip.visible: hh.hovered
@ -48,4 +44,13 @@ Rectangle{
} }
} }
} }
padding: Nheko.paddingSmall
Layout.maximumWidth: redactedLayout.Layout.maximumWidth + padding * 2
background: Rectangle {
color: palette.alternateBase
radius: fontMetrics.lineSpacing / 2 + 2 * Nheko.paddingSmall
}
} }

View file

@ -14,128 +14,88 @@ AbstractButton {
id: r id: r
property color userColor: "red" property color userColor: "red"
property double proportionalHeight
property int type
property string typeString
property int originalWidth
property string blurhash
property string body
property string formattedBody
property string eventId
property string filename
property string filesize
property string url
property bool isOnlyEmoji
property bool isStateEvent
property string userId
property string userName
property string thumbnailUrl
property string roomTopic
property string roomName
property string callType
property int duration
property int encryptionError
property int relatedEventCacheBuster
property int maxWidth
property bool keepFullText: false property bool keepFullText: false
height: replyContainer.height required property string eventId
implicitHeight: replyContainer.height
implicitWidth: visible? colorLine.width+Math.max(replyContainer.implicitWidth,userName_.fullTextWidth) : 0 // visible? seems to be causing issues property var room_: room
property string userId: eventId ? room.dataById(eventId, Room.UserId, "") : ""
property string userName: eventId ? room.dataById(eventId, Room.UserName, "") : ""
implicitHeight: replyContainer.implicitHeight
implicitWidth: replyContainer.implicitWidth
required property int maxWidth
NhekoCursorShape { NhekoCursorShape {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
} }
Rectangle {
id: colorLine
anchors.top: replyContainer.top
anchors.bottom: replyContainer.bottom
width: 4
color: TimelineManager.userColor(userId, palette.base)
}
onClicked: { onClicked: {
let link = reply.child.linkAt != undefined && reply.child.linkAt(pressX-colorLine.width, pressY - userName_.implicitHeight); let link = reply.child.linkAt != undefined && reply.child.linkAt(pressX-colorline.width, pressY - userName_.implicitHeight);
if (link) { if (link) {
Nheko.openLink(link) Nheko.openLink(link)
} else { } else {
room.showEvent(r.eventId) room.showEvent(r.eventId)
} }
} }
onPressAndHold: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(pressX-colorLine.width, pressY - userName_.implicitHeight), r.eventId) onPressAndHold: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(pressX-colorline.width, pressY - userName_.implicitHeight), r.eventId)
ColumnLayout { contentItem: TimelineEvent {
id: replyContainer id: timelineEvent
anchors.left: colorLine.right isStateEvent: false
width: parent.width - 4 room: room_
spacing: 0 eventId: r.eventId
replyTo: ""
mainInset: 4 + Nheko.paddingMedium
maxWidth: r.maxWidth
TapHandler { //height: replyContainer.implicitHeight
acceptedButtons: Qt.RightButton data: Row {
onSingleTapped: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(eventPoint.position.x, eventPoint.position.y - userName_.implicitHeight), r.eventId) id: replyContainer
gesturePolicy: TapHandler.ReleaseWithinBounds
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
}
AbstractButton { spacing: Nheko.paddingSmall
Layout.leftMargin: 4
Layout.fillWidth: true Rectangle {
contentItem: ElidedLabel { id: colorline
id: userName_
fullText: userName width: 4
color: r.userColor height: content.height
textFormat: Text.RichText
width: parent.width color: TimelineManager.userColor(r.userId, palette.base)
elideWidth: width
} }
onClicked: room.openUserProfile(userId)
}
MessageDelegate { Column {
Layout.leftMargin: 4 id: content
Layout.preferredHeight: height spacing: 0
id: reply
blurhash: r.blurhash
body: r.body
formattedBody: r.formattedBody
eventId: r.eventId
filename: r.filename
filesize: r.filesize
proportionalHeight: r.proportionalHeight
type: r.type
typeString: r.typeString ?? ""
url: r.url
thumbnailUrl: r.thumbnailUrl
duration: r.duration
originalWidth: r.originalWidth
isOnlyEmoji: r.isOnlyEmoji
isStateEvent: r.isStateEvent
userId: r.userId
userName: r.userName
roomTopic: r.roomTopic
roomName: r.roomName
callType: r.callType
relatedEventCacheBuster: r.relatedEventCacheBuster
encryptionError: r.encryptionError
// This is disabled so that left clicking the reply goes to its location
enabled: false
Layout.fillWidth: true
isReply: true
keepFullText: r.keepFullText
}
AbstractButton {
id: usernameBtn
contentItem: Label {
id: userName_
text: r.userName
color: r.userColor
textFormat: Text.RichText
width: timelineEvent.main?.width
}
onClicked: room.openUserProfile(r.userId)
}
data: [
usernameBtn, timelineEvent.main,
]
}
}
} }
Rectangle { background: Rectangle {
id: backgroundItem id: backgroundItem
z: -1 z: -1
anchors.fill: replyContainer property color userColor: TimelineManager.userColor(r.userId, palette.base)
property color userColor: TimelineManager.userColor(userId, palette.base)
property color bgColor: palette.base property color bgColor: palette.base
color: Qt.tint(bgColor, Qt.hsla(userColor.hslHue, 0.5, userColor.hslLightness, 0.1)) color: Qt.tint(bgColor, Qt.hsla(userColor.hslHue, 0.5, userColor.hslLightness, 0.1))
} }

View file

@ -9,11 +9,12 @@ import im.nheko
MatrixText { MatrixText {
required property string body required property string body
required property bool isOnlyEmoji required property bool isOnlyEmoji
required property bool isReply property bool isReply: EventDelegateChooser.isReply
required property bool keepFullText required property bool keepFullText
required property string formatted required property string formatted
property string copyText: selectedText ? getText(selectionStart, selectionEnd) : body property string copyText: selectedText ? getText(selectionStart, selectionEnd) : body
property int metadataWidth property int metadataWidth: 100
property bool fitsMetadata: false //positionAt(width,height-4) == positionAt(width-metadataWidth-10, height-4) property bool fitsMetadata: false //positionAt(width,height-4) == positionAt(width-metadataWidth-10, height-4)
// table border-collapse doesn't seem to work // table border-collapse doesn't seem to work
@ -39,11 +40,8 @@ MatrixText {
}" : "") + // TODO(Nico): Figure out how to support mobile }" : "") + // TODO(Nico): Figure out how to support mobile
"</style> "</style>
" + formatted.replace(/<del>/g, "<s>").replace(/<\/del>/g, "</s>").replace(/<strike>/g, "<s>").replace(/<\/strike>/g, "</s>") " + formatted.replace(/<del>/g, "<s>").replace(/<\/del>/g, "</s>").replace(/<strike>/g, "<s>").replace(/<\/strike>/g, "</s>")
width: parent?.width ?? 0
height: !keepFullText ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : implicitHeight
clip: !keepFullText
selectByMouse: !isReply selectByMouse: !isReply
// enabled: !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 {

View file

@ -9,6 +9,7 @@ Item {
id: effectRoot id: effectRoot
readonly property int maxLifespan: Math.max(confettiEmitter.lifeSpan, rainfallEmitter.lifeSpan) readonly property int maxLifespan: Math.max(confettiEmitter.lifeSpan, rainfallEmitter.lifeSpan)
required property bool shouldEffectsRun required property bool shouldEffectsRun
visible: effectRoot.shouldEffectsRun
function pulseConfetti() function pulseConfetti()
{ {
@ -23,8 +24,9 @@ Item {
ParticleSystem { ParticleSystem {
id: particleSystem id: particleSystem
Component.onCompleted: pause(); Component.onCompleted: stop();
paused: !effectRoot.shouldEffectsRun paused: !effectRoot.shouldEffectsRun
running: effectRoot.shouldEffectsRun
} }
Emitter { Emitter {
@ -89,26 +91,47 @@ Item {
enabled: false enabled: false
anchors.horizontalCenter: effectRoot.horizontalCenter anchors.horizontalCenter: effectRoot.horizontalCenter
y: -60 y: -60
emitRate: effectRoot.width / 50 emitRate: effectRoot.width / 30
lifeSpan: 10000 lifeSpan: 10000
system: particleSystem system: particleSystem
velocity: PointDirection { velocity: PointDirection {
x: 0 x: 0
y: 300 y: 400
xVariation: 0 xVariation: 0
yVariation: 75 yVariation: 75
} }
ItemParticle { // causes high CPU load, see: https://bugreports.qt.io/browse/QTBUG-117923
system: particleSystem //ItemParticle {
groups: ["rain"] // system: particleSystem
fade: false // groups: ["rain"]
delegate: Rectangle { // fade: false
width: 2 // visible: effectRoot.shouldEffectsRun
height: 30 + 30 * Math.random() // delegate: Rectangle {
radius: 2 // width: 2
// height: 30 + 30 * Math.random()
// radius: 2
// color: "#0099ff"
// }
//}
ImageParticle {
system: particleSystem
groups: ["rain"]
source: "qrc:/confettiparticle.svg"
rotationVelocity: 0
rotationVelocityVariation: 0
colorVariation: 0
color: "#0099ff" color: "#0099ff"
entryEffect: ImageParticle.None
xVector: PointDirection {
x: 0.01
y: 0
}
yVector: PointDirection {
x: 0
y: 5
}
} }
} }
} }
}

View file

@ -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();
}
}
}

View file

@ -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> *);
};

View file

@ -843,8 +843,8 @@ EventStore::get(const std::string &id,
nhlog::net()->error( nhlog::net()->error(
"Failed to retrieve event with id {}, which was " "Failed to retrieve event with id {}, which was "
"requested to show the replyTo for event {}", "requested to show the replyTo for event {}",
relatedTo, id,
id); relatedTo);
return; return;
} }
emit eventFetched(id, relatedTo, timeline); emit eventFetched(id, relatedTo, timeline);

View file

@ -541,7 +541,7 @@ RoomlistModel::sync(const mtx::responses::Sync &sync_)
if (auto t = if (auto t =
std::get_if<mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>( std::get_if<mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>(
&ev)) { &ev)) {
std::vector<QString> typing; QStringList typing;
typing.reserve(t->content.user_ids.size()); typing.reserve(t->content.user_ids.size());
for (const auto &user : t->content.user_ids) { for (const auto &user : t->content.user_ids) {
if (user != http::client()->user_id().to_string()) if (user != http::client()->user_id().to_string())

View file

@ -532,6 +532,7 @@ TimelineModel::roleNames() const
{IsOnlyEmoji, "isOnlyEmoji"}, {IsOnlyEmoji, "isOnlyEmoji"},
{Body, "body"}, {Body, "body"},
{FormattedBody, "formattedBody"}, {FormattedBody, "formattedBody"},
{FormattedStateEvent, "formattedStateEvent"},
{IsSender, "isSender"}, {IsSender, "isSender"},
{UserId, "userId"}, {UserId, "userId"},
{UserName, "userName"}, {UserName, "userName"},
@ -560,6 +561,7 @@ TimelineModel::roleNames() const
{ReplyTo, "replyTo"}, {ReplyTo, "replyTo"},
{ThreadId, "threadId"}, {ThreadId, "threadId"},
{Reactions, "reactions"}, {Reactions, "reactions"},
{Room, "room"},
{RoomId, "roomId"}, {RoomId, "roomId"},
{RoomName, "roomName"}, {RoomName, "roomName"},
{RoomTopic, "roomTopic"}, {RoomTopic, "roomTopic"},
@ -599,12 +601,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
case UserName: case UserName:
return QVariant(displayName(QString::fromStdString(acc::sender(event)))); return QVariant(displayName(QString::fromStdString(acc::sender(event))));
case UserPowerlevel: { case UserPowerlevel: {
return static_cast<qlonglong>(mtx::events::state::PowerLevels{ return static_cast<qlonglong>(
cache::client() permissions_.powerlevelEvent().user_level(acc::sender(event)));
->getStateEvent<mtx::events::state::PowerLevels>(room_id_.toStdString())
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.content}
.user_level(acc::sender(event)));
} }
case Day: { case Day: {
@ -692,8 +690,90 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
formattedBody_.replace(curImg, imgReplacement); formattedBody_.replace(curImg, imgReplacement);
} }
if (auto effectMessage =
std::get_if<mtx::events::RoomEvent<mtx::events::msg::ElementEffect>>(&event)) {
if (effectMessage->content.msgtype == std::string_view("nic.custom.confetti")) {
formattedBody_.append(QUtf8StringView(u8"🎊"));
} else if (effectMessage->content.msgtype ==
std::string_view("io.element.effect.rainfall")) {
formattedBody_.append(QUtf8StringView(u8"🌧️"));
}
}
return QVariant(utils::replaceEmoji(utils::linkifyMessage(formattedBody_))); return QVariant(utils::replaceEmoji(utils::linkifyMessage(formattedBody_)));
} }
case FormattedStateEvent: {
if (mtx::accessors::is_state_event(event)) {
return std::visit(
[this](const auto &e) {
constexpr auto t = mtx::events::state_content_to_type<decltype(e.content)>;
if constexpr (t == mtx::events::EventType::RoomServerAcl)
return tr("%1 changed which servers are allowed in this room.")
.arg(displayName(QString::fromStdString(e.sender)));
else if constexpr (t == mtx::events::EventType::RoomName) {
if (e.content.name.empty())
return tr("%1 removed the room name.")
.arg(displayName(QString::fromStdString(e.sender)));
else
return tr("%1 changed the room name to: %2")
.arg(displayName(QString::fromStdString(e.sender)))
.arg(QString::fromStdString(e.content.name).toHtmlEscaped());
} else if constexpr (t == mtx::events::EventType::RoomTopic) {
if (e.content.topic.empty())
return tr("%1 removed the topic.")
.arg(displayName(QString::fromStdString(e.sender)));
else
return tr("%1 changed the topic to: %2")
.arg(displayName(QString::fromStdString(e.sender)))
.arg(QString::fromStdString(e.content.topic).toHtmlEscaped());
} else if constexpr (t == mtx::events::EventType::RoomAvatar) {
if (e.content.url.starts_with("mxc://"))
return tr("%1 changed the room avatar to: %2")
.arg(displayName(QString::fromStdString(e.sender)))
.arg(QStringLiteral("<img height=\"32\" src=\"%1\">")
.arg(QUrl::toPercentEncoding(
QString::fromStdString(e.content.url))));
else
return tr("%1 removed the room avatar.")
.arg(displayName(QString::fromStdString(e.sender)));
} else if constexpr (t == mtx::events::EventType::RoomPinnedEvents)
return tr("%1 changed the pinned messages.")
.arg(displayName(QString::fromStdString(e.sender)));
else if constexpr (t == mtx::events::EventType::ImagePackInRoom)
formatImagePackEvent(e);
else if constexpr (t == mtx::events::EventType::RoomCanonicalAlias)
return tr("%1 changed the addresses for this room.")
.arg(displayName(QString::fromStdString(e.sender)));
else if constexpr (t == mtx::events::EventType::SpaceParent)
return tr("%1 changed the parent communities for this room.")
.arg(displayName(QString::fromStdString(e.sender)));
else if constexpr (t == mtx::events::EventType::RoomCreate)
return tr("%1 created and configured room: %2")
.arg(displayName(QString::fromStdString(e.sender)))
.arg(room_id_);
else if constexpr (t == mtx::events::EventType::RoomPowerLevels)
return formatPowerLevelEvent(e);
else if constexpr (t == mtx::events::EventType::PolicyRuleRoom)
return formatPolicyRule(QString::fromStdString(e.event_id));
else if constexpr (t == mtx::events::EventType::PolicyRuleUser)
return formatPolicyRule(QString::fromStdString(e.event_id));
else if constexpr (t == mtx::events::EventType::PolicyRuleServer)
return formatPolicyRule(QString::fromStdString(e.event_id));
else if constexpr (t == mtx::events::EventType::RoomHistoryVisibility)
return formatHistoryVisibilityEvent(e);
else if constexpr (t == mtx::events::EventType::RoomGuestAccess)
return formatGuestAccessEvent(e);
else if constexpr (t == mtx::events::EventType::RoomMember)
return formatMemberEvent(e);
return tr("%1 changed unknown state event %2.")
.arg(displayName(QString::fromStdString(e.sender)))
.arg(QString::fromStdString(to_string(e.type)));
},
event);
}
return QString();
}
case Url: case Url:
return QVariant(QString::fromStdString(url(event))); return QVariant(QString::fromStdString(url(event)));
case ThumbnailUrl: case ThumbnailUrl:
@ -828,6 +908,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
auto id = relations(event).replaces().value_or(event_id(event)); auto id = relations(event).replaces().value_or(event_id(event));
return QVariant::fromValue(events.reactions(id)); return QVariant::fromValue(events.reactions(id));
} }
case Room:
return QVariant::fromValue(this);
case RoomId: case RoomId:
return QVariant(room_id_); return QVariant(room_id_);
case RoomName: case RoomName:
@ -926,6 +1008,26 @@ TimelineModel::multiData(const QModelIndex &index, QModelRoleDataSpan roleDataSp
} }
} }
void
TimelineModel::multiData(const QString &id,
const QString &relatedTo,
QModelRoleDataSpan roleDataSpan) const
{
if (id.isEmpty())
return;
auto event = events.get(id.toStdString(), relatedTo.toStdString());
if (!event)
return;
for (QModelRoleData &roleData : roleDataSpan) {
int role = roleData.role();
roleData.setData(data(*event, role));
}
}
QVariant QVariant
TimelineModel::dataById(const QString &id, int role, const QString &relatedTo) TimelineModel::dataById(const QString &id, int role, const QString &relatedTo)
{ {
@ -2196,7 +2298,7 @@ TimelineModel::markSpecialEffectsDone()
} }
QString QString
TimelineModel::formatTypingUsers(const std::vector<QString> &users, const QColor &bg) TimelineModel::formatTypingUsers(const QStringList &users, const QColor &bg)
{ {
QString temp = QString temp =
tr("%1 and %2 are typing.", tr("%1 and %2 are typing.",
@ -2243,7 +2345,7 @@ TimelineModel::formatTypingUsers(const std::vector<QString> &users, const QColor
}; };
uidWithoutLast.reserve(static_cast<int>(users.size())); uidWithoutLast.reserve(static_cast<int>(users.size()));
for (size_t i = 0; i + 1 < users.size(); i++) { for (qsizetype i = 0; i + 1 < users.size(); i++) {
uidWithoutLast.append(formatUser(users[i])); uidWithoutLast.append(formatUser(users[i]));
} }
@ -2288,20 +2390,13 @@ TimelineModel::formatJoinRuleEvent(const QString &id)
} }
QString QString
TimelineModel::formatGuestAccessEvent(const QString &id) TimelineModel::formatGuestAccessEvent(
const mtx::events::StateEvent<mtx::events::state::GuestAccess> &event) const
{ {
auto e = events.get(id.toStdString(), ""); QString user = QString::fromStdString(event.sender);
if (!e)
return {};
auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::GuestAccess>>(e);
if (!event)
return {};
QString user = QString::fromStdString(event->sender);
QString name = utils::replaceEmoji(displayName(user)); QString name = utils::replaceEmoji(displayName(user));
switch (event->content.guest_access) { switch (event.content.guest_access) {
case mtx::events::state::AccessState::CanJoin: case mtx::events::state::AccessState::CanJoin:
return tr("%1 made the room open to guests.").arg(name); return tr("%1 made the room open to guests.").arg(name);
case mtx::events::state::AccessState::Forbidden: case mtx::events::state::AccessState::Forbidden:
@ -2312,21 +2407,13 @@ TimelineModel::formatGuestAccessEvent(const QString &id)
} }
QString QString
TimelineModel::formatHistoryVisibilityEvent(const QString &id) TimelineModel::formatHistoryVisibilityEvent(
const mtx::events::StateEvent<mtx::events::state::HistoryVisibility> &event) const
{ {
auto e = events.get(id.toStdString(), ""); QString user = QString::fromStdString(event.sender);
if (!e)
return {};
auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::HistoryVisibility>>(e);
if (!event)
return {};
QString user = QString::fromStdString(event->sender);
QString name = utils::replaceEmoji(displayName(user)); QString name = utils::replaceEmoji(displayName(user));
switch (event->content.history_visibility) { switch (event.content.history_visibility) {
case mtx::events::state::Visibility::WorldReadable: case mtx::events::state::Visibility::WorldReadable:
return tr("%1 made the room history world readable. Events may be now read by " return tr("%1 made the room history world readable. Events may be now read by "
"non-joined people.") "non-joined people.")
@ -2344,32 +2431,25 @@ TimelineModel::formatHistoryVisibilityEvent(const QString &id)
} }
QString QString
TimelineModel::formatPowerLevelEvent(const QString &id) TimelineModel::formatPowerLevelEvent(
const mtx::events::StateEvent<mtx::events::state::PowerLevels> &event) const
{ {
auto e = events.get(id.toStdString(), "");
if (!e)
return {};
auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(e);
if (!event)
return QString();
mtx::events::StateEvent<mtx::events::state::PowerLevels> const *prevEvent = nullptr; mtx::events::StateEvent<mtx::events::state::PowerLevels> const *prevEvent = nullptr;
if (!event->unsigned_data.replaces_state.empty()) { if (!event.unsigned_data.replaces_state.empty()) {
auto tempPrevEvent = events.get(event->unsigned_data.replaces_state, event->event_id); auto tempPrevEvent = events.get(event.unsigned_data.replaces_state, event.event_id);
if (tempPrevEvent) { if (tempPrevEvent) {
prevEvent = prevEvent =
std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(tempPrevEvent); std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(tempPrevEvent);
} }
} }
QString user = QString::fromStdString(event->sender); QString user = QString::fromStdString(event.sender);
QString sender_name = utils::replaceEmoji(displayName(user)); QString sender_name = utils::replaceEmoji(displayName(user));
// Get the rooms levels for redactions and powerlevel changes to determine "Administrator" and // Get the rooms levels for redactions and powerlevel changes to determine "Administrator" and
// "Moderator" powerlevels. // "Moderator" powerlevels.
auto administrator_power_level = event->content.state_level("m.room.power_levels"); auto administrator_power_level = event.content.state_level("m.room.power_levels");
auto moderator_power_level = event->content.redact; auto moderator_power_level = event.content.redact;
auto default_powerlevel = event->content.users_default; auto default_powerlevel = event.content.users_default;
if (!prevEvent) if (!prevEvent)
return tr("%1 has changed the room's permissions.").arg(sender_name); return tr("%1 has changed the room's permissions.").arg(sender_name);
@ -2379,7 +2459,7 @@ TimelineModel::formatPowerLevelEvent(const QString &id)
auto numberOfAffected = 0; auto numberOfAffected = 0;
// We do only compare to people with explicit PL. Usually others are not going to be // We do only compare to people with explicit PL. Usually others are not going to be
// affected either way and this is cheaper to iterate over. // affected either way and this is cheaper to iterate over.
for (auto const &[mxid, currentPowerlevel] : event->content.users) { for (auto const &[mxid, currentPowerlevel] : event.content.users) {
if (currentPowerlevel == newPowerlevelSetting && if (currentPowerlevel == newPowerlevelSetting &&
prevEvent->content.user_level(mxid) < newPowerlevelSetting) { prevEvent->content.user_level(mxid) < newPowerlevelSetting) {
numberOfAffected++; numberOfAffected++;
@ -2393,16 +2473,16 @@ TimelineModel::formatPowerLevelEvent(const QString &id)
QStringList resultingMessage{}; QStringList resultingMessage{};
// These affect only a few people. Therefor we can print who is affected. // These affect only a few people. Therefor we can print who is affected.
if (event->content.kick != prevEvent->content.kick) { if (event.content.kick != prevEvent->content.kick) {
auto default_message = tr("%1 has changed the room's kick powerlevel from %2 to %3.") auto default_message = tr("%1 has changed the room's kick powerlevel from %2 to %3.")
.arg(sender_name) .arg(sender_name)
.arg(prevEvent->content.kick) .arg(prevEvent->content.kick)
.arg(event->content.kick); .arg(event.content.kick);
// We only calculate affected users if we change to a level above the default users PL // We only calculate affected users if we change to a level above the default users PL
// to not accidentally have a DoS vector // to not accidentally have a DoS vector
if (event->content.kick > default_powerlevel) { if (event.content.kick > default_powerlevel) {
auto [affected, number_of_affected] = calc_affected(event->content.kick); auto [affected, number_of_affected] = calc_affected(event.content.kick);
if (number_of_affected != 0) { if (number_of_affected != 0) {
auto true_affected_rest = number_of_affected - affected.size(); auto true_affected_rest = number_of_affected - affected.size();
@ -2424,16 +2504,16 @@ TimelineModel::formatPowerLevelEvent(const QString &id)
} }
} }
if (event->content.redact != prevEvent->content.redact) { if (event.content.redact != prevEvent->content.redact) {
auto default_message = tr("%1 has changed the room's redact powerlevel from %2 to %3.") auto default_message = tr("%1 has changed the room's redact powerlevel from %2 to %3.")
.arg(sender_name) .arg(sender_name)
.arg(prevEvent->content.redact) .arg(prevEvent->content.redact)
.arg(event->content.redact); .arg(event.content.redact);
// We only calculate affected users if we change to a level above the default users PL // We only calculate affected users if we change to a level above the default users PL
// to not accidentally have a DoS vector // to not accidentally have a DoS vector
if (event->content.redact > default_powerlevel) { if (event.content.redact > default_powerlevel) {
auto [affected, number_of_affected] = calc_affected(event->content.redact); auto [affected, number_of_affected] = calc_affected(event.content.redact);
if (number_of_affected != 0) { if (number_of_affected != 0) {
auto true_affected_rest = number_of_affected - affected.size(); auto true_affected_rest = number_of_affected - affected.size();
@ -2456,16 +2536,16 @@ TimelineModel::formatPowerLevelEvent(const QString &id)
} }
} }
if (event->content.ban != prevEvent->content.ban) { if (event.content.ban != prevEvent->content.ban) {
auto default_message = tr("%1 has changed the room's ban powerlevel from %2 to %3.") auto default_message = tr("%1 has changed the room's ban powerlevel from %2 to %3.")
.arg(sender_name) .arg(sender_name)
.arg(prevEvent->content.ban) .arg(prevEvent->content.ban)
.arg(event->content.ban); .arg(event.content.ban);
// We only calculate affected users if we change to a level above the default users PL // We only calculate affected users if we change to a level above the default users PL
// to not accidentally have a DoS vector // to not accidentally have a DoS vector
if (event->content.ban > default_powerlevel) { if (event.content.ban > default_powerlevel) {
auto [affected, number_of_affected] = calc_affected(event->content.ban); auto [affected, number_of_affected] = calc_affected(event.content.ban);
if (number_of_affected != 0) { if (number_of_affected != 0) {
auto true_affected_rest = number_of_affected - affected.size(); auto true_affected_rest = number_of_affected - affected.size();
@ -2487,17 +2567,17 @@ TimelineModel::formatPowerLevelEvent(const QString &id)
} }
} }
if (event->content.state_default != prevEvent->content.state_default) { if (event.content.state_default != prevEvent->content.state_default) {
auto default_message = auto default_message =
tr("%1 has changed the room's state_default powerlevel from %2 to %3.") tr("%1 has changed the room's state_default powerlevel from %2 to %3.")
.arg(sender_name) .arg(sender_name)
.arg(prevEvent->content.state_default) .arg(prevEvent->content.state_default)
.arg(event->content.state_default); .arg(event.content.state_default);
// We only calculate affected users if we change to a level above the default users PL // We only calculate affected users if we change to a level above the default users PL
// to not accidentally have a DoS vector // to not accidentally have a DoS vector
if (event->content.state_default > default_powerlevel) { if (event.content.state_default > default_powerlevel) {
auto [affected, number_of_affected] = calc_affected(event->content.kick); auto [affected, number_of_affected] = calc_affected(event.content.kick);
if (number_of_affected != 0) { if (number_of_affected != 0) {
auto true_affected_rest = number_of_affected - affected.size(); auto true_affected_rest = number_of_affected - affected.size();
@ -2521,42 +2601,42 @@ TimelineModel::formatPowerLevelEvent(const QString &id)
// These affect potentially the whole room. We there for do not calculate who gets affected // These affect potentially the whole room. We there for do not calculate who gets affected
// by this to prevent huge lists of people. // by this to prevent huge lists of people.
if (event->content.invite != prevEvent->content.invite) { if (event.content.invite != prevEvent->content.invite) {
resultingMessage.append(tr("%1 has changed the room's invite powerlevel from %2 to %3.") resultingMessage.append(tr("%1 has changed the room's invite powerlevel from %2 to %3.")
.arg(sender_name, .arg(sender_name,
QString::number(prevEvent->content.invite), QString::number(prevEvent->content.invite),
QString::number(event->content.invite))); QString::number(event.content.invite)));
} }
if (event->content.events_default != prevEvent->content.events_default) { if (event.content.events_default != prevEvent->content.events_default) {
if ((event->content.events_default > default_powerlevel) && if ((event.content.events_default > default_powerlevel) &&
prevEvent->content.events_default <= default_powerlevel) { prevEvent->content.events_default <= default_powerlevel) {
resultingMessage.append( resultingMessage.append(
tr("%1 has changed the room's events_default powerlevel from %2 to %3. New " tr("%1 has changed the room's events_default powerlevel from %2 to %3. New "
"users can now not send any events.") "users can now not send any events.")
.arg(sender_name, .arg(sender_name,
QString::number(prevEvent->content.events_default), QString::number(prevEvent->content.events_default),
QString::number(event->content.events_default))); QString::number(event.content.events_default)));
} else if ((event->content.events_default < prevEvent->content.events_default) && } else if ((event.content.events_default < prevEvent->content.events_default) &&
(event->content.events_default < default_powerlevel) && (event.content.events_default < default_powerlevel) &&
(prevEvent->content.events_default > default_powerlevel)) { (prevEvent->content.events_default > default_powerlevel)) {
resultingMessage.append( resultingMessage.append(
tr("%1 has changed the room's events_default powerlevel from %2 to %3. New " tr("%1 has changed the room's events_default powerlevel from %2 to %3. New "
"users can now send events that are not otherwise restricted.") "users can now send events that are not otherwise restricted.")
.arg(sender_name, .arg(sender_name,
QString::number(prevEvent->content.events_default), QString::number(prevEvent->content.events_default),
QString::number(event->content.events_default))); QString::number(event.content.events_default)));
} else { } else {
resultingMessage.append( resultingMessage.append(
tr("%1 has changed the room's events_default powerlevel from %2 to %3.") tr("%1 has changed the room's events_default powerlevel from %2 to %3.")
.arg(sender_name, .arg(sender_name,
QString::number(prevEvent->content.events_default), QString::number(prevEvent->content.events_default),
QString::number(event->content.events_default))); QString::number(event.content.events_default)));
} }
} }
// Compare if a Powerlevel of a user changed // Compare if a Powerlevel of a user changed
for (auto const &[mxid, powerlevel] : event->content.users) { for (auto const &[mxid, powerlevel] : event.content.users) {
auto nameOfChangedUser = utils::replaceEmoji(displayName(QString::fromStdString(mxid))); auto nameOfChangedUser = utils::replaceEmoji(displayName(QString::fromStdString(mxid)));
if (prevEvent->content.user_level(mxid) != powerlevel) { if (prevEvent->content.user_level(mxid) != powerlevel) {
if (powerlevel >= administrator_power_level) { if (powerlevel >= administrator_power_level) {
@ -2581,7 +2661,7 @@ TimelineModel::formatPowerLevelEvent(const QString &id)
} }
// Handle added/removed/changed event type // Handle added/removed/changed event type
for (auto const &[event_type, powerlevel] : event->content.events) { for (auto const &[event_type, powerlevel] : event.content.events) {
auto prev_not_present = auto prev_not_present =
prevEvent->content.events.find(event_type) == prevEvent->content.events.end(); prevEvent->content.events.find(event_type) == prevEvent->content.events.end();
@ -2620,26 +2700,19 @@ TimelineModel::formatPowerLevelEvent(const QString &id)
} }
QString QString
TimelineModel::formatImagePackEvent(const QString &id) TimelineModel::formatImagePackEvent(
const mtx::events::StateEvent<mtx::events::msc2545::ImagePack> &event) const
{ {
auto e = events.get(id.toStdString(), "");
if (!e)
return {};
auto event = std::get_if<mtx::events::StateEvent<mtx::events::msc2545::ImagePack>>(e);
if (!event)
return {};
mtx::events::StateEvent<mtx::events::msc2545::ImagePack> const *prevEvent = nullptr; mtx::events::StateEvent<mtx::events::msc2545::ImagePack> const *prevEvent = nullptr;
if (!event->unsigned_data.replaces_state.empty()) { if (!event.unsigned_data.replaces_state.empty()) {
auto tempPrevEvent = events.get(event->unsigned_data.replaces_state, event->event_id); auto tempPrevEvent = events.get(event.unsigned_data.replaces_state, event.event_id);
if (tempPrevEvent) { if (tempPrevEvent) {
prevEvent = prevEvent =
std::get_if<mtx::events::StateEvent<mtx::events::msc2545::ImagePack>>(tempPrevEvent); std::get_if<mtx::events::StateEvent<mtx::events::msc2545::ImagePack>>(tempPrevEvent);
} }
} }
const auto &newImages = event->content.images; const auto &newImages = event.content.images;
const auto oldImages = prevEvent ? prevEvent->content.images : decltype(newImages){}; const auto oldImages = prevEvent ? prevEvent->content.images : decltype(newImages){};
auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent(); auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent();
@ -2662,12 +2735,12 @@ TimelineModel::formatImagePackEvent(const QString &id)
auto added = calcChange(newImages, oldImages); auto added = calcChange(newImages, oldImages);
auto removed = calcChange(oldImages, newImages); auto removed = calcChange(oldImages, newImages);
auto sender = utils::replaceEmoji(displayName(QString::fromStdString(event->sender))); auto sender = utils::replaceEmoji(displayName(QString::fromStdString(event.sender)));
const auto packId = [&event]() -> QString { const auto packId = [&event]() -> QString {
if (event->content.pack && !event->content.pack->display_name.empty()) { if (event.content.pack && !event.content.pack->display_name.empty()) {
return event->content.pack->display_name.c_str(); return event.content.pack->display_name.c_str();
} else if (!event->state_key.empty()) { } else if (!event.state_key.empty()) {
return event->state_key.c_str(); return event.state_key.c_str();
} }
return tr("(empty)"); return tr("(empty)");
}(); }();
@ -2692,7 +2765,7 @@ TimelineModel::formatImagePackEvent(const QString &id)
} }
QString QString
TimelineModel::formatPolicyRule(const QString &id) TimelineModel::formatPolicyRule(const QString &id) const
{ {
auto idStr = id.toStdString(); auto idStr = id.toStdString();
auto e = events.get(idStr, ""); auto e = events.get(idStr, "");
@ -2893,34 +2966,27 @@ TimelineModel::joinReplacementRoom(const QString &id)
} }
QString QString
TimelineModel::formatMemberEvent(const QString &id) TimelineModel::formatMemberEvent(
const mtx::events::StateEvent<mtx::events::state::Member> &event) const
{ {
auto e = events.get(id.toStdString(), "");
if (!e)
return {};
auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e);
if (!event)
return {};
mtx::events::StateEvent<mtx::events::state::Member> const *prevEvent = nullptr; mtx::events::StateEvent<mtx::events::state::Member> const *prevEvent = nullptr;
if (!event->unsigned_data.replaces_state.empty()) { if (!event.unsigned_data.replaces_state.empty()) {
auto tempPrevEvent = events.get(event->unsigned_data.replaces_state, event->event_id); auto tempPrevEvent = events.get(event.unsigned_data.replaces_state, event.event_id);
if (tempPrevEvent) { if (tempPrevEvent) {
prevEvent = prevEvent =
std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(tempPrevEvent); std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(tempPrevEvent);
} }
} }
QString user = QString::fromStdString(event->state_key); QString user = QString::fromStdString(event.state_key);
QString name = utils::replaceEmoji(displayName(user)); QString name = utils::replaceEmoji(displayName(user));
QString rendered; QString rendered;
QString sender = QString::fromStdString(event->sender); QString sender = QString::fromStdString(event.sender);
QString senderName = utils::replaceEmoji(displayName(sender)); QString senderName = utils::replaceEmoji(displayName(sender));
// see table https://matrix.org/docs/spec/client_server/latest#m-room-member // see table https://matrix.org/docs/spec/client_server/latest#m-room-member
using namespace mtx::events::state; using namespace mtx::events::state;
switch (event->content.membership) { switch (event.content.membership) {
case Membership::Invite: case Membership::Invite:
rendered = tr("%1 invited %2.").arg(senderName, name); rendered = tr("%1 invited %2.").arg(senderName, name);
break; break;
@ -2929,9 +2995,8 @@ TimelineModel::formatMemberEvent(const QString &id)
QString oldName = utils::replaceEmoji( QString oldName = utils::replaceEmoji(
QString::fromStdString(prevEvent->content.display_name).toHtmlEscaped()); QString::fromStdString(prevEvent->content.display_name).toHtmlEscaped());
bool displayNameChanged = bool displayNameChanged = prevEvent->content.display_name != event.content.display_name;
prevEvent->content.display_name != event->content.display_name; bool avatarChanged = prevEvent->content.avatar_url != event.content.avatar_url;
bool avatarChanged = prevEvent->content.avatar_url != event->content.avatar_url;
if (displayNameChanged && avatarChanged) if (displayNameChanged && avatarChanged)
rendered = tr("%1 has changed their avatar and changed their " rendered = tr("%1 has changed their avatar and changed their "
@ -2946,30 +3011,30 @@ TimelineModel::formatMemberEvent(const QString &id)
// the case of nothing changed but join follows join shouldn't happen, so // the case of nothing changed but join follows join shouldn't happen, so
// just show it as join // just show it as join
} else { } else {
if (event->content.join_authorised_via_users_server.empty()) if (event.content.join_authorised_via_users_server.empty())
rendered = tr("%1 joined.").arg(name); rendered = tr("%1 joined.").arg(name);
else else
rendered = rendered =
tr("%1 joined via authorisation from %2's server.") tr("%1 joined via authorisation from %2's server.")
.arg(name, .arg(name,
QString::fromStdString(event->content.join_authorised_via_users_server)); QString::fromStdString(event.content.join_authorised_via_users_server));
} }
break; break;
case Membership::Leave: case Membership::Leave:
if (!prevEvent || prevEvent->content.membership == Membership::Join) { if (!prevEvent || prevEvent->content.membership == Membership::Join) {
if (event->state_key == event->sender) if (event.state_key == event.sender)
rendered = tr("%1 left the room.").arg(name); rendered = tr("%1 left the room.").arg(name);
else else
rendered = tr("%2 kicked %1.").arg(name, senderName); rendered = tr("%2 kicked %1.").arg(name, senderName);
} else if (prevEvent->content.membership == Membership::Invite) { } else if (prevEvent->content.membership == Membership::Invite) {
if (event->state_key == event->sender) if (event.state_key == event.sender)
rendered = tr("%1 rejected their invite.").arg(name); rendered = tr("%1 rejected their invite.").arg(name);
else else
rendered = tr("%2 revoked the invite to %1.").arg(name, senderName); rendered = tr("%2 revoked the invite to %1.").arg(name, senderName);
} else if (prevEvent->content.membership == Membership::Ban) { } else if (prevEvent->content.membership == Membership::Ban) {
rendered = tr("%2 unbanned %1.").arg(name, senderName); rendered = tr("%2 unbanned %1.").arg(name, senderName);
} else if (prevEvent->content.membership == Membership::Knock) { } else if (prevEvent->content.membership == Membership::Knock) {
if (event->state_key == event->sender) if (event.state_key == event.sender)
rendered = tr("%1 redacted their knock.").arg(name); rendered = tr("%1 redacted their knock.").arg(name);
else else
rendered = tr("%2 rejected the knock from %1.").arg(name, senderName); rendered = tr("%2 rejected the knock from %1.").arg(name, senderName);
@ -2988,8 +3053,8 @@ TimelineModel::formatMemberEvent(const QString &id)
break; break;
} }
if (event->content.reason != "") { if (event.content.reason != "") {
rendered += " " + tr("Reason: %1").arg(QString::fromStdString(event->content.reason)); rendered += " " + tr("Reason: %1").arg(QString::fromStdString(event.content.reason));
} }
return rendered; return rendered;

View file

@ -199,8 +199,8 @@ class TimelineModel final : public QAbstractListModel
QML_UNCREATABLE("") QML_UNCREATABLE("")
Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
Q_PROPERTY(std::vector<QString> typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY Q_PROPERTY(
typingUsersChanged) QStringList typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY typingUsersChanged)
Q_PROPERTY(QString scrollTarget READ scrollTarget NOTIFY scrollTargetChanged) Q_PROPERTY(QString scrollTarget READ scrollTarget NOTIFY scrollTargetChanged)
Q_PROPERTY(QString reply READ reply WRITE setReply NOTIFY replyChanged RESET resetReply) Q_PROPERTY(QString reply READ reply WRITE setReply NOTIFY replyChanged RESET resetReply)
Q_PROPERTY(QString edit READ edit WRITE setEdit NOTIFY editChanged RESET resetEdit) Q_PROPERTY(QString edit READ edit WRITE setEdit NOTIFY editChanged RESET resetEdit)
@ -238,6 +238,7 @@ public:
IsOnlyEmoji, IsOnlyEmoji,
Body, Body,
FormattedBody, FormattedBody,
FormattedStateEvent,
IsSender, IsSender,
UserId, UserId,
UserName, UserName,
@ -266,6 +267,7 @@ public:
ReplyTo, ReplyTo,
ThreadId, ThreadId,
Reactions, Reactions,
Room,
RoomId, RoomId,
RoomName, RoomName,
RoomTopic, RoomTopic,
@ -286,6 +288,8 @@ public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
void multiData(const QModelIndex &index, QModelRoleDataSpan roleDataSpan) const override; void multiData(const QModelIndex &index, QModelRoleDataSpan roleDataSpan) const override;
void
multiData(const QString &id, const QString &relatedTo, QModelRoleDataSpan roleDataSpan) const;
QVariant data(const mtx::events::collections::TimelineEvents &event, int role) const; QVariant data(const mtx::events::collections::TimelineEvents &event, int role) const;
Q_INVOKABLE QVariant dataById(const QString &id, int role, const QString &relatedTo); Q_INVOKABLE QVariant dataById(const QString &id, int role, const QString &relatedTo);
Q_INVOKABLE QVariant dataByIndex(int i, int role = Qt::DisplayRole) const Q_INVOKABLE QVariant dataByIndex(int i, int role = Qt::DisplayRole) const
@ -302,17 +306,22 @@ public:
Q_INVOKABLE QString displayName(const QString &id) const; Q_INVOKABLE QString displayName(const QString &id) const;
Q_INVOKABLE QString avatarUrl(const QString &id) const; Q_INVOKABLE QString avatarUrl(const QString &id) const;
Q_INVOKABLE QString formatDateSeparator(QDate date) const; Q_INVOKABLE QString formatDateSeparator(QDate date) const;
Q_INVOKABLE QString formatTypingUsers(const std::vector<QString> &users, const QColor &bg); Q_INVOKABLE QString formatTypingUsers(const QStringList &users, const QColor &bg);
Q_INVOKABLE bool showAcceptKnockButton(const QString &id); Q_INVOKABLE bool showAcceptKnockButton(const QString &id);
Q_INVOKABLE void acceptKnock(const QString &id); Q_INVOKABLE void acceptKnock(const QString &id);
Q_INVOKABLE void joinReplacementRoom(const QString &id); Q_INVOKABLE void joinReplacementRoom(const QString &id);
Q_INVOKABLE QString formatMemberEvent(const QString &id); Q_INVOKABLE QString
formatMemberEvent(const mtx::events::StateEvent<mtx::events::state::Member> &event) const;
Q_INVOKABLE QString formatJoinRuleEvent(const QString &id); Q_INVOKABLE QString formatJoinRuleEvent(const QString &id);
Q_INVOKABLE QString formatHistoryVisibilityEvent(const QString &id); QString formatHistoryVisibilityEvent(
Q_INVOKABLE QString formatGuestAccessEvent(const QString &id); const mtx::events::StateEvent<mtx::events::state::HistoryVisibility> &event) const;
Q_INVOKABLE QString formatPowerLevelEvent(const QString &id); QString
Q_INVOKABLE QString formatImagePackEvent(const QString &id); formatGuestAccessEvent(const mtx::events::StateEvent<mtx::events::state::GuestAccess> &) const;
Q_INVOKABLE QString formatPolicyRule(const QString &id); QString formatPowerLevelEvent(
const mtx::events::StateEvent<mtx::events::state::PowerLevels> &event) const;
QString formatImagePackEvent(
const mtx::events::StateEvent<mtx::events::msc2545::ImagePack> &event) const;
Q_INVOKABLE QString formatPolicyRule(const QString &id) const;
Q_INVOKABLE QVariantMap formatRedactedEvent(const QString &id); Q_INVOKABLE QVariantMap formatRedactedEvent(const QString &id);
Q_INVOKABLE void viewRawMessage(const QString &id); Q_INVOKABLE void viewRawMessage(const QString &id);
@ -396,14 +405,14 @@ public slots:
void lastReadIdOnWindowFocus(); void lastReadIdOnWindowFocus();
void checkAfterFetch(); void checkAfterFetch();
QVariantMap getDump(const QString &eventId, const QString &relatedTo) const; QVariantMap getDump(const QString &eventId, const QString &relatedTo) const;
void updateTypingUsers(const std::vector<QString> &users) void updateTypingUsers(const QStringList &users)
{ {
if (this->typingUsers_ != users) { if (this->typingUsers_ != users) {
this->typingUsers_ = users; this->typingUsers_ = users;
emit typingUsersChanged(typingUsers_); emit typingUsersChanged(typingUsers_);
} }
} }
std::vector<QString> typingUsers() const { return typingUsers_; } QStringList typingUsers() const { return typingUsers_; }
bool paginationInProgress() const { return m_paginationInProgress; } bool paginationInProgress() const { return m_paginationInProgress; }
QString reply() const { return reply_; } QString reply() const { return reply_; }
void setReply(const QString &newReply); void setReply(const QString &newReply);
@ -461,7 +470,7 @@ signals:
void redactionFailed(QString id); void redactionFailed(QString id);
void mediaCached(QString mxcUrl, QString cacheUrl); void mediaCached(QString mxcUrl, QString cacheUrl);
void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo);
void typingUsersChanged(std::vector<QString> users); void typingUsersChanged(QStringList users);
void replyChanged(QString reply); void replyChanged(QString reply);
void editChanged(QString reply); void editChanged(QString reply);
void threadChanged(QString id); void threadChanged(QString id);
@ -519,7 +528,7 @@ private:
QString currentId, currentReadId; QString currentId, currentReadId;
QString reply_, edit_, thread_; QString reply_, edit_, thread_;
QString textBeforeEdit, replyBeforeEdit; QString textBeforeEdit, replyBeforeEdit;
std::vector<QString> typingUsers_; QStringList typingUsers_;
TimelineViewManager *manager_; TimelineViewManager *manager_;

View file

@ -102,10 +102,12 @@ MxcAnimatedImage::startDownload()
if (buffer.bytesAvailable() < if (buffer.bytesAvailable() <
4LL * 1024 * 1024 * 1024) // cache images smaller than 4MB in RAM 4LL * 1024 * 1024 * 1024) // cache images smaller than 4MB in RAM
movie.setCacheMode(QMovie::CacheAll); movie.setCacheMode(QMovie::CacheAll);
if (play_) if (play_ && movie.frameCount() > 1)
movie.start(); movie.start();
else else {
movie.jumpToFrame(0); movie.jumpToFrame(0);
movie.setPaused(true);
}
emit loadedChanged(); emit loadedChanged();
update(); update();
}); });
@ -173,6 +175,9 @@ MxcAnimatedImage::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeD
if (!imageDirty) if (!imageDirty)
return oldNode; return oldNode;
if (clipRect().isEmpty())
return oldNode;
imageDirty = false; imageDirty = false;
QSGImageNode *n = static_cast<QSGImageNode *>(oldNode); QSGImageNode *n = static_cast<QSGImageNode *>(oldNode);
if (!n) { if (!n) {

View file

@ -29,6 +29,7 @@ public:
connect(this, &MxcAnimatedImage::roomChanged, &MxcAnimatedImage::startDownload); connect(this, &MxcAnimatedImage::roomChanged, &MxcAnimatedImage::startDownload);
connect(&movie, &QMovie::frameChanged, this, &MxcAnimatedImage::newFrame); connect(&movie, &QMovie::frameChanged, this, &MxcAnimatedImage::newFrame);
setFlag(QQuickItem::ItemHasContents); setFlag(QQuickItem::ItemHasContents);
setFlag(QQuickItem::ItemObservesViewport);
// setAcceptHoverEvents(true); // setAcceptHoverEvents(true);
} }
@ -55,7 +56,12 @@ public:
{ {
if (play_ != newPlay) { if (play_ != newPlay) {
play_ = newPlay; play_ = newPlay;
movie.setPaused(!play_); if (movie.frameCount() > 1)
movie.setPaused(!play_);
else {
movie.jumpToFrame(0);
movie.setPaused(true);
}
emit playChanged(); emit playChanged();
} }
} }
@ -77,7 +83,8 @@ private slots:
{ {
currentFrame = frame; currentFrame = frame;
imageDirty = true; imageDirty = true;
update(); if (!clipRect().isEmpty())
update();
} }
private: private:

View file

@ -92,7 +92,8 @@ CallManager::CallManager(QObject *parent)
if (QGuiApplication::platformName() != QStringLiteral("wayland")) { if (QGuiApplication::platformName() != QStringLiteral("wayland")) {
// Selected by default // Selected by default
screenShareType_ = ScreenShareType::X11; screenShareType_ = ScreenShareType::X11;
std::swap(screenShareTypes_[0], screenShareTypes_[1]); if (screenShareTypes_.size() >= 2)
std::swap(screenShareTypes_[0], screenShareTypes_[1]);
} }
} }
#endif #endif