mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-22 11:00:48 +03:00
460 lines
15 KiB
QML
460 lines
15 KiB
QML
// SPDX-FileCopyrightText: Nheko Contributors
|
|
//
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
import "./components"
|
|
import "./emoji"
|
|
import "./ui"
|
|
import "./voip"
|
|
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
import QtQuick.Window
|
|
import im.nheko
|
|
|
|
Item {
|
|
id: timelineView
|
|
|
|
required property PrivacyScreen privacyScreen
|
|
property var room: null
|
|
property var roomPreview: null
|
|
property bool shouldEffectsRun: false
|
|
property bool showBackButton: false
|
|
|
|
clip: true
|
|
|
|
// focus message input on key press, but not on Ctrl-C and such.
|
|
Keys.onPressed: event => {
|
|
if (event.text && event.key !== Qt.Key_Enter && event.key !== Qt.Key_Return && !topBar.searchHasFocus) {
|
|
TimelineManager.focusMessageInput();
|
|
if (event.modifiers != Qt.ControlModifier) {
|
|
room.input.setText(room.input.text + event.text);
|
|
}
|
|
}
|
|
}
|
|
onRoomChanged: if (room != null)
|
|
room.triggerSpecialEffects()
|
|
|
|
StickerPicker {
|
|
id: emojiPopup
|
|
|
|
emoji: true
|
|
}
|
|
Shortcut {
|
|
sequences: [StandardKey.Close]
|
|
|
|
onActivated: Rooms.resetCurrentRoom()
|
|
}
|
|
Label {
|
|
anchors.centerIn: parent
|
|
font.pointSize: 24
|
|
text: qsTr("No room open")
|
|
color: palette.text
|
|
visible: !room && !TimelineManager.isInitialSync && (!roomPreview || !roomPreview.roomid)
|
|
}
|
|
Spinner {
|
|
anchors.centerIn: parent
|
|
foreground: palette.mid
|
|
// height is somewhat arbitrary here... don't set width because width scales w/ height
|
|
height: parent.height / 16
|
|
opacity: hh.hovered ? 0.3 : 1
|
|
running: TimelineManager.isInitialSync
|
|
visible: TimelineManager.isInitialSync
|
|
z: 3
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: 100
|
|
}
|
|
}
|
|
|
|
HoverHandler {
|
|
id: hh
|
|
|
|
}
|
|
}
|
|
ColumnLayout {
|
|
id: timelineLayout
|
|
|
|
anchors.fill: parent
|
|
enabled: visible
|
|
spacing: 0
|
|
visible: room != null && !room.isSpace
|
|
|
|
TopBar {
|
|
id: topBar
|
|
|
|
showBackButton: timelineView.showBackButton
|
|
}
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
color: Nheko.theme.separator
|
|
implicitHeight: 1
|
|
z: 3
|
|
}
|
|
Rectangle {
|
|
id: msgView
|
|
|
|
Layout.fillHeight: true
|
|
Layout.fillWidth: true
|
|
color: palette.base
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: 0
|
|
|
|
StackLayout {
|
|
id: stackLayout
|
|
|
|
currentIndex: 0
|
|
|
|
Connections {
|
|
function onRoomChanged() {
|
|
stackLayout.currentIndex = 0;
|
|
}
|
|
|
|
target: timelineView
|
|
}
|
|
MessageView {
|
|
Layout.fillWidth: true
|
|
implicitHeight: msgView.height - typingIndicator.height
|
|
searchString: topBar.searchString
|
|
}
|
|
Loader {
|
|
source: CallManager.isOnCall && CallManager.callType != Voip.VOICE ? (Qt.platform.os != "windows" ? "voip/VideoCall.qml" : "voip/VideoCallD3D11.qml") : ""
|
|
|
|
onLoaded: TimelineManager.setVideoCallItem()
|
|
}
|
|
}
|
|
TypingIndicator {
|
|
id: typingIndicator
|
|
|
|
}
|
|
}
|
|
}
|
|
CallInviteBar {
|
|
id: callInviteBar
|
|
|
|
Layout.fillWidth: true
|
|
z: 3
|
|
}
|
|
ActiveCallBar {
|
|
Layout.fillWidth: true
|
|
z: 3
|
|
}
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
color: Nheko.theme.separator
|
|
implicitHeight: 1
|
|
z: 3
|
|
}
|
|
UploadBox {
|
|
}
|
|
Repeater {
|
|
model: room ? room.input.mentions : null
|
|
|
|
MessageInputWarning {
|
|
required property string modelData
|
|
bubbleColor: modelData == "@room" ? Nheko.theme.error : Nheko.theme.orange
|
|
text: modelData == "@room" ? qsTr("You are about to notify the whole room") : qsTr("You will be mentioning %1").arg(modelData)
|
|
showRemove: true
|
|
onRemoveClicked: room.input.removeMention(modelData);
|
|
}
|
|
}
|
|
MessageInputWarning {
|
|
text: qsTr("The command /%1 is not recognized and will be sent as part of your message").arg(room ? room.input.currentCommand : "")
|
|
visible: room ? room.input.containsInvalidCommand && !room.input.containsIncompleteCommand : false
|
|
}
|
|
MessageInputWarning {
|
|
bubbleColor: Nheko.theme.orange
|
|
text: qsTr("/%1 looks like an incomplete command. To send it anyway, add a space to the end of your message.").arg(room ? room.input.currentCommand : "")
|
|
visible: room ? room.input.containsIncompleteCommand : false
|
|
}
|
|
ReplyPopup {
|
|
}
|
|
MessageInput {
|
|
}
|
|
}
|
|
ColumnLayout {
|
|
id: preview
|
|
|
|
property string avatarUrl: room ? room.roomAvatarUrl : (roomPreview ? roomPreview.roomAvatarUrl : "")
|
|
property string reason: roomPreview ? roomPreview.reason : ""
|
|
property string roomId: room ? room.roomId : (roomPreview ? roomPreview.roomid : "")
|
|
property string roomName: room ? room.roomName : (roomPreview ? roomPreview.roomName : "")
|
|
property string roomTopic: room ? room.roomTopic : (roomPreview ? roomPreview.roomTopic : "")
|
|
|
|
anchors.fill: parent
|
|
anchors.margins: Nheko.paddingLarge
|
|
enabled: visible
|
|
spacing: Nheko.paddingLarge
|
|
visible: room != null && room.isSpace || roomPreview != null
|
|
|
|
Item {
|
|
Layout.fillHeight: true
|
|
}
|
|
Avatar {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
displayName: parent.roomName
|
|
enabled: false
|
|
implicitHeight: 130
|
|
roomid: parent.roomId
|
|
url: parent.avatarUrl.replace("mxc://", "image://MxcImage/")
|
|
implicitWidth: 130
|
|
}
|
|
RowLayout {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
spacing: Nheko.paddingMedium
|
|
Layout.fillWidth: true
|
|
|
|
MatrixText {
|
|
Layout.preferredWidth: implicitWidth
|
|
horizontalAlignment: TextEdit.AlignRight
|
|
font.pixelSize: 24
|
|
text: (!room && !(roomPreview?.isFetched ?? false)) ? qsTr("No preview available") : preview.roomName
|
|
}
|
|
ImageButton {
|
|
ToolTip.text: qsTr("Settings")
|
|
ToolTip.visible: hovered
|
|
hoverEnabled: true
|
|
image: ":/icons/icons/ui/settings.svg"
|
|
visible: !!room
|
|
|
|
onClicked: TimelineManager.openRoomSettings(room.roomId)
|
|
}
|
|
}
|
|
RowLayout {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
spacing: Nheko.paddingMedium
|
|
visible: !!room
|
|
Layout.fillWidth: true
|
|
|
|
MatrixText {
|
|
Layout.preferredWidth: implicitWidth
|
|
text: qsTr("%n member(s)", "", room ? room.roomMemberCount : 0)
|
|
}
|
|
ImageButton {
|
|
ToolTip.text: qsTr("View members of %1").arg(room ? room.roomName : "")
|
|
ToolTip.visible: hovered
|
|
hoverEnabled: true
|
|
image: ":/icons/icons/ui/people.svg"
|
|
|
|
onClicked: TimelineManager.openRoomMembers(room)
|
|
}
|
|
}
|
|
ScrollView {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: Nheko.paddingLarge
|
|
Layout.rightMargin: Nheko.paddingLarge
|
|
Layout.maximumHeight: timelineView.height / 3
|
|
|
|
TextArea {
|
|
background: null
|
|
horizontalAlignment: TextEdit.AlignHCenter
|
|
readOnly: true
|
|
selectByMouse: true
|
|
text: (room || (roomPreview?.isFetched ?? false)) ? TimelineManager.escapeEmoji(preview.roomTopic) : qsTr("This room is possibly inaccessible. If this room is private, you should remove it from this community.")
|
|
textFormat: TextEdit.RichText
|
|
wrapMode: TextEdit.WordWrap
|
|
|
|
onLinkActivated: Nheko.openLink(link)
|
|
|
|
NhekoCursorShape {
|
|
anchors.fill: parent
|
|
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
}
|
|
}
|
|
}
|
|
FlatButton {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: qsTr("join the conversation")
|
|
visible: roomPreview && !roomPreview.isInvite
|
|
|
|
onClicked: Rooms.joinPreview(roomPreview.roomid)
|
|
}
|
|
FlatButton {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: qsTr("accept invite")
|
|
visible: roomPreview && roomPreview.isInvite
|
|
|
|
onClicked: Rooms.acceptInvite(roomPreview.roomid)
|
|
}
|
|
FlatButton {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: qsTr("decline invite")
|
|
visible: roomPreview && roomPreview.isInvite
|
|
|
|
onClicked: Rooms.declineInvite(roomPreview.roomid)
|
|
}
|
|
FlatButton {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: qsTr("decline invite and ignore user")
|
|
visible: roomPreview && roomPreview.isInvite
|
|
|
|
onClicked: {
|
|
var inviter = TimelineManager.getGlobalUserProfile(roomPreview.inviterUserId)
|
|
inviter.ignored = true
|
|
}
|
|
}
|
|
FlatButton {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: qsTr("leave")
|
|
visible: !!room
|
|
|
|
onClicked: TimelineManager.openLeaveRoomDialog(room.roomId)
|
|
}
|
|
RowLayout {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
Layout.fillWidth: true
|
|
spacing: Nheko.paddingMedium
|
|
visible: roomPreview && roomPreview.isInvite && reasonField.showReason
|
|
|
|
MatrixText {
|
|
text: qsTr("Invited by %1 (%2)").arg(TimelineManager.escapeEmoji(inviterAvatar.displayName)).arg(TimelineManager.escapeEmoji(TimelineManager.htmlEscape(inviterAvatar.userid)))
|
|
}
|
|
Avatar {
|
|
id: inviterAvatar
|
|
|
|
Layout.alignment: Qt.AlignHCenter
|
|
displayName: roomPreview?.inviterDisplayName ?? ""
|
|
enabled: true
|
|
implicitHeight: 48
|
|
roomid: preview.roomId
|
|
url: (roomPreview?.inviterAvatarUrl ?? "").replace("mxc://", "image://MxcImage/")
|
|
userid: roomPreview?.inviterUserId ?? ""
|
|
implicitWidth: 48
|
|
|
|
onClicked: TimelineManager.openGlobalUserProfile(roomPreview.inviterUserId)
|
|
}
|
|
}
|
|
ScrollView {
|
|
id: reasonField
|
|
|
|
property bool showReason: false
|
|
|
|
Layout.alignment: Qt.AlignHCenter
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: Nheko.paddingLarge
|
|
Layout.rightMargin: Nheko.paddingLarge
|
|
visible: preview.reason !== "" && showReason
|
|
|
|
TextArea {
|
|
background: null
|
|
horizontalAlignment: TextEdit.AlignHCenter
|
|
readOnly: true
|
|
selectByMouse: true
|
|
text: TimelineManager.escapeEmoji(preview.reason)
|
|
textFormat: TextEdit.RichText
|
|
wrapMode: TextEdit.WordWrap
|
|
}
|
|
}
|
|
Button {
|
|
id: showReasonButton
|
|
|
|
Layout.alignment: Qt.AlignHCenter
|
|
//Layout.fillWidth: true
|
|
Layout.leftMargin: Nheko.paddingLarge
|
|
Layout.rightMargin: Nheko.paddingLarge
|
|
text: reasonField.showReason ? qsTr("Hide invite reason") : qsTr("Show invite reason")
|
|
visible: roomPreview && roomPreview.isInvite
|
|
|
|
onClicked: {
|
|
reasonField.showReason = !reasonField.showReason;
|
|
}
|
|
}
|
|
Item {
|
|
Layout.preferredHeight: Math.ceil(fontMetrics.lineSpacing * 2)
|
|
visible: room != null
|
|
}
|
|
Item {
|
|
Layout.fillHeight: true
|
|
}
|
|
}
|
|
ImageButton {
|
|
id: backToRoomsButton
|
|
|
|
ToolTip.text: qsTr("Back to room list")
|
|
ToolTip.visible: hovered
|
|
anchors.left: parent.left
|
|
anchors.margins: Nheko.paddingMedium
|
|
anchors.top: parent.top
|
|
enabled: visible
|
|
height: Nheko.avatarSize
|
|
image: ":/icons/icons/ui/angle-arrow-left.svg"
|
|
visible: (room == null || room.isSpace) && showBackButton
|
|
width: Nheko.avatarSize
|
|
|
|
onClicked: Rooms.resetCurrentRoom()
|
|
}
|
|
TimelineEffects {
|
|
id: timelineEffects
|
|
|
|
anchors.fill: parent
|
|
shouldEffectsRun: timelineView.shouldEffectsRun
|
|
}
|
|
NhekoDropArea {
|
|
anchors.fill: parent
|
|
roomid: room ? room.roomId : ""
|
|
}
|
|
Timer {
|
|
id: effectsTimer
|
|
|
|
interval: timelineEffects.maxLifespan
|
|
repeat: false
|
|
running: false
|
|
|
|
onTriggered: {
|
|
timelineEffects.removeParticles()
|
|
shouldEffectsRun = false
|
|
}
|
|
}
|
|
Connections {
|
|
function onConfetti() {
|
|
if (!Settings.fancyEffects)
|
|
return;
|
|
shouldEffectsRun = true;
|
|
timelineEffects.pulseConfetti();
|
|
room.markSpecialEffectsDone();
|
|
}
|
|
function onConfettiDone() {
|
|
if (!Settings.fancyEffects)
|
|
return;
|
|
effectsTimer.restart();
|
|
}
|
|
function onOpenReadReceiptsDialog(rr) {
|
|
var dialog = readReceiptsDialog.createObject(timelineRoot, {
|
|
"readReceipts": rr,
|
|
"room": room
|
|
});
|
|
dialog.show();
|
|
timelineRoot.destroyOnClose(dialog);
|
|
}
|
|
function onRainfall() {
|
|
if (!Settings.fancyEffects)
|
|
return;
|
|
shouldEffectsRun = true;
|
|
timelineEffects.pulseRainfall();
|
|
room.markSpecialEffectsDone();
|
|
}
|
|
function onRainfallDone() {
|
|
if (!Settings.fancyEffects)
|
|
return;
|
|
effectsTimer.restart();
|
|
}
|
|
function onShowRawMessageDialog(rawMessage) {
|
|
var component = Qt.createComponent("qrc:/resources/qml/dialogs/RawMessageDialog.qml");
|
|
if (component.status == Component.Ready) {
|
|
var dialog = component.createObject(timelineRoot, {
|
|
"rawMessage": rawMessage
|
|
});
|
|
dialog.show();
|
|
timelineRoot.destroyOnClose(dialog);
|
|
} else {
|
|
console.error("Failed to create component: " + component.errorString());
|
|
}
|
|
}
|
|
|
|
target: room
|
|
}
|
|
}
|