Move Chat/ dir under Pages/
This commit is contained in:
93
src/gui/Pages/Chat/Banners/Banner.qml
Normal file
93
src/gui/Pages/Chat/Banners/Banner.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
51
src/gui/Pages/Chat/Banners/InviteBanner.qml
Normal file
51
src/gui/Pages/Chat/Banners/InviteBanner.qml
Normal 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
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
39
src/gui/Pages/Chat/Banners/LeftBanner.qml
Normal file
39
src/gui/Pages/Chat/Banners/LeftBanner.qml
Normal 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,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
24
src/gui/Pages/Chat/Banners/UnknownDevicesBanner.qml
Normal file
24
src/gui/Pages/Chat/Banners/UnknownDevicesBanner.qml
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
59
src/gui/Pages/Chat/Chat.qml
Normal file
59
src/gui/Pages/Chat/Chat.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
70
src/gui/Pages/Chat/ChatPage.qml
Normal file
70
src/gui/Pages/Chat/ChatPage.qml
Normal 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
|
||||
}
|
||||
|
||||
}
|
230
src/gui/Pages/Chat/Composer.qml
Normal file
230
src/gui/Pages/Chat/Composer.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
192
src/gui/Pages/Chat/FileTransfer/Transfer.qml
Normal file
192
src/gui/Pages/Chat/FileTransfer/Transfer.qml
Normal 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 {} }
|
||||
}
|
||||
}
|
33
src/gui/Pages/Chat/FileTransfer/TransferList.qml
Normal file
33
src/gui/Pages/Chat/FileTransfer/TransferList.qml
Normal 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 }
|
||||
}
|
||||
}
|
11
src/gui/Pages/Chat/LoadingRoomProgressBar.qml
Normal file
11
src/gui/Pages/Chat/LoadingRoomProgressBar.qml
Normal 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 {} }
|
||||
}
|
69
src/gui/Pages/Chat/RoomHeader.qml
Normal file
69
src/gui/Pages/Chat/RoomHeader.qml
Normal 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 : ""
|
||||
}
|
||||
}
|
||||
}
|
39
src/gui/Pages/Chat/RoomPane/MemberDelegate.qml
Normal file
39
src/gui/Pages/Chat/RoomPane/MemberDelegate.qml
Normal 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 {} }
|
||||
}
|
95
src/gui/Pages/Chat/RoomPane/MemberView.qml
Normal file
95
src/gui/Pages/Chat/RoomPane/MemberView.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
79
src/gui/Pages/Chat/RoomPane/RoomPane.qml
Normal file
79
src/gui/Pages/Chat/RoomPane/RoomPane.qml
Normal 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 }
|
||||
}
|
||||
}
|
||||
}
|
132
src/gui/Pages/Chat/RoomPane/SettingsView.qml
Normal file
132
src/gui/Pages/Chat/RoomPane/SettingsView.qml
Normal 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 {}
|
||||
}
|
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] : []
|
||||
}
|
50
src/gui/Pages/Chat/TypingMembersBar.qml
Normal file
50
src/gui/Pages/Chat/TypingMembersBar.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user