Move Chat/ dir under Pages/
This commit is contained in:
9
src/gui/Pages/Chat/Timeline/Daybreak.qml
Normal file
9
src/gui/Pages/Chat/Timeline/Daybreak.qml
Normal file
@@ -0,0 +1,9 @@
|
||||
import QtQuick 2.12
|
||||
import "../../../Base"
|
||||
|
||||
HNoticePage {
|
||||
text: model.date.toLocaleDateString()
|
||||
color: theme.chat.daybreak.text
|
||||
backgroundColor: theme.chat.daybreak.background
|
||||
radius: theme.chat.daybreak.radius
|
||||
}
|
15
src/gui/Pages/Chat/Timeline/EventAudio.qml
Normal file
15
src/gui/Pages/Chat/Timeline/EventAudio.qml
Normal file
@@ -0,0 +1,15 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtAV 1.7
|
||||
import "../../../Base"
|
||||
import "../../../Base/MediaPlayer"
|
||||
|
||||
AudioPlayer {
|
||||
id: audio
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged:
|
||||
eventDelegate.hoveredMediaTypeUrl =
|
||||
hovered ? [EventDelegate.Media.Audio, audio.source] : []
|
||||
}
|
||||
}
|
182
src/gui/Pages/Chat/Timeline/EventContent.qml
Normal file
182
src/gui/Pages/Chat/Timeline/EventContent.qml
Normal file
@@ -0,0 +1,182 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../../Base"
|
||||
|
||||
HRowLayout {
|
||||
id: eventContent
|
||||
spacing: theme.spacing / 1.25
|
||||
layoutDirection: onRight ? Qt.RightToLeft: Qt.LeftToRight
|
||||
|
||||
|
||||
readonly property string senderText:
|
||||
hideNameLine ? "" : (
|
||||
"<div class='sender'>" +
|
||||
utils.coloredNameHtml(model.sender_name, model.sender_id) +
|
||||
"</div>"
|
||||
)
|
||||
readonly property string contentText: utils.processedEventText(model)
|
||||
readonly property string timeText: utils.formatTime(model.date, false)
|
||||
readonly property string localEchoText:
|
||||
model.is_local_echo ?
|
||||
` <font size=${theme.fontSize.small}px>⏳</font>` :
|
||||
""
|
||||
|
||||
readonly property bool pureMedia: ! contentText && linksRepeater.count
|
||||
|
||||
readonly property string hoveredLink: contentLabel.hoveredLink
|
||||
readonly property bool hoveredSelectable: contentHover.hovered
|
||||
|
||||
readonly property int xOffset:
|
||||
onRight ?
|
||||
contentLabel.width - contentLabel.paintedWidth -
|
||||
contentLabel.leftPadding - contentLabel.rightPadding :
|
||||
0
|
||||
|
||||
// 600px max with a 16px font
|
||||
readonly property int maxMessageWidth: theme.fontSize.normal * 0.5 * 75
|
||||
|
||||
|
||||
TapHandler {
|
||||
enabled: debugMode
|
||||
onDoubleTapped:
|
||||
utils.debug(eventContent, null, con => { con.runJS("json()") })
|
||||
}
|
||||
|
||||
Item {
|
||||
id: avatarWrapper
|
||||
opacity: collapseAvatar ? 0 : 1
|
||||
visible: ! hideAvatar
|
||||
|
||||
Layout.minimumWidth: theme.chat.message.avatarSize
|
||||
Layout.minimumHeight:
|
||||
collapseAvatar ? 1 :
|
||||
smallAvatar ? theme.chat.message.collapsedAvatarSize :
|
||||
Layout.minimumWidth
|
||||
|
||||
Layout.maximumWidth: Layout.minimumWidth
|
||||
Layout.maximumHeight: Layout.minimumHeight
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
HUserAvatar {
|
||||
id: avatar
|
||||
userId: model.sender_id
|
||||
displayName: model.sender_name
|
||||
mxc: model.sender_avatar
|
||||
width: parent.width
|
||||
height: collapseAvatar ? 1 : theme.chat.message.avatarSize
|
||||
}
|
||||
}
|
||||
|
||||
HColumnLayout {
|
||||
id: contentColumn
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
HSelectableLabel {
|
||||
id: contentLabel
|
||||
container: selectableLabelContainer
|
||||
index: model.index
|
||||
visible: ! pureMedia
|
||||
|
||||
topPadding: theme.spacing / 1.75
|
||||
bottomPadding: topPadding
|
||||
leftPadding: eventContent.spacing
|
||||
rightPadding: leftPadding
|
||||
|
||||
color: model.event_type === "RoomMessageNotice" ?
|
||||
theme.chat.message.noticeBody :
|
||||
theme.chat.message.body
|
||||
|
||||
font.italic: model.event_type === "RoomMessageEmote"
|
||||
wrapMode: TextEdit.Wrap
|
||||
textFormat: Text.RichText
|
||||
text:
|
||||
// CSS
|
||||
theme.chat.message.styleInclude +
|
||||
|
||||
// Sender name
|
||||
eventContent.senderText +
|
||||
|
||||
// Message body
|
||||
eventContent.contentText +
|
||||
|
||||
// Time
|
||||
// For some reason, if there's only one space,
|
||||
// times will be on their own lines most of the time.
|
||||
" " +
|
||||
`<font size=${theme.fontSize.small}px ` +
|
||||
`color=${theme.chat.message.date}>` +
|
||||
timeText +
|
||||
"</font>" +
|
||||
|
||||
// Local echo icon
|
||||
(model.is_local_echo ?
|
||||
` <font size=${theme.fontSize.small}px>⏳</font>` : "")
|
||||
|
||||
transform: Translate { x: xOffset }
|
||||
|
||||
Layout.maximumWidth: eventContent.maxMessageWidth
|
||||
Layout.fillWidth: true
|
||||
|
||||
function selectAllText() {
|
||||
// Select the message body without the date or name
|
||||
container.clearSelection()
|
||||
contentLabel.select(
|
||||
0,
|
||||
contentLabel.length -
|
||||
timeText.length - 1 // - 1: separating space
|
||||
)
|
||||
contentLabel.updateContainerSelectedTexts()
|
||||
}
|
||||
|
||||
HoverHandler { id: contentHover }
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(
|
||||
parent.paintedWidth +
|
||||
parent.leftPadding + parent.rightPadding,
|
||||
|
||||
linksRepeater.childrenWidth +
|
||||
(pureMedia ? 0 : parent.leftPadding + parent.rightPadding),
|
||||
)
|
||||
height: contentColumn.height
|
||||
z: -1
|
||||
color: isOwn?
|
||||
theme.chat.message.ownBackground :
|
||||
theme.chat.message.background
|
||||
|
||||
Rectangle {
|
||||
visible: model.event_type === "RoomMessageNotice"
|
||||
width: theme.chat.message.noticeLineWidth
|
||||
height: parent.height
|
||||
color: utils.nameColor(
|
||||
model.sender_name || model.sender_id.substring(1),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HRepeater {
|
||||
id: linksRepeater
|
||||
model: eventDelegate.currentModel.links
|
||||
|
||||
EventMediaLoader {
|
||||
singleMediaInfo: eventDelegate.currentModel
|
||||
mediaUrl: modelData
|
||||
showSender: pureMedia ? senderText : ""
|
||||
showDate: pureMedia ? timeText : ""
|
||||
showLocalEcho: pureMedia ? localEchoText : ""
|
||||
|
||||
transform: Translate { x: xOffset }
|
||||
|
||||
Layout.bottomMargin: pureMedia ? 0 : contentLabel.bottomPadding
|
||||
Layout.leftMargin: pureMedia ? 0 : contentLabel.leftPadding
|
||||
Layout.rightMargin: pureMedia ? 0 : contentLabel.rightPadding
|
||||
|
||||
Layout.preferredWidth: item ? item.width : -1
|
||||
Layout.preferredHeight: item ? item.height : -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HSpacer {}
|
||||
}
|
181
src/gui/Pages/Chat/Timeline/EventDelegate.qml
Normal file
181
src/gui/Pages/Chat/Timeline/EventDelegate.qml
Normal file
@@ -0,0 +1,181 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../../Base"
|
||||
|
||||
HColumnLayout {
|
||||
id: eventDelegate
|
||||
width: eventList.width
|
||||
|
||||
|
||||
enum Media { Page, File, Image, Video, Audio }
|
||||
|
||||
property var hoveredMediaTypeUrl: []
|
||||
|
||||
// Remember timeline goes from newest message at index 0 to oldest
|
||||
readonly property var previousModel: eventList.model.get(model.index + 1)
|
||||
readonly property var nextModel: eventList.model.get(model.index - 1)
|
||||
readonly property QtObject currentModel: model
|
||||
|
||||
property bool isOwn: chat.userId === model.sender_id
|
||||
property bool onRight: eventList.ownEventsOnRight && isOwn
|
||||
property bool combine: eventList.canCombine(previousModel, model)
|
||||
property bool talkBreak: eventList.canTalkBreak(previousModel, model)
|
||||
property bool dayBreak: eventList.canDayBreak(previousModel, model)
|
||||
|
||||
readonly property bool smallAvatar:
|
||||
eventList.canCombine(model, nextModel) &&
|
||||
(model.event_type === "RoomMessageEmote" ||
|
||||
! (model.event_type.startsWith("RoomMessage") ||
|
||||
model.event_type.startsWith("RoomEncrypted")))
|
||||
|
||||
readonly property bool collapseAvatar: combine
|
||||
readonly property bool hideAvatar: onRight
|
||||
|
||||
readonly property bool hideNameLine:
|
||||
model.event_type === "RoomMessageEmote" ||
|
||||
! (
|
||||
model.event_type.startsWith("RoomMessage") ||
|
||||
model.event_type.startsWith("RoomEncrypted")
|
||||
) ||
|
||||
onRight ||
|
||||
combine
|
||||
|
||||
readonly property int cursorShape:
|
||||
eventContent.hoveredLink || hoveredMediaTypeUrl.length > 0 ?
|
||||
Qt.PointingHandCursor :
|
||||
|
||||
eventContent.hoveredSelectable ? Qt.IBeamCursor :
|
||||
|
||||
Qt.ArrowCursor
|
||||
|
||||
readonly property int separationSpacing:
|
||||
dayBreak ? theme.spacing * 4 :
|
||||
talkBreak ? theme.spacing * 6 :
|
||||
combine ? theme.spacing / 2 :
|
||||
theme.spacing * 2
|
||||
|
||||
// Needed because of eventList's MouseArea which steals the
|
||||
// HSelectableLabel's MouseArea hover events
|
||||
onCursorShapeChanged: eventList.cursorShape = cursorShape
|
||||
|
||||
|
||||
function json() {
|
||||
return JSON.stringify(
|
||||
{
|
||||
"model": utils.getItem(
|
||||
modelSources[[
|
||||
"Event", chat.userId, chat.roomId
|
||||
]],
|
||||
"client_id",
|
||||
model.client_id
|
||||
),
|
||||
"source": py.getattr(model.source, "__dict__"),
|
||||
},
|
||||
null, 4)
|
||||
}
|
||||
|
||||
function openContextMenu() {
|
||||
contextMenu.media = eventDelegate.hoveredMediaTypeUrl
|
||||
contextMenu.link = eventContent.hoveredLink
|
||||
contextMenu.popup()
|
||||
}
|
||||
|
||||
|
||||
Item {
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight:
|
||||
model.event_type === "RoomCreateEvent" ? 0 : separationSpacing
|
||||
}
|
||||
|
||||
Daybreak {
|
||||
visible: dayBreak
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: parent.width
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: dayBreak
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: separationSpacing
|
||||
}
|
||||
|
||||
EventContent {
|
||||
id: eventContent
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
Behavior on x { HNumberAnimation {} }
|
||||
}
|
||||
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: openContextMenu()
|
||||
}
|
||||
|
||||
HMenu {
|
||||
id: contextMenu
|
||||
|
||||
property var media: []
|
||||
property string link: ""
|
||||
|
||||
onClosed: { media = []; link = "" }
|
||||
|
||||
HMenuItem {
|
||||
id: copyMedia
|
||||
icon.name: "copy-link"
|
||||
text:
|
||||
contextMenu.media.length < 1 ? "" :
|
||||
|
||||
contextMenu.media[0] === EventDelegate.Media.Page ?
|
||||
qsTr("Copy page address") :
|
||||
|
||||
contextMenu.media[0] === EventDelegate.Media.File ?
|
||||
qsTr("Copy file address") :
|
||||
|
||||
contextMenu.media[0] === EventDelegate.Media.Image ?
|
||||
qsTr("Copy image address") :
|
||||
|
||||
contextMenu.media[0] === EventDelegate.Media.Video ?
|
||||
qsTr("Copy video address") :
|
||||
|
||||
contextMenu.media[0] === EventDelegate.Media.Audio ?
|
||||
qsTr("Copy audio address") :
|
||||
|
||||
qsTr("Copy media address")
|
||||
|
||||
visible: Boolean(text)
|
||||
onTriggered: Clipboard.text = contextMenu.media[1]
|
||||
}
|
||||
|
||||
HMenuItem {
|
||||
id: copyLink
|
||||
icon.name: "copy-link"
|
||||
text: qsTr("Copy link address")
|
||||
visible: Boolean(contextMenu.link)
|
||||
onTriggered: Clipboard.text = contextMenu.link
|
||||
}
|
||||
|
||||
HMenuItem {
|
||||
icon.name: "copy-text"
|
||||
text: qsTr("Copy text")
|
||||
visible: enabled || (! copyLink.visible && ! copyMedia.visible)
|
||||
enabled: Boolean(selectableLabelContainer.joinedSelection)
|
||||
onTriggered:
|
||||
Clipboard.text = selectableLabelContainer.joinedSelection
|
||||
}
|
||||
|
||||
HMenuItem {
|
||||
icon.name: "clear-messages"
|
||||
text: qsTr("Clear messages")
|
||||
onTriggered: utils.makePopup(
|
||||
"Popups/ClearMessagesPopup.qml",
|
||||
chat,
|
||||
{userId: chat.userId, roomId: chat.roomId},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
42
src/gui/Pages/Chat/Timeline/EventFile.qml
Normal file
42
src/gui/Pages/Chat/Timeline/EventFile.qml
Normal file
@@ -0,0 +1,42 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../../Base"
|
||||
|
||||
HTile {
|
||||
id: file
|
||||
width: Math.min(
|
||||
eventDelegate.width,
|
||||
eventContent.maxMessageWidth,
|
||||
Math.max(theme.chat.message.fileMinWidth, implicitWidth),
|
||||
)
|
||||
height: Math.max(theme.chat.message.avatarSize, implicitHeight)
|
||||
|
||||
title.text: loader.singleMediaInfo.media_title || qsTr("Untitled file")
|
||||
title.elide: Text.ElideMiddle
|
||||
subtitle.text: CppUtils.formattedBytes(loader.singleMediaInfo.media_size)
|
||||
|
||||
image: HIcon {
|
||||
svgName: "download"
|
||||
}
|
||||
|
||||
onLeftClicked: download(Qt.openUrlExternally)
|
||||
onRightClicked: eventDelegate.openContextMenu()
|
||||
|
||||
onHoveredChanged: {
|
||||
if (! hovered) {
|
||||
eventDelegate.hoveredMediaTypeUrl = []
|
||||
return
|
||||
}
|
||||
|
||||
eventDelegate.hoveredMediaTypeUrl = [
|
||||
EventDelegate.Media.File,
|
||||
loader.downloadedPath.replace(/^file:\/\//, "") || loader.mediaUrl
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
property EventMediaLoader loader
|
||||
|
||||
readonly property bool cryptDict: loader.singleMediaInfo.media_crypt_dict
|
||||
readonly property bool isEncrypted: ! utils.isEmptyObject(cryptDict)
|
||||
}
|
119
src/gui/Pages/Chat/Timeline/EventImage.qml
Normal file
119
src/gui/Pages/Chat/Timeline/EventImage.qml
Normal file
@@ -0,0 +1,119 @@
|
||||
import QtQuick 2.12
|
||||
import "../../../Base"
|
||||
|
||||
HMxcImage {
|
||||
id: image
|
||||
width: fitSize.width
|
||||
height: fitSize.height
|
||||
horizontalAlignment: Image.AlignLeft
|
||||
|
||||
animated: loader.singleMediaInfo.media_mime === "image/gif" ||
|
||||
utils.urlExtension(loader.mediaUrl) === "gif"
|
||||
thumbnail: ! animated && loader.thumbnailMxc
|
||||
mxc: thumbnail ?
|
||||
(loader.thumbnailMxc || loader.mediaUrl) :
|
||||
(loader.mediaUrl || loader.thumbnailMxc)
|
||||
cryptDict: thumbnail && loader.thumbnailMxc ?
|
||||
loader.singleMediaInfo.thumbnail_crypt_dict :
|
||||
loader.singleMediaInfo.media_crypt_dict
|
||||
|
||||
|
||||
property EventMediaLoader loader
|
||||
|
||||
readonly property bool isEncrypted: ! utils.isEmptyObject(cryptDict)
|
||||
|
||||
readonly property real maxHeight:
|
||||
theme.chat.message.thumbnailMaxHeightRatio
|
||||
|
||||
readonly property size fitSize: utils.fitSize(
|
||||
// Minimum display size
|
||||
theme.chat.message.thumbnailMinSize.width,
|
||||
theme.chat.message.thumbnailMinSize.height,
|
||||
|
||||
// Real size
|
||||
(
|
||||
loader.singleMediaInfo.thumbnail_width ||
|
||||
loader.singleMediaInfo.media_width ||
|
||||
implicitWidth ||
|
||||
800
|
||||
) * theme.uiScale,
|
||||
|
||||
(
|
||||
loader.singleMediaInfo.thumbnail_height ||
|
||||
loader.singleMediaInfo.media_height ||
|
||||
implicitHeight ||
|
||||
600
|
||||
) * theme.uiScale,
|
||||
|
||||
// Maximum display size
|
||||
Math.min(
|
||||
eventList.height * maxHeight,
|
||||
eventContent.maxMessageWidth * Math.min(1, theme.uiScale), // XXX
|
||||
),
|
||||
eventList.height * maxHeight,
|
||||
)
|
||||
|
||||
|
||||
function getOpenUrl(callback) {
|
||||
if (image.isEncrypted && loader.mediaUrl) {
|
||||
loader.download(callback)
|
||||
return
|
||||
}
|
||||
|
||||
if (image.isEncrypted) {
|
||||
callback(image.cachedPath)
|
||||
return
|
||||
}
|
||||
|
||||
let toOpen = loader.mediaUrl || loader.thumbnailMxc
|
||||
let isMxc = toOpen.startsWith("mxc://")
|
||||
|
||||
isMxc ?
|
||||
py.callClientCoro(chat.userId, "mxc_to_http", [toOpen], callback) :
|
||||
callback(toOpen)
|
||||
}
|
||||
|
||||
|
||||
TapHandler {
|
||||
onTapped: if (! image.animated) getOpenUrl(Qt.openUrlExternally)
|
||||
onDoubleTapped: getOpenUrl(Qt.openUrlExternally)
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: hover
|
||||
onHoveredChanged: {
|
||||
if (! hovered) {
|
||||
eventDelegate.hoveredMediaTypeUrl = []
|
||||
return
|
||||
}
|
||||
|
||||
eventDelegate.hoveredMediaTypeUrl = [
|
||||
EventDelegate.Media.Image,
|
||||
loader.downloadedPath.replace(/^file:\/\//, "") ||
|
||||
loader.mediaUrl
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
EventImageTextBubble {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
text: loader.showSender
|
||||
textFormat: Text.StyledText
|
||||
opacity: hover.hovered ? 0 : 1
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity { HNumberAnimation {} }
|
||||
}
|
||||
|
||||
EventImageTextBubble {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
text: [loader.showDate, loader.showLocalEcho].join(" ").trim()
|
||||
textFormat: Text.StyledText
|
||||
opacity: hover.hovered ? 0 : 1
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity { HNumberAnimation {} }
|
||||
}
|
||||
}
|
24
src/gui/Pages/Chat/Timeline/EventImageTextBubble.qml
Normal file
24
src/gui/Pages/Chat/Timeline/EventImageTextBubble.qml
Normal file
@@ -0,0 +1,24 @@
|
||||
import QtQuick 2.12
|
||||
import "../../../Base"
|
||||
|
||||
HLabel {
|
||||
id: bubble
|
||||
anchors.margins: theme.spacing / 4
|
||||
|
||||
topPadding: theme.spacing / 2
|
||||
bottomPadding: topPadding
|
||||
leftPadding: theme.spacing / 1.5
|
||||
rightPadding: leftPadding
|
||||
|
||||
font.pixelSize: theme.fontSize.small
|
||||
|
||||
background: Rectangle {
|
||||
color: Qt.hsla(0, 0, 0, 0.7)
|
||||
radius: theme.radius
|
||||
}
|
||||
|
||||
Binding on visible {
|
||||
value: false
|
||||
when: ! Boolean(bubble.text)
|
||||
}
|
||||
}
|
173
src/gui/Pages/Chat/Timeline/EventList.qml
Normal file
173
src/gui/Pages/Chat/Timeline/EventList.qml
Normal file
@@ -0,0 +1,173 @@
|
||||
import QtQuick 2.12
|
||||
import "../../../Base"
|
||||
|
||||
Rectangle {
|
||||
property alias selectableLabelContainer: selectableLabelContainer
|
||||
property alias eventList: eventList
|
||||
|
||||
color: theme.chat.eventList.background
|
||||
|
||||
HSelectableLabelContainer {
|
||||
id: selectableLabelContainer
|
||||
anchors.fill: parent
|
||||
reversed: eventList.verticalLayoutDirection === ListView.BottomToTop
|
||||
|
||||
DragHandler {
|
||||
target: null
|
||||
onActiveChanged: if (! active) dragFlicker.speed = 0
|
||||
onCentroidChanged: {
|
||||
let left = centroid.pressedButtons & Qt.LeftButton
|
||||
let vel = centroid.velocity.y
|
||||
let pos = centroid.position.y
|
||||
let dist = Math.min(selectableLabelContainer.height / 4, 50)
|
||||
let boost = 20 * (pos < dist ? -pos : -(height - pos))
|
||||
|
||||
dragFlicker.speed =
|
||||
left && vel && pos < dist ? 1000 + boost :
|
||||
left && vel && pos > height - dist ? -1000 + -boost :
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: dragFlicker
|
||||
interval: 100
|
||||
running: speed !== 0
|
||||
repeat: true
|
||||
|
||||
onTriggered: {
|
||||
if (eventList.verticalOvershoot !== 0) return
|
||||
if (speed < 0 && eventList.atYEnd) return
|
||||
if (eventList.atYBeggining) {
|
||||
if (bouncedStart) { return } else { bouncedStart = true }
|
||||
}
|
||||
|
||||
eventList.flick(0, speed * acceleration)
|
||||
acceleration = Math.min(8, acceleration * 1.05)
|
||||
}
|
||||
onRunningChanged: if (! running) {
|
||||
acceleration = 1.0
|
||||
bouncedStart = false
|
||||
eventList.cancelFlick()
|
||||
eventList.returnToBounds()
|
||||
}
|
||||
|
||||
property real speed: 0.0
|
||||
property real acceleration: 1.0
|
||||
property bool bouncedStart: false
|
||||
}
|
||||
|
||||
HListView {
|
||||
id: eventList
|
||||
clip: true
|
||||
allowDragging: false
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: theme.spacing
|
||||
anchors.rightMargin: theme.spacing
|
||||
|
||||
topMargin: theme.spacing
|
||||
bottomMargin: theme.spacing
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
|
||||
// Keep x scroll pages cached, to limit images having to be
|
||||
// reloaded from network.
|
||||
cacheBuffer: height * 2
|
||||
|
||||
onYPosChanged:
|
||||
if (canLoad && yPos < 0.1) Qt.callLater(loadPastEvents)
|
||||
|
||||
// When an invited room becomes joined, we should now be able to
|
||||
// fetch past events.
|
||||
onInviterChanged: canLoad = true
|
||||
|
||||
Component.onCompleted: shortcuts.flickTarget = eventList
|
||||
|
||||
|
||||
property string inviter: chat.roomInfo.inviter || ""
|
||||
property real yPos: visibleArea.yPosition
|
||||
property bool canLoad: true
|
||||
|
||||
property bool ownEventsOnRight:
|
||||
width < theme.chat.eventList.ownEventsOnRightUnderWidth
|
||||
|
||||
|
||||
function canCombine(item, itemAfter) {
|
||||
if (! item || ! itemAfter) return false
|
||||
|
||||
return Boolean(
|
||||
! canTalkBreak(item, itemAfter) &&
|
||||
! canDayBreak(item, itemAfter) &&
|
||||
item.sender_id === itemAfter.sender_id &&
|
||||
utils.minutesBetween(item.date, itemAfter.date) <= 5
|
||||
)
|
||||
}
|
||||
|
||||
function canTalkBreak(item, itemAfter) {
|
||||
if (! item || ! itemAfter) return false
|
||||
|
||||
return Boolean(
|
||||
! canDayBreak(item, itemAfter) &&
|
||||
utils.minutesBetween(item.date, itemAfter.date) >= 20
|
||||
)
|
||||
}
|
||||
|
||||
function canDayBreak(item, itemAfter) {
|
||||
if (itemAfter && itemAfter.event_type === "RoomCreateEvent")
|
||||
return true
|
||||
|
||||
if (! item || ! itemAfter || ! item.date || ! itemAfter.date)
|
||||
return false
|
||||
|
||||
return item.date.getDate() !== itemAfter.date.getDate()
|
||||
}
|
||||
|
||||
function loadPastEvents() {
|
||||
// try/catch blocks to hide pyotherside error when the
|
||||
// component is destroyed but func is still running
|
||||
|
||||
try {
|
||||
eventList.canLoad = false
|
||||
chat.loadingMessages = true
|
||||
|
||||
py.callClientCoro(
|
||||
chat.userId, "load_past_events", [chat.roomId],
|
||||
moreToLoad => {
|
||||
try {
|
||||
eventList.canLoad = moreToLoad
|
||||
|
||||
// Call yPosChanged() to run this func again
|
||||
// if the loaded messages aren't enough to fill
|
||||
// the screen.
|
||||
if (moreToLoad) yPosChanged()
|
||||
|
||||
chat.loadingMessages = false
|
||||
} catch (err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
model: HListModel {
|
||||
keyField: "client_id"
|
||||
source: modelSources[[
|
||||
"Event", chat.userId, chat.roomId
|
||||
]] || []
|
||||
}
|
||||
|
||||
delegate: EventDelegate {}
|
||||
}
|
||||
}
|
||||
|
||||
HNoticePage {
|
||||
text: qsTr("No messages to show yet")
|
||||
|
||||
visible: eventList.model.count < 1
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
89
src/gui/Pages/Chat/Timeline/EventMediaLoader.qml
Normal file
89
src/gui/Pages/Chat/Timeline/EventMediaLoader.qml
Normal file
@@ -0,0 +1,89 @@
|
||||
import QtQuick 2.12
|
||||
import "../../../Base"
|
||||
|
||||
HLoader {
|
||||
id: loader
|
||||
x: eventContent.spacing
|
||||
|
||||
onTypeChanged: {
|
||||
if (type === EventDelegate.Media.Image) {
|
||||
var file = "EventImage.qml"
|
||||
|
||||
} else if (type !== EventDelegate.Media.Page) {
|
||||
var file = "EventFile.qml"
|
||||
|
||||
} else { return }
|
||||
|
||||
loader.setSource(file, {loader})
|
||||
}
|
||||
|
||||
|
||||
property QtObject singleMediaInfo
|
||||
property string mediaUrl
|
||||
property string showSender: ""
|
||||
property string showDate: ""
|
||||
property string showLocalEcho: ""
|
||||
|
||||
property string downloadedPath: ""
|
||||
|
||||
readonly property var imageExtensions: [
|
||||
"bmp", "gif", "jpg", "jpeg", "png", "pbm", "pgm", "ppm", "xbm", "xpm",
|
||||
"tiff", "webp", "svg",
|
||||
]
|
||||
|
||||
readonly property var videoExtensions: [
|
||||
"3gp", "avi", "flv", "m4p", "m4v", "mkv", "mov", "mp4",
|
||||
"mpeg", "mpg", "ogv", "qt", "vob", "webm", "wmv", "yuv",
|
||||
]
|
||||
|
||||
readonly property var audioExtensions: [
|
||||
"pcm", "wav", "raw", "aiff", "flac", "m4a", "tta", "aac", "mp3",
|
||||
"ogg", "oga", "opus",
|
||||
]
|
||||
|
||||
readonly property int type: {
|
||||
if (singleMediaInfo.event_type === "RoomAvatarEvent")
|
||||
return EventDelegate.Media.Image
|
||||
|
||||
let mainType = singleMediaInfo.media_mime.split("/")[0].toLowerCase()
|
||||
|
||||
if (mainType === "image") return EventDelegate.Media.Image
|
||||
if (mainType === "video") return EventDelegate.Media.Video
|
||||
if (mainType === "audio") return EventDelegate.Media.Audio
|
||||
|
||||
let fileEvents = ["RoomMessageFile", "RoomEncryptedFile"]
|
||||
|
||||
if (fileEvents.includes(singleMediaInfo.event_type))
|
||||
return EventDelegate.Media.File
|
||||
|
||||
// If this is a preview for a link in a normal message
|
||||
let ext = utils.urlExtension(mediaUrl)
|
||||
|
||||
if (imageExtensions.includes(ext)) return EventDelegate.Media.Image
|
||||
if (videoExtensions.includes(ext)) return EventDelegate.Media.Video
|
||||
if (audioExtensions.includes(ext)) return EventDelegate.Media.Audio
|
||||
|
||||
return EventDelegate.Media.Page
|
||||
}
|
||||
|
||||
readonly property string thumbnailMxc: singleMediaInfo.thumbnail_url
|
||||
|
||||
|
||||
function download(callback) {
|
||||
if (! loader.mediaUrl.startsWith("mxc://")) {
|
||||
downloadedPath = loader.mediaUrl
|
||||
callback(loader.mediaUrl)
|
||||
return
|
||||
}
|
||||
|
||||
if (! downloadedPath) print("Downloading " + loader.mediaUrl + " ...")
|
||||
|
||||
const args = [loader.mediaUrl, loader.singleMediaInfo.media_crypt_dict]
|
||||
|
||||
py.callCoro("media_cache.get_media", args, path => {
|
||||
if (! downloadedPath) print("Done: " + path)
|
||||
downloadedPath = path
|
||||
callback(path)
|
||||
})
|
||||
}
|
||||
}
|
13
src/gui/Pages/Chat/Timeline/EventVideo.qml
Normal file
13
src/gui/Pages/Chat/Timeline/EventVideo.qml
Normal file
@@ -0,0 +1,13 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtAV 1.7
|
||||
import "../../../Base"
|
||||
import "../../../Base/MediaPlayer"
|
||||
|
||||
VideoPlayer {
|
||||
id: video
|
||||
|
||||
onHoveredChanged:
|
||||
eventDelegate.hoveredMediaTypeUrl =
|
||||
hovered ? [EventDelegate.Media.Video, video.source] : []
|
||||
}
|
Reference in New Issue
Block a user