mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-24 03:58:49 +03:00
Merge branch 'delegate-rework'
This commit is contained in:
commit
3a0d5788e1
36 changed files with 2356 additions and 1897 deletions
|
@ -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)"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onHoveredChanged: {
|
delegate: Settings.bubbles ? bubbleMessageStyle : defaultMessageStyle
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
334
resources/qml/TimelineBubbleMessageStyle.qml
Normal file
334
resources/qml/TimelineBubbleMessageStyle.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
327
resources/qml/TimelineDefaultMessageStyle.qml
Normal file
327
resources/qml/TimelineDefaultMessageStyle.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
266
resources/qml/TimelineEvent.qml
Normal file
266
resources/qml/TimelineEvent.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
98
resources/qml/TimelineMetadata.qml
Normal file
98
resources/qml/TimelineMetadata.qml
Normal 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
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
164
resources/qml/TimelineSectionHeader.qml
Normal file
164
resources/qml/TimelineSectionHeader.qml
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
// SPDX-FileCopyrightText: Nheko Contributors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import Qt.labs.platform 1.1 as Platform
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15
|
||||||
|
import QtQuick.Layouts 1.2
|
||||||
|
import QtQuick.Window 2.13
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
|
||||||
|
required property var day
|
||||||
|
required property bool isSender
|
||||||
|
required property bool isStateEvent
|
||||||
|
required property int parentWidth
|
||||||
|
required property var previousMessageDay
|
||||||
|
required property bool previousMessageIsStateEvent
|
||||||
|
required property string previousMessageUserId
|
||||||
|
required property date timestamp
|
||||||
|
required property string userId
|
||||||
|
required property string userName
|
||||||
|
required property string userPowerlevel
|
||||||
|
|
||||||
|
bottomPadding: Settings.bubbles ? (isSender && previousMessageDay == day ? 0 : 2) : 3
|
||||||
|
spacing: 8
|
||||||
|
topPadding: userName_.visible ? 4 : 0
|
||||||
|
visible: (previousMessageUserId !== userId || previousMessageDay !== day || isStateEvent !== previousMessageIsStateEvent)
|
||||||
|
width: parentWidth
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: dateBubble
|
||||||
|
|
||||||
|
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
||||||
|
color: palette.text
|
||||||
|
height: Math.round(fontMetrics.height * 1.4)
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: room ? room.formatDateSeparator(timestamp) : ""
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
visible: room && previousMessageDay !== day
|
||||||
|
width: contentWidth * 1.2
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: palette.window
|
||||||
|
radius: parent.height / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
id: userInfo
|
||||||
|
|
||||||
|
property int remainingWidth: chat.delegateMaxWidth - spacing - messageUserAvatar.width
|
||||||
|
|
||||||
|
height: userName_.height
|
||||||
|
spacing: 8
|
||||||
|
visible: !isStateEvent && (!isSender || !Settings.bubbles)
|
||||||
|
|
||||||
|
Avatar {
|
||||||
|
id: messageUserAvatar
|
||||||
|
|
||||||
|
ToolTip.delay: Nheko.tooltipDelay
|
||||||
|
ToolTip.text: userid
|
||||||
|
ToolTip.visible: messageUserAvatar.hovered
|
||||||
|
displayName: userName
|
||||||
|
height: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1)
|
||||||
|
url: !room ? "" : room.avatarUrl(userId).replace("mxc://", "image://MxcImage/")
|
||||||
|
userid: userId
|
||||||
|
width: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1)
|
||||||
|
|
||||||
|
onClicked: room.openUserProfile(userId)
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
function onRoomAvatarUrlChanged() {
|
||||||
|
messageUserAvatar.url = room.avatarUrl(userId).replace("mxc://", "image://MxcImage/");
|
||||||
|
}
|
||||||
|
function onScrollToIndex(index) {
|
||||||
|
chat.positionViewAtIndex(index, ListView.Center);
|
||||||
|
}
|
||||||
|
|
||||||
|
target: room
|
||||||
|
}
|
||||||
|
|
||||||
|
AbstractButton {
|
||||||
|
id: userNameButton
|
||||||
|
|
||||||
|
PowerlevelIndicator {
|
||||||
|
id: powerlevelIndicator
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: timelineEvent
|
||||||
|
|
||||||
|
isStateEvent: false
|
||||||
|
room: room_
|
||||||
|
eventId: r.eventId
|
||||||
|
replyTo: ""
|
||||||
|
mainInset: 4 + Nheko.paddingMedium
|
||||||
|
maxWidth: r.maxWidth
|
||||||
|
|
||||||
|
//height: replyContainer.implicitHeight
|
||||||
|
data: Row {
|
||||||
id: replyContainer
|
id: replyContainer
|
||||||
|
|
||||||
anchors.left: colorLine.right
|
spacing: Nheko.paddingSmall
|
||||||
width: parent.width - 4
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
TapHandler {
|
|
||||||
acceptedButtons: Qt.RightButton
|
|
||||||
onSingleTapped: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(eventPoint.position.x, eventPoint.position.y - userName_.implicitHeight), r.eventId)
|
|
||||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
|
|
||||||
}
|
|
||||||
|
|
||||||
AbstractButton {
|
|
||||||
Layout.leftMargin: 4
|
|
||||||
Layout.fillWidth: true
|
|
||||||
contentItem: ElidedLabel {
|
|
||||||
id: userName_
|
|
||||||
fullText: userName
|
|
||||||
color: r.userColor
|
|
||||||
textFormat: Text.RichText
|
|
||||||
width: parent.width
|
|
||||||
elideWidth: width
|
|
||||||
}
|
|
||||||
onClicked: room.openUserProfile(userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageDelegate {
|
|
||||||
Layout.leftMargin: 4
|
|
||||||
Layout.preferredHeight: height
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
id: colorline
|
||||||
|
|
||||||
|
width: 4
|
||||||
|
height: content.height
|
||||||
|
|
||||||
|
color: TimelineManager.userColor(r.userId, palette.base)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: content
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
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,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
//ItemParticle {
|
||||||
|
// system: particleSystem
|
||||||
|
// groups: ["rain"]
|
||||||
|
// fade: false
|
||||||
|
// visible: effectRoot.shouldEffectsRun
|
||||||
|
// delegate: Rectangle {
|
||||||
|
// width: 2
|
||||||
|
// height: 30 + 30 * Math.random()
|
||||||
|
// radius: 2
|
||||||
|
// color: "#0099ff"
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
ImageParticle {
|
||||||
system: particleSystem
|
system: particleSystem
|
||||||
groups: ["rain"]
|
groups: ["rain"]
|
||||||
fade: false
|
source: "qrc:/confettiparticle.svg"
|
||||||
delegate: Rectangle {
|
rotationVelocity: 0
|
||||||
width: 2
|
rotationVelocityVariation: 0
|
||||||
height: 30 + 30 * Math.random()
|
colorVariation: 0
|
||||||
radius: 2
|
|
||||||
color: "#0099ff"
|
color: "#0099ff"
|
||||||
|
entryEffect: ImageParticle.None
|
||||||
|
xVector: PointDirection {
|
||||||
|
x: 0.01
|
||||||
|
y: 0
|
||||||
|
}
|
||||||
|
yVector: PointDirection {
|
||||||
|
x: 0
|
||||||
|
y: 5
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
356
src/timeline/EventDelegateChooser.cpp
Normal file
356
src/timeline/EventDelegateChooser.cpp
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
276
src/timeline/EventDelegateChooser.h
Normal file
276
src/timeline/EventDelegateChooser.h
Normal 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> *);
|
||||||
|
};
|
|
@ -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);
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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_;
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
if (movie.frameCount() > 1)
|
||||||
movie.setPaused(!play_);
|
movie.setPaused(!play_);
|
||||||
|
else {
|
||||||
|
movie.jumpToFrame(0);
|
||||||
|
movie.setPaused(true);
|
||||||
|
}
|
||||||
emit playChanged();
|
emit playChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +83,7 @@ private slots:
|
||||||
{
|
{
|
||||||
currentFrame = frame;
|
currentFrame = frame;
|
||||||
imageDirty = true;
|
imageDirty = true;
|
||||||
|
if (!clipRect().isEmpty())
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -92,6 +92,7 @@ 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;
|
||||||
|
if (screenShareTypes_.size() >= 2)
|
||||||
std::swap(screenShareTypes_[0], screenShareTypes_[1]);
|
std::swap(screenShareTypes_[0], screenShareTypes_[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue