Newer
Older
// SPDX-FileCopyrightText: 2021 Nheko Contributors
import Qt.labs.platform 1.1 as Platform
import QtGraphicalEffects 1.0
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtQuick.Window 2.2
ScrollView {
clip: false
ScrollBar.horizontal.visible: false
ListView {
id: chat
property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding * 2
boundsBehavior: Flickable.StopAtBounds
pixelAligned: true
spacing: 4
verticalLayoutDirection: ListView.BottomToTop
onCountChanged: {
// Mark timeline as read
if (atYEnd)
model.currentIndex = 0;
Rectangle {
//closePolicy: Popup.NoAutoClose
id: messageActions
property Item attached: null
property alias model: row.model
// use comma to update on scroll
property var attachedPos: chat.contentY, attached ? chat.mapFromItem(attached, attached ? attached.width - width : 0, -height) : null
visible: Settings.buttonsInTimeline && !!attached && (attached.hovered || messageActionHover.hovered)
x: attached ? attachedPos.x : 0
y: attached ? attachedPos.y : 0
z: 10
height: row.implicitHeight + padding * 2
width: row.implicitWidth + padding * 2
color: Nheko.colors.window
border.color: Nheko.colors.buttonText
border.width: 1
radius: padding
HoverHandler {
id: messageActionHover
grabPermissions: PointerHandler.CanTakeOverFromAnything
}
Row {
id: row
property var model
anchors.centerIn: parent
spacing: messageActions.padding
ImageButton {
id: editButton
visible: !!row.model && row.model.isEditable
width: 16
hoverEnabled: true
image: ":/icons/icons/ui/edit.png"
ToolTip.visible: hovered
onClicked: {
if (row.model.isEditable)
chat.model.editAction(row.model.id);
}
}
EmojiButton {
id: reactButton
visible: chat.model ? chat.model.permissions.canSend(MtxEvent.Reaction) : false
width: 16
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("React")
emojiPicker: emojiPopup
event_id: row.model ? row.model.id : ""
}
ImageButton {
id: replyButton
visible: chat.model ? chat.model.permissions.canSend(MtxEvent.TextMessage) : false
width: 16
hoverEnabled: true
image: ":/icons/icons/ui/mail-reply.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Reply")
onClicked: chat.model.replyAction(row.model.id)
}
ImageButton {
id: optionsButton
width: 16
hoverEnabled: true
image: ":/icons/icons/ui/vertical-ellipsis.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Options")
onClicked: messageContextMenu.show(row.model.id, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton)
ScrollHelper {
flickable: parent
anchors.fill: parent
enabled: !Settings.mobileMode
Shortcut {
sequence: StandardKey.MoveToPreviousPage
onActivated: {
chat.contentY = chat.contentY - chat.height / 2;
chat.returnToBounds();
}
Shortcut {
sequence: StandardKey.MoveToNextPage
onActivated: {
chat.contentY = chat.contentY + chat.height / 2;
chat.returnToBounds();
}
Shortcut {
sequence: StandardKey.Cancel
onActivated: {
if (chat.model.reply)
chat.model.reply = undefined;
else
chat.model.edit = undefined;
}
}
Shortcut {
sequence: "Alt+Up"
onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply ? chat.model.idToIndex(chat.model.reply) + 1 : 0)
Shortcut {
sequence: "Alt+Down"
onActivated: {
var idx = chat.model.reply ? chat.model.idToIndex(chat.model.reply) - 1 : -1;
chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : null;
}
}
Shortcut {
sequence: "Alt+F"
onActivated: {
if (chat.model.reply) {
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
forwardMess.setMessageEventId(chat.model.reply);
forwardMess.open();
chat.model.reply = null;
}
Shortcut {
sequence: "Ctrl+E"
onActivated: {
chat.model.edit = chat.model.reply;
}
}
Connections {
target: TimelineManager
onFocusChanged: readTimer.running = TimelineManager.isWindowFocused
}
Timer {
id: readTimer
// force current read index to update
onTriggered: chat.model.setCurrentIndex(chat.model.currentIndex)
interval: 1000
}
Component {
id: sectionHeader
Column {
topPadding: 4
bottomPadding: 4
visible: modelData && (modelData.previousMessageUserId !== modelData.userId || modelData.previousMessageDay !== modelData.day)
width: parentWidth
height: ((modelData && modelData.previousMessageDay !== modelData.day) ? dateBubble.height + 8 + userName.height : userName.height) + 8
Label {
id: dateBubble
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
visible: modelData && modelData.previousMessageDay !== modelData.day
text: modelData ? chat.model.formatDateSeparator(modelData.timestamp) : ""
height: Math.round(fontMetrics.height * 1.4)
width: contentWidth * 1.2
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
background: Rectangle {
radius: parent.height / 2
Row {
height: userName.height
spacing: 8
Avatar {
id: messageUserAvatar
width: Nheko.avatarSize
height: Nheko.avatarSize
url: modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : ""
displayName: modelData ? modelData.userName : ""
userid: modelData ? modelData.userId : ""
onClicked: chat.model.openUserProfile(modelData.userId)
ToolTip.visible: avatarHover.hovered
ToolTip.text: userid
HoverHandler {
id: avatarHover
}
Jedi18
committed
}
Connections {
target: chat.model
onRoomAvatarUrlChanged: {
messageUserAvatar.url = modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : "";
}
onScrollToIndex: chat.positionViewAtIndex(index, ListView.Visible)
Label {
id: userName
text: modelData ? TimelineManager.escapeEmoji(modelData.userName) : ""
color: TimelineManager.userColor(modelData ? modelData.userId : "", Nheko.colors.window)
textFormat: Text.RichText
ToolTip.visible: displayNameHover.hovered
ToolTip.text: modelData ? modelData.userId : ""
TapHandler {
onSingleTapped: chat.model.openUserProfile(modelData.userId)
}
CursorShape {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
HoverHandler {
id: displayNameHover
}
text: modelData ? TimelineManager.userStatus(modelData.userId) : ""
textFormat: Text.PlainText
elide: Text.ElideRight
width: chat.delegateMaxWidth - parent.spacing * 2 - userName.implicitWidth - Nheko.avatarSize
font.italic: true
}
delegate: Item {
id: wrapper
property bool scrolledToThis: model.id === chat.model.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
width: chat.delegateMaxWidth
height: section ? section.height + timelinerow.height : timelinerow.height
Rectangle {
id: scrollHighlight
opacity: 0
visible: true
anchors.fill: timelinerow
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
states: State {
name: "revealed"
when: wrapper.scrolledToThis
}
transitions: Transition {
from: ""
to: "revealed"
SequentialAnimation {
PropertyAnimation {
target: scrollHighlight
properties: "opacity"
easing.type: Easing.InOutQuad
from: 0
to: 1
duration: 500
}
PropertyAnimation {
target: scrollHighlight
properties: "opacity"
easing.type: Easing.InOutQuad
from: 1
to: 0
duration: 500
}
ScriptAction {
script: chat.model.eventShown()
}
}
}
}
Loader {
id: section
property var modelData: model
property int parentWidth: parent.width
active: model.previousMessageUserId !== undefined && model.previousMessageUserId !== model.userId || model.previousMessageDay !== model.day
//asynchronous: true
sourceComponent: sectionHeader
visible: status == Loader.Ready
}
TimelineRow {
id: timelinerow
property alias hovered: hoverHandler.hovered
y: section.visible && section.active ? section.y + section.height : 0
HoverHandler {
id: hoverHandler
enabled: !Settings.mobileMode
onHoveredChanged: {
if (hovered) {
if (!messageActionHover.hovered) {
messageActions.attached = timelinerow;
messageActions.model = model;
}
}
}
}
Connections {
target: chat
onMovementEnded: {
if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
chat.model.currentIndex = index;
anchors.horizontalCenter: parent.horizontalCenter
running: chat.model && chat.model.paginationInProgress
foreground: Nheko.colors.mid
visible: chat.model && chat.model.paginationInProgress
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
Platform.Menu {
id: messageContextMenu
property string eventId
property string link
property string text
property int eventType
property bool isEncrypted
property bool isEditable
property bool isSender
function show(eventId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) {
eventId = eventId_;
eventType = eventType_;
isEncrypted = isEncrypted_;
isEditable = isEditable_;
isSender = isSender_;
if (text_)
text = text_;
else
text = "";
if (link_)
link = link_;
else
link = "";
if (showAt_)
open(showAt_);
else
open();
}
Platform.MenuItem {
visible: messageContextMenu.text
enabled: visible
onTriggered: Clipboard.text = messageContextMenu.text
}
Platform.MenuItem {
visible: messageContextMenu.link
enabled: visible
text: qsTr("Copy &link location")
onTriggered: Clipboard.text = messageContextMenu.link
}
Platform.MenuItem {
id: reactionOption
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
onTriggered: emojiPopup.show(null, function(emoji) {
room.input.reaction(messageContextMenu.eventId, emoji);
})
}
Platform.MenuItem {
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
onTriggered: room.replyAction(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
enabled: visible
onTriggered: room.editAction(messageContextMenu.eventId)
}
Platform.MenuItem {
onTriggered: room.readReceiptsAction(messageContextMenu.eventId)
}
Platform.MenuItem {
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
onTriggered: {
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
forwardMess.setMessageEventId(messageContextMenu.eventId);
forwardMess.open();
}
}
Platform.MenuItem {
}
Platform.MenuItem {
text: qsTr("View raw message")
onTriggered: room.viewRawMessage(messageContextMenu.eventId)
}
Platform.MenuItem {
// TODO(Nico): Fix this still being iterated over, when using keyboard to select options
visible: messageContextMenu.isEncrypted
enabled: visible
text: qsTr("View decrypted raw message")
onTriggered: room.viewDecryptedRawMessage(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: (room ? room.permissions.canRedact() : false) || messageContextMenu.isSender
onTriggered: room.redactEvent(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
enabled: visible
onTriggered: room.saveMedia(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
enabled: visible
text: qsTr("&Open in external program")
onTriggered: room.openMedia(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventId
enabled: visible
text: qsTr("Copy link to eve&nt")
onTriggered: room.copyLinkToEvent(messageContextMenu.eventId)
}
}
Component {
id: forwardCompleterComponent
ForwardCompleter {
}
}