Move Chat/ dir under Pages/

This commit is contained in:
miruka
2019-12-18 04:53:08 -04:00
parent 2bdf21d528
commit f4d7636df6
30 changed files with 38 additions and 38 deletions

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

View 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] : []
}
}

View 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 ?
`&nbsp;<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 ?
`&nbsp;<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 {}
}

View 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},
)
}
}
}

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

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

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

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

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

View 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] : []
}