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,93 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../../Base"
Rectangle {
id: banner
implicitHeight: childrenRect.height
color: theme.controls.box.background
property alias avatar: bannerAvatar
property alias icon: bannerIcon
property alias labelText: bannerLabel.text
property alias buttonModel: bannerRepeater.model
property var buttonCallbacks: []
HGridLayout {
id: bannerGrid
width: parent.width
flow: bannerAvatarWrapper.width +
bannerIcon.width +
bannerLabel.implicitWidth +
bannerButtons.width >
parent.width ?
GridLayout.TopToBottom : GridLayout.LeftToRight
HRowLayout {
id: bannerRow
Rectangle {
id: bannerAvatarWrapper
color: "black"
Layout.preferredWidth: bannerAvatar.width
Layout.minimumHeight: bannerAvatar.height
Layout.preferredHeight: bannerLabel.height
HUserAvatar {
id: bannerAvatar
anchors.centerIn: parent
}
}
HIcon {
id: bannerIcon
visible: Boolean(svgName)
Layout.leftMargin: theme.spacing / 2
}
HLabel {
id: bannerLabel
textFormat: Text.StyledText
wrapMode: Text.Wrap
Layout.fillWidth: true
Layout.leftMargin: bannerIcon.Layout.leftMargin
Layout.rightMargin: Layout.leftMargin
}
HSpacer {}
}
HRowLayout {
HRowLayout {
id: bannerButtons
Repeater {
id: bannerRepeater
model: []
HButton {
id: button
text: modelData.text
icon.name: modelData.iconName
icon.color: modelData.iconColor || theme.icons.colorize
onClicked: buttonCallbacks[modelData.name](button)
Layout.preferredHeight: theme.baseElementsHeight
}
}
}
Rectangle {
id: buttonsRightPadding
color: theme.controls.button.background
visible: bannerGrid.flow === GridLayout.TopToBottom
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}
}

View File

@@ -0,0 +1,51 @@
import QtQuick 2.12
import "../../../Base"
Banner {
property string inviterId: chat.roomInfo.inviter
property string inviterName: chat.roomInfo.inviter_name
property string inviterAvatar: chat.roomInfo.inviter_avatar
color: theme.chat.inviteBanner.background
avatar.userId: inviterId
avatar.displayName: inviterName
avatar.mxc: inviterAvatar
labelText: qsTr("%1 invited you to this room").arg(
utils.coloredNameHtml(inviterName, inviterId)
)
buttonModel: [
{
name: "accept",
text: qsTr("Join"),
iconName: "invite-accept",
iconColor: theme.colors.positiveBackground
},
{
name: "decline",
text: qsTr("Decline"),
iconName: "invite-decline",
iconColor: theme.colors.negativeBackground
}
]
buttonCallbacks: ({
accept: button => {
button.loading = true
py.callClientCoro(
chat.userId, "join", [chat.roomId], () => {
button.loading = false
})
},
decline: button => {
button.loading = true
py.callClientCoro(
chat.userId, "room_leave", [chat.roomId], () => {
button.loading = false
})
}
})
}

View File

@@ -0,0 +1,39 @@
import QtQuick 2.12
import "../../../Base"
Banner {
color: theme.chat.leftBanner.background
// TODO: avatar func auto
avatar.userId: chat.userId
avatar.displayName: chat.userInfo.display_name
avatar.mxc: chat.userInfo.avatar_url
labelText: qsTr("You are not part of this room anymore")
buttonModel: [
{
name: "forget",
text: qsTr("Forget"),
iconName: "room-forget",
iconColor: theme.colors.negativeBackground
}
]
buttonCallbacks: ({
forget: button => {
utils.makePopup(
"Popups/ForgetRoomPopup.qml",
mainUI, // Must not be destroyed with chat
{
userId: chat.userId,
roomId: chat.roomId,
roomName: chat.roomInfo.display_name,
},
obj => {
obj.onOk.connect(() => { button.loading = true })
},
false,
)
}
})
}

View File

@@ -0,0 +1,24 @@
import QtQuick 2.12
import "../../../Base"
Banner {
color: theme.chat.unknownDevices.background
avatar.visible: false
icon.svgName: "unknown-devices-warning"
labelText: qsTr("Unknown devices are present in this encrypted room")
buttonModel: [
{
name: "inspect",
text: qsTr("Inspect"),
iconName: "unknown-devices-inspect",
}
]
buttonCallbacks: ({
inspect: button => {
print("show")
}
})
}

View File

@@ -0,0 +1,59 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
import "RoomPane"
Item {
id: chat
onFocusChanged: if (focus && loader.item) loader.item.composer.takeFocus()
property string userId: ""
property string roomId: ""
property bool loadingMessages: false
property bool ready: userInfo !== "waiting" && roomInfo !== "waiting"
readonly property var userInfo:
utils.getItem(modelSources["Account"] || [], "user_id", userId) ||
"waiting"
readonly property var roomInfo: utils.getItem(
modelSources[["Room", userId]] || [], "room_id", roomId
) || "waiting"
readonly property alias loader: loader
readonly property alias roomPane: roomPaneLoader.item
HLoader {
id: loader
anchors.rightMargin: ready ? roomPane.visibleSize : 0
anchors.fill: parent
visible:
ready ? ! roomPane.hidden || anchors.rightMargin < width : true
onLoaded: if (chat.focus) item.composer.takeFocus()
source: ready ? "ChatPage.qml" : ""
HLoader {
anchors.centerIn: parent
width: 96 * theme.uiScale
height: width
source: opacity > 0 ? "../../Base/HBusyIndicator.qml" : ""
opacity: ready ? 0 : 1
Behavior on opacity { HNumberAnimation { factor: 2 } }
}
}
HLoader {
id: roomPaneLoader
active: ready
sourceComponent: RoomPane {
id: roomPane
referenceSizeParent: chat
}
}
}

View File

@@ -0,0 +1,70 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
import "Banners"
import "Timeline"
import "FileTransfer"
HPage {
id: chatPage
leftPadding: 0
rightPadding: 0
// The target will be our EventList, not the page itself
becomeKeyboardFlickableTarget: false
readonly property alias composer: composer
RoomHeader {
Layout.fillWidth: true
}
LoadingRoomProgressBar {
Layout.fillWidth: true
}
EventList {
id: eventList
// Avoid a certain binding loop
Layout.minimumWidth: theme.minimumSupportedWidth
Layout.fillWidth: true
Layout.fillHeight: true
}
TypingMembersBar {
Layout.fillWidth: true
}
TransferList {
Layout.fillWidth: true
Layout.minimumHeight: implicitHeight
Layout.preferredHeight: implicitHeight * transferCount
Layout.maximumHeight: chatPage.height / 6
Behavior on Layout.preferredHeight { HNumberAnimation {} }
}
InviteBanner {
id: inviteBanner
visible: ! chat.roomInfo.left && inviterId
inviterId: chat.roomInfo.inviter_id
Layout.fillWidth: true
}
LeftBanner {
id: leftBanner
visible: chat.roomInfo.left
Layout.fillWidth: true
}
Composer {
id: composer
visible: ! chat.roomInfo.left &&
! chat.roomInfo.inviter_id
}
}

View File

@@ -0,0 +1,230 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
import "../../Dialogs"
Rectangle {
property string indent: " "
property var aliases: window.settings.writeAliases
property string toSend: ""
property string writingUserId: chat.userId
readonly property var writingUserInfo:
utils.getItem(modelSources["Account"] || [], "user_id", writingUserId)
property bool textChangedSinceLostFocus: false
property alias textArea: areaScrollView.area
readonly property int cursorPosition:
textArea.cursorPosition
readonly property int cursorY:
textArea.text.substring(0, cursorPosition).split("\n").length - 1
readonly property int cursorX:
cursorPosition - lines.slice(0, cursorY).join("").length - cursorY
readonly property var lines: textArea.text.split("\n")
readonly property string lineText: lines[cursorY] || ""
readonly property string lineTextUntilCursor:
lineText.substring(0, cursorX)
readonly property int deleteCharsOnBackspace:
lineTextUntilCursor.match(/^ +$/) ?
lineTextUntilCursor.match(/ {1,4}/g).slice(-1)[0].length :
1
function takeFocus() { areaScrollView.forceActiveFocus() }
// property var pr: lineTextUntilCursor
// onPrChanged: print(
// "y", cursorY, "x", cursorX,
// "ltuc <" + lineTextUntilCursor + ">", "dob",
// deleteCharsOnBackspace, "m", lineTextUntilCursor.match(/^ +$/))
id: composer
Layout.fillWidth: true
Layout.minimumHeight: theme.baseElementsHeight
Layout.preferredHeight: areaScrollView.implicitHeight
Layout.maximumHeight: pageLoader.height / 2
color: theme.chat.composer.background
HRowLayout {
anchors.fill: parent
HUserAvatar {
id: avatar
userId: writingUserId
displayName: writingUserInfo.display_name
mxc: writingUserInfo.avatar_url
}
HScrollableTextArea {
id: areaScrollView
saveName: "composer"
saveId: [chat.roomId, writingUserId]
enabled: chat.roomInfo.can_send_messages
disabledText:
qsTr("You do not have permission to post in this room")
placeholderText: qsTr("Type a message...")
backgroundColor: "transparent"
area.tabStopDistance: 4 * 4 // 4 spaces
area.focus: true
Layout.fillHeight: true
Layout.fillWidth: true
function setTyping(typing) {
py.callClientCoro(
writingUserId,
"room_typing",
[chat.roomId, typing, 5000]
)
}
onTextChanged: {
if (utils.isEmptyObject(aliases)) {
writingUserId = Qt.binding(() => chat.userId)
toSend = text
setTyping(Boolean(text))
textChangedSinceLostFocus = true
return
}
let foundAlias = null
for (let [user, writing_alias] of Object.entries(aliases)) {
if (text.startsWith(writing_alias + " ")) {
writingUserId = user
foundAlias = new RegExp("^" + writing_alias + " ")
break
}
}
if (foundAlias) {
toSend = text.replace(foundAlias, "")
setTyping(Boolean(text))
textChangedSinceLostFocus = true
return
}
writingUserId = Qt.binding(() => chat.userId)
toSend = text
let vals = Object.values(aliases)
let longestAlias =
vals.reduce((a, b) => a.length > b.length ? a: b)
let textNotStartsWithAnyAlias =
! vals.some(a => a.startsWith(text))
let textContainsCharNotInAnyAlias =
vals.every(a => text.split("").some(c => ! a.includes(c)))
// Only set typing when it's sure that the user will not use
// an alias and has written something
if (toSend &&
(text.length > longestAlias.length ||
textNotStartsWithAnyAlias ||
textContainsCharNotInAnyAlias))
{
setTyping(Boolean(text))
textChangedSinceLostFocus = true
}
}
area.onEditingFinished: { // when lost focus
if (text && textChangedSinceLostFocus) {
setTyping(false)
textChangedSinceLostFocus = false
}
}
area.onSelectedTextChanged: if (area.selectedText) {
eventList.selectableLabelContainer.clearSelection()
}
Component.onCompleted: {
area.Keys.onReturnPressed.connect(ev => {
ev.accepted = true
if (ev.modifiers & Qt.ShiftModifier ||
ev.modifiers & Qt.ControlModifier ||
ev.modifiers & Qt.AltModifier)
{
let indents = 0
let parts = lineText.split(indent)
for (const [i, part] of parts.entries()) {
if (i === parts.length - 1 || part) { break }
indents += 1
}
let add = indent.repeat(indents)
textArea.insert(cursorPosition, "\n" + add)
return
}
if (textArea.text === "") { return }
let args = [chat.roomId, toSend]
py.callClientCoro(writingUserId, "send_text", args)
area.clear()
})
area.Keys.onEnterPressed.connect(area.Keys.onReturnPressed)
area.Keys.onTabPressed.connect(ev => {
ev.accepted = true
textArea.insert(cursorPosition, indent)
})
area.Keys.onPressed.connect(ev => {
if (ev.matches(StandardKey.Copy) &&
eventList.selectableLabelContainer.joinedSelection
) {
ev.accepted = true
Clipboard.text =
eventList.selectableLabelContainer.joinedSelection
return
}
if (ev.modifiers === Qt.NoModifier &&
ev.key === Qt.Key_Backspace &&
! textArea.selectedText)
{
ev.accepted = true
textArea.remove(
cursorPosition - deleteCharsOnBackspace,
cursorPosition
)
}
})
}
}
HButton {
enabled: chat.roomInfo.can_send_messages
icon.name: "upload-file"
backgroundColor: theme.chat.composer.uploadButton.background
toolTip.text: qsTr("Send files")
onClicked: sendFilePicker.dialog.open()
Layout.fillHeight: true
SendFilePicker {
id: sendFilePicker
userId: chat.userId
roomId: chat.roomId
}
}
}
}

View File

@@ -0,0 +1,192 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../../Base"
HColumnLayout {
id: transfer
property bool paused: false
property int msLeft: model.time_left || 0
property int uploaded: model.uploaded
readonly property int speed: model.speed
readonly property int totalSize: model.total_size
readonly property string status: model.status
function cancel() {
// Python might take a sec to cancel, but we want
// immediate visual feedback
transfer.height = 0
// Python will delete this model item on cancel
py.call(py.getattr(model.task, "cancel"))
}
function pause() {
transfer.paused = ! transfer.paused
py.setattr(model.monitor, "pause", transfer.paused)
}
Behavior on height { HNumberAnimation {} }
HRowLayout {
HIcon {
svgName: "uploading"
colorize:
transfer.status === "Error" ? theme.colors.negativeBackground :
transfer.paused ? theme.colors.middleBackground :
theme.icons.colorize
Layout.preferredWidth: theme.baseElementsHeight
}
HLabel {
id: statusLabel
elide: expand ? Text.ElideNone : Text.ElideRight
wrapMode: expand ? Text.Wrap : Text.NoWrap
text:
status === "Uploading" ? fileName :
status === "Caching" ?
qsTr("Caching %1...").arg(fileName) :
model.error === "MatrixForbidden" ?
qsTr("Forbidden file type or quota exceeded: %1")
.arg(fileName) :
model.error === "MatrixTooLarge" ?
qsTr("Too large for this server: %1").arg(fileName) :
model.error === "IsADirectoryError" ?
qsTr("Can't upload folders, need a file: %1").arg(filePath) :
model.error === "FileNotFoundError" ?
qsTr("Non-existant file: %1").arg(filePath) :
model.error === "PermissionError" ?
qsTr("No permission to read this file: %1").arg(filePath) :
qsTr("Unknown error for %1: %2 - %3")
.arg(filePath).arg(model.error).arg(model.error_args)
topPadding: theme.spacing / 2
bottomPadding: topPadding
leftPadding: theme.spacing / 1.5
rightPadding: leftPadding
Layout.fillWidth: true
property bool expand: status === "Error"
readonly property string fileName:
model.filepath.split("/").slice(-1)[0]
readonly property string filePath:
model.filepath.replace(/^file:\/\//, "")
HoverHandler { id: statusLabelHover }
HToolTip {
text: parent.truncated ? parent.text : ""
visible: text && statusLabelHover.hovered
}
}
HSpacer {}
Repeater {
model: [
msLeft ? qsTr("-%1").arg(utils.formatDuration(msLeft)) : "",
speed ? qsTr("%1/s").arg(CppUtils.formattedBytes(speed)) : "",
qsTr("%1/%2").arg(CppUtils.formattedBytes(uploaded))
.arg(CppUtils.formattedBytes(totalSize)),
]
HLabel {
text: modelData
visible: text && Layout.preferredWidth > 0
leftPadding: theme.spacing / 1.5
rightPadding: leftPadding
Layout.preferredWidth:
status === "Uploading" ? implicitWidth : 0
Behavior on Layout.preferredWidth { HNumberAnimation {} }
}
}
HButton {
visible: Layout.preferredWidth > 0
padded: false
icon.name: transfer.paused ?
"upload-resume" : "upload-pause"
icon.color: transfer.paused ?
theme.colors.positiveBackground :
theme.colors.middleBackground
toolTip.text: transfer.paused ?
qsTr("Resume") : qsTr("Pause")
onClicked: transfer.pause()
Layout.preferredWidth:
status === "Uploading" ?
theme.baseElementsHeight : 0
Layout.fillHeight: true
Behavior on Layout.preferredWidth { HNumberAnimation {} }
}
HButton {
icon.name: "upload-cancel"
icon.color: theme.colors.negativeBackground
padded: false
onClicked: transfer.cancel()
Layout.preferredWidth: theme.baseElementsHeight
Layout.fillHeight: true
}
TapHandler {
onTapped: {
if (status === "Error") { transfer.cancel() }
else { statusLabel.expand = ! statusLabel.expand }
}
}
}
HProgressBar {
id: progressBar
visible: Layout.maximumHeight !== 0
indeterminate: status !== "Uploading"
value: uploaded
to: totalSize
// TODO: bake this in hprogressbar
foregroundColor:
status === "Error" ?
theme.controls.progressBar.errorForeground :
transfer.paused ?
theme.controls.progressBar.pausedForeground :
theme.controls.progressBar.foreground
Layout.fillWidth: true
Layout.maximumHeight:
status === "Error" && indeterminate ? 0 : -1
Behavior on value { HNumberAnimation { duration: 1200 } }
Behavior on Layout.maximumHeight { HNumberAnimation {} }
}
}

View File

@@ -0,0 +1,33 @@
import QtQuick 2.12
import "../../../Base"
Rectangle {
implicitWidth: 800
implicitHeight: firstDelegate ? firstDelegate.height : 0
color: theme.chat.fileTransfer.background
opacity: implicitHeight ? 1 : 0
clip: true
property int delegateHeight: 0
readonly property var firstDelegate:
transferList.contentItem.visibleChildren[0]
readonly property alias transferCount: transferList.count
Behavior on implicitHeight { HNumberAnimation {} }
HListView {
id: transferList
anchors.fill: parent
model: HListModel {
keyField: "uuid"
source: modelSources[["Upload", chat.roomId]] || []
}
delegate: Transfer { width: transferList.width }
}
}

View File

@@ -0,0 +1,11 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
HProgressBar {
indeterminate: true
height: chat.loadingMessages ? implicitHeight : 0
visible: height > 0
Behavior on height { HNumberAnimation {} }
}

View File

@@ -0,0 +1,69 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
Rectangle {
implicitHeight: theme.baseElementsHeight
color: theme.chat.roomHeader.background
HRowLayout {
id: row
anchors.fill: parent
HRoomAvatar {
id: avatar
displayName: chat.roomInfo.display_name
mxc: chat.roomInfo.avatar_url
Layout.alignment: Qt.AlignTop
}
HLabel {
id: nameLabel
text: chat.roomInfo.display_name || qsTr("Empty room")
font.pixelSize: theme.fontSize.big
color: theme.chat.roomHeader.name
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
leftPadding: theme.spacing
rightPadding: leftPadding
Layout.preferredWidth: Math.min(
implicitWidth, row.width - row.spacing - avatar.width
)
Layout.fillHeight: true
HoverHandler { id: nameHover }
}
HRichLabel {
id: topicLabel
text: chat.roomInfo.topic
textFormat: Text.StyledText
font.pixelSize: theme.fontSize.small
color: theme.chat.roomHeader.topic
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
rightPadding: nameLabel.rightPadding
Layout.fillWidth: true
Layout.fillHeight: true
HoverHandler { id: topicHover }
}
HToolTip {
visible: text && (nameHover.hovered || topicHover.hovered)
label.textFormat: Text.StyledText
text: name && topic ? (`${name}<br>${topic}`) : (name || topic)
readonly property string name:
nameLabel.truncated ?
(`<b>${chat.roomInfo.display_name}</b>`) : ""
readonly property string topic:
topicLabel.truncated ? chat.roomInfo.topic : ""
}
}
}

View File

@@ -0,0 +1,39 @@
import QtQuick 2.12
import "../../../Base"
HTileDelegate {
id: memberDelegate
backgroundColor: theme.chat.roomPane.member.background
contentOpacity:
model.invited ? theme.chat.roomPane.member.invitedOpacity : 1
image: HUserAvatar {
userId: model.user_id
displayName: model.display_name
mxc: model.avatar_url
powerLevel: model.power_level
shiftMembershipIconPosition: ! roomPane.collapsed
invited: model.invited
}
title.text: model.display_name || model.user_id
title.color:
memberDelegate.hovered ?
utils.nameColor(model.display_name || model.user_id.substring(1)) :
theme.chat.roomPane.member.name
subtitle.text: model.display_name ? model.user_id : ""
subtitle.color: theme.chat.roomPane.member.subtitle
contextMenu: HMenu {
HMenuItem {
icon.name: "copy-user-id"
text: qsTr("Copy user ID")
onTriggered: Clipboard.text = model.user_id
}
}
Behavior on title.color { HColorAnimation {} }
Behavior on contentOpacity { HNumberAnimation {} }
}

View File

@@ -0,0 +1,95 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../../Base"
HColumnLayout {
HListView {
id: memberList
clip: true
Layout.fillWidth: true
Layout.fillHeight: true
readonly property var originSource:
modelSources[["Member", chat.userId, chat.roomId]] || []
onOriginSourceChanged: filterLimiter.restart()
function filterSource() {
model.source =
utils.filterModelSource(originSource, filterField.text)
}
model: HListModel {
keyField: "user_id"
source: memberList.originSource
}
delegate: MemberDelegate {
width: memberList.width
}
Timer {
id: filterLimiter
interval: 16
onTriggered: memberList.filterSource()
}
}
HRowLayout {
Layout.minimumHeight: theme.baseElementsHeight
Layout.maximumHeight: Layout.minimumHeight
HTextField {
id: filterField
saveName: "memberFilterField"
saveId: chat.roomId
placeholderText: qsTr("Filter members")
backgroundColor: theme.chat.roomPane.filterMembers.background
bordered: false
opacity: width >= 16 * theme.uiScale ? 1 : 0
onTextChanged: filterLimiter.restart()
Layout.fillWidth: true
Layout.fillHeight: true
Behavior on opacity { HNumberAnimation {} }
}
HButton {
id: inviteButton
icon.name: "room-send-invite"
backgroundColor: theme.chat.roomPane.inviteButton.background
enabled: chat.roomInfo.can_invite
toolTip.text:
enabled ?
qsTr("Invite members to this room") :
qsTr("No permission to invite members in this room")
topPadding: 0 // XXX
bottomPadding: 0
onClicked: utils.makePopup(
"Popups/InviteToRoomPopup.qml",
chat,
{
userId: chat.userId,
roomId: chat.roomId,
roomName: chat.roomInfo.display_name,
invitingAllowed: Qt.binding(() => inviteButton.enabled),
},
)
// onEnabledChanged: if (openedPopup && ! enabled)
Layout.fillHeight: true
}
}
}

View File

@@ -0,0 +1,79 @@
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import "../../../Base"
HDrawer {
id: roomPane
saveName: "roomPane"
edge: Qt.RightEdge
defaultSize: buttonRepeater.childrenImplicitWidth
minimumSize:
buttonRepeater.count > 0 ? buttonRepeater.itemAt(0).implicitWidth : 0
background: HColumnLayout{
Rectangle {
color: theme.chat.roomPaneButtons.background
Layout.fillWidth: true
Layout.preferredHeight: theme.baseElementsHeight
}
Rectangle {
color: theme.chat.roomPane.background
Layout.fillWidth: true
Layout.fillHeight: true
}
}
HColumnLayout {
anchors.fill: parent
HFlow {
populate: null
Layout.fillWidth: true
HRepeater {
id: buttonRepeater
model: [
"members", "files", "notifications", "history", "settings"
]
HButton {
height: theme.baseElementsHeight
backgroundColor: "transparent"
icon.name: "room-view-" + modelData
toolTip.text: qsTr(
modelData.charAt(0).toUpperCase() + modelData.slice(1)
)
autoExclusive: true
checked: swipeView.currentIndex === 0 && index === 0 ||
swipeView.currentIndex === 1 && index === 4
enabled: ["members", "settings"].includes(modelData)
onClicked: swipeView.currentIndex = Math.min(index, 1)
}
}
}
HSwipeView {
id: swipeView
clip: true
interactive: ! roomPane.collapsed
saveName: "roomPaneView"
saveId: chat.roomId
Layout.fillWidth: true
Layout.fillHeight: true
MemberView {}
SettingsView { fillAvailableHeight: true }
}
}
}

View File

@@ -0,0 +1,132 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../../Base"
HBox {
color: "transparent"
buttonModel: [
{
name: "apply",
text: qsTr("Save"),
iconName: "apply",
// enabled: anyChange, TODO
enabled: false,
loading: saveFuture !== null,
disableWhileLoading: false,
},
{
name: "cancel",
text: qsTr("Cancel"),
iconName: "cancel",
enabled: anyChange || saveFuture !== null,
},
]
buttonCallbacks: ({
apply: button => {
if (saveFuture) saveFuture.cancel()
// TODO
},
cancel: button => {
if (saveFuture) {
saveFuture.cancel()
saveFuture = null
}
nameField.reset()
topicField.reset()
encryptCheckBox.reset()
requireInviteCheckbox.reset()
forbidGuestsCheckBox.reset()
},
})
property var saveFuture: null
readonly property bool anyChange:
nameField.changed || topicField.changed || encryptCheckBox.changed ||
requireInviteCheckbox.changed || forbidGuestsCheckBox.changed
HRoomAvatar {
id: avatar
displayName: chat.roomInfo.display_name
mxc: chat.roomInfo.avatar_url
// enabled: chat.roomInfo.can_set_avatar # put this in "change avatar"
Layout.fillWidth: true
Layout.preferredHeight: width
Layout.maximumWidth: 256 * theme.uiScale
}
HTextField {
id: nameField
placeholderText: qsTr("Room name")
maximumLength: 255
defaultText: chat.roomInfo.given_name
enabled: chat.roomInfo.can_set_name
Layout.fillWidth: true
}
HScrollableTextArea {
id: topicField
placeholderText: qsTr("Room topic")
defaultText: chat.roomInfo.plain_topic
enabled: chat.roomInfo.can_set_topic
Layout.fillWidth: true
}
HCheckBox {
id: encryptCheckBox
text: qsTr("Encrypt messages")
subtitle.text:
qsTr("Only you and those you trust will be able to read the " +
"conversation") +
`<br><font color="${theme.colors.middleBackground}">` +
(
chat.roomInfo.encrypted ?
qsTr("Cannot be disabled") :
qsTr("Cannot be disabled later!")
) +
"</font>"
subtitle.textFormat: Text.StyledText
defaultChecked: chat.roomInfo.encrypted
enabled: chat.roomInfo.can_set_encryption && ! chat.roomInfo.encrypted
Layout.fillWidth: true
}
HCheckBox {
id: requireInviteCheckbox
text: qsTr("Require being invited")
subtitle.text: qsTr("Users will need an invite to join the room")
defaultChecked: chat.roomInfo.invite_required
enabled: chat.roomInfo.can_set_join_rules
Layout.fillWidth: true
}
HCheckBox {
id: forbidGuestsCheckBox
text: qsTr("Forbid guests")
subtitle.text: qsTr("Users without an account won't be able to join")
defaultChecked: ! chat.roomInfo.guests_allowed
enabled: chat.roomInfo.can_set_guest_access
Layout.fillWidth: true
}
// HCheckBox { TODO
// text: qsTr("Make this room visible in the public room directory")
// checked: chat.roomInfo.published_in_directory
// Layout.fillWidth: true
// }
HSpacer {}
}

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

View File

@@ -0,0 +1,50 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
Rectangle {
id: typingMembersBar
property alias label: typingLabel
color: theme.chat.typingMembers.background
implicitHeight: typingLabel.text ? rowLayout.height : 0
opacity: implicitHeight ? 1 : 0
Behavior on implicitHeight { HNumberAnimation {} }
HRowLayout {
id: rowLayout
spacing: theme.spacing
HIcon {
id: icon
svgName: "typing" // TODO: animate
Layout.fillHeight: true
Layout.leftMargin: rowLayout.spacing / 2
}
HLabel {
id: typingLabel
textFormat: Text.StyledText
elide: Text.ElideRight
text: {
let tm = chat.roomInfo.typing_members
if (tm.length === 0) return ""
if (tm.length === 1) return qsTr("%1 is typing...").arg(tm[0])
return qsTr("%1 and %2 are typing...")
.arg(tm.slice(0, -1).join(", ")).arg(tm.slice(-1)[0])
}
Layout.fillWidth: true
Layout.fillHeight: true
Layout.topMargin: rowLayout.spacing / 4
Layout.bottomMargin: rowLayout.spacing / 4
Layout.leftMargin: rowLayout.spacing / 2
Layout.rightMargin: rowLayout.spacing / 2
}
}
}