Move QML files to src/qml and don't use internal qml modules
This commit is contained in:
179
src/qml/Page/ImageEditorPage.qml
Normal file
179
src/qml/Page/ImageEditorPage.qml
Normal file
@@ -0,0 +1,179 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
import Qt.labs.platform 1.1 as Platform
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
import org.kde.kquickimageeditor 1.0 as KQuickImageEditor
|
||||
|
||||
Kirigami.Page {
|
||||
id: rootEditorView
|
||||
|
||||
property bool resizing: false;
|
||||
required property string imagePath
|
||||
|
||||
signal newPathChanged(string newPath);
|
||||
|
||||
title: i18n("Edit")
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
function crop() {
|
||||
const ratioX = editImage.paintedWidth / editImage.nativeWidth;
|
||||
const ratioY = editImage.paintedHeight / editImage.nativeHeight;
|
||||
rootEditorView.resizing = false
|
||||
imageDoc.crop(selectionTool.selectionX / ratioX, selectionTool.selectionY / ratioY, selectionTool.selectionWidth / ratioX, selectionTool.selectionHeight / ratioY);
|
||||
}
|
||||
|
||||
actions {
|
||||
left: Kirigami.Action {
|
||||
id: undoAction
|
||||
text: i18nc("@action:button Undo modification", "Undo")
|
||||
iconName: "edit-undo"
|
||||
onTriggered: imageDoc.undo();
|
||||
visible: imageDoc.edited
|
||||
}
|
||||
main: Kirigami.Action {
|
||||
id: okAction
|
||||
text: i18nc("@action:button Accept image modification", "Accept")
|
||||
iconName: "dialog-ok"
|
||||
onTriggered: {
|
||||
let newPath = Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + (new Date()).getTime() + "." + imagePath.split('.').pop();
|
||||
if (imageDoc.saveAs(newPath)) {;
|
||||
newPathChanged(newPath);
|
||||
} else {
|
||||
msg.type = Kirigami.MessageType.Error
|
||||
msg.text = i18n("Unable to save file. Check if you have the correct permission to edit the cache directory.")
|
||||
msg.visible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
KQuickImageEditor.ImageItem {
|
||||
id: editImage
|
||||
// Assigning this to the contentItem and setting the padding causes weird positioning issues
|
||||
anchors.fill: parent
|
||||
anchors.margins: Kirigami.Units.gridUnit
|
||||
fillMode: KQuickImageEditor.ImageItem.PreserveAspectFit
|
||||
image: imageDoc.image
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Undo
|
||||
onActivated: undoAction.trigger();
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequences: [StandardKey.Save, "Enter"]
|
||||
onActivated: saveAction.trigger();
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.SaveAs
|
||||
onActivated: saveAsAction.trigger();
|
||||
}
|
||||
|
||||
KQuickImageEditor.ImageDocument {
|
||||
id: imageDoc
|
||||
path: rootEditorView.imagePath
|
||||
}
|
||||
|
||||
KQuickImageEditor.SelectionTool {
|
||||
id: selectionTool
|
||||
visible: rootEditorView.resizing
|
||||
width: editImage.paintedWidth
|
||||
height: editImage.paintedHeight
|
||||
x: editImage.horizontalPadding
|
||||
y: editImage.verticalPadding
|
||||
KQuickImageEditor.CropBackground {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
insideX: selectionTool.selectionX
|
||||
insideY: selectionTool.selectionY
|
||||
insideWidth: selectionTool.selectionWidth
|
||||
insideHeight: selectionTool.selectionHeight
|
||||
}
|
||||
Connections {
|
||||
target: selectionTool.selectionArea
|
||||
function onDoubleClicked() {
|
||||
rootEditorView.crop()
|
||||
}
|
||||
}
|
||||
}
|
||||
onImageChanged: {
|
||||
selectionTool.selectionX = 0
|
||||
selectionTool.selectionY = 0
|
||||
selectionTool.selectionWidth = Qt.binding(() => selectionTool.width)
|
||||
selectionTool.selectionHeight = Qt.binding(() => selectionTool.height)
|
||||
}
|
||||
}
|
||||
|
||||
header: QQC2.ToolBar {
|
||||
contentItem: Kirigami.ActionToolBar {
|
||||
id: actionToolBar
|
||||
display: QQC2.Button.TextBesideIcon
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
iconName: rootEditorView.resizing ? "dialog-cancel" : "transform-crop"
|
||||
text: rootEditorView.resizing ? i18n("Cancel") : i18nc("@action:button Crop an image", "Crop");
|
||||
onTriggered: {
|
||||
resizeRectangle.width = editImage.paintedWidth
|
||||
resizeRectangle.height = editImage.paintedHeight
|
||||
resizeRectangle.x = editImage.horizontalPadding
|
||||
resizeRectangle.y = editImage.verticalPadding
|
||||
resizeRectangle.insideX = 100
|
||||
resizeRectangle.insideY = 100
|
||||
resizeRectangle.insideWidth = 100
|
||||
resizeRectangle.insideHeight = 100
|
||||
|
||||
rootEditorView.resizing = !rootEditorView.resizing;
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
iconName: "dialog-ok"
|
||||
visible: rootEditorView.resizing
|
||||
text: i18nc("@action:button Crop an image", "Crop");
|
||||
onTriggered: rootEditorView.crop();
|
||||
},
|
||||
Kirigami.Action {
|
||||
iconName: "object-rotate-left"
|
||||
text: i18nc("@action:button Rotate an image to the left", "Rotate left");
|
||||
onTriggered: imageDoc.rotate(-90);
|
||||
visible: !rootEditorView.resizing
|
||||
},
|
||||
Kirigami.Action {
|
||||
iconName: "object-rotate-right"
|
||||
text: i18nc("@action:button Rotate an image to the right", "Rotate right");
|
||||
onTriggered: imageDoc.rotate(90);
|
||||
visible: !rootEditorView.resizing
|
||||
},
|
||||
Kirigami.Action {
|
||||
iconName: "object-flip-vertical"
|
||||
text: i18nc("@action:button Mirror an image vertically", "Flip");
|
||||
onTriggered: imageDoc.mirror(false, true);
|
||||
visible: !rootEditorView.resizing
|
||||
},
|
||||
Kirigami.Action {
|
||||
iconName: "object-flip-horizontal"
|
||||
text: i18nc("@action:button Mirror an image horizontally", "Mirror");
|
||||
onTriggered: imageDoc.mirror(true, false);
|
||||
visible: !rootEditorView.resizing
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
footer: Kirigami.InlineMessage {
|
||||
id: msg
|
||||
type: Kirigami.MessageType.Error
|
||||
showCloseButton: true
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
131
src/qml/Page/InviteUserPage.qml
Normal file
131
src/qml/Page/InviteUserPage.qml
Normal file
@@ -0,0 +1,131 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
id: root
|
||||
|
||||
property var room
|
||||
|
||||
title: i18n("Invite a User")
|
||||
|
||||
actions {
|
||||
main: Kirigami.Action {
|
||||
icon.name: "dialog-close"
|
||||
text: i18nc("@action", "Cancel")
|
||||
onTriggered: applicationWindow().pageStack.layers.pop()
|
||||
}
|
||||
}
|
||||
header: RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
|
||||
Kirigami.SearchField {
|
||||
id: identifierField
|
||||
property bool isUserID: text.match(/@(.+):(.+)/g)
|
||||
Layout.fillWidth: true
|
||||
|
||||
placeholderText: i18n("Find a user...")
|
||||
onAccepted: userDictListModel.search()
|
||||
}
|
||||
|
||||
Button {
|
||||
visible: identifierField.isUserID
|
||||
|
||||
text: i18n("Add")
|
||||
highlighted: true
|
||||
|
||||
onClicked: {
|
||||
room.inviteToRoom(identifierField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
id: userDictListView
|
||||
|
||||
clip: true
|
||||
|
||||
model: UserDirectoryListModel {
|
||||
id: userDictListModel
|
||||
|
||||
connection: root.room.connection
|
||||
keyword: identifierField.text
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
|
||||
visible: userDictListView.count < 1
|
||||
|
||||
text: i18n("No users available")
|
||||
}
|
||||
|
||||
delegate: Kirigami.AbstractListItem {
|
||||
id: delegate
|
||||
property bool inRoom: room && room.containsUser(userID)
|
||||
|
||||
topPadding: Kirigami.Units.largeSpacing
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
|
||||
contentItem: RowLayout {
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: height
|
||||
Layout.fillHeight: true
|
||||
|
||||
source: avatar ? ("image://mxc/" + avatar) : ""
|
||||
name: name
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
level: 3
|
||||
|
||||
text: name
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
text: userID
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
visible: !inRoom
|
||||
icon.name: "document-send"
|
||||
text: i18n("Send invitation")
|
||||
|
||||
onClicked: {
|
||||
room.inviteToRoom(userID);
|
||||
applicationWindow().pageStack.layers.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
161
src/qml/Page/JoinRoomPage.qml
Normal file
161
src/qml/Page/JoinRoomPage.qml
Normal file
@@ -0,0 +1,161 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
id: root
|
||||
property var connection
|
||||
|
||||
property alias keyword: identifierField.text
|
||||
property string server
|
||||
|
||||
title: i18n("Explore Rooms")
|
||||
|
||||
Component.onCompleted: identifierField.forceActiveFocus()
|
||||
|
||||
header: Control {
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
contentItem: RowLayout {
|
||||
Kirigami.SearchField {
|
||||
id: identifierField
|
||||
property bool isRoomAlias: text.match(/#(.+):(.+)/g)
|
||||
property var room: isRoomAlias ? connection.roomByAlias(text) : null
|
||||
property bool isJoined: room != null
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
placeholderText: i18n("Find a room...")
|
||||
}
|
||||
|
||||
Button {
|
||||
id: joinButton
|
||||
|
||||
visible: identifierField.isRoomAlias
|
||||
|
||||
text: identifierField.isJoined ? i18n("View") : i18n("Join")
|
||||
highlighted: true
|
||||
|
||||
onClicked: {
|
||||
if (!identifierField.isJoined) {
|
||||
Controller.joinRoom(identifierField.text);
|
||||
// When joining the room, the room will be opened
|
||||
}
|
||||
applicationWindow().pageStack.layers.pop();
|
||||
}
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
Layout.maximumWidth: 120
|
||||
|
||||
id: serverField
|
||||
|
||||
editable: currentIndex == 1
|
||||
|
||||
model: [i18n("Local"), i18n("Global"), "matrix.org"]
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
if (currentIndex == 0) {
|
||||
server = ""
|
||||
} else if (currentIndex == 2) {
|
||||
server = "matrix.org"
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
if (currentIndex == 1) {
|
||||
server = editText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: publicRoomsListView
|
||||
clip: true
|
||||
model: PublicRoomListModel {
|
||||
id: publicRoomListModel
|
||||
|
||||
connection: root.connection
|
||||
server: root.server
|
||||
keyword: root.keyword
|
||||
}
|
||||
|
||||
onContentYChanged: {
|
||||
if(publicRoomListModel.hasMore && contentHeight - contentY < publicRoomsListView.height + 200)
|
||||
publicRoomListModel.next();
|
||||
}
|
||||
delegate: Kirigami.AbstractListItem {
|
||||
property bool justJoined: false
|
||||
width: publicRoomsListView.width
|
||||
onClicked: {
|
||||
if (!isJoined) {
|
||||
Controller.joinRoom(roomID)
|
||||
justJoined = true;
|
||||
} else {
|
||||
RoomManager.enterRoom(connection.room(roomID))
|
||||
}
|
||||
applicationWindow().pageStack.layers.pop();
|
||||
}
|
||||
contentItem: RowLayout {
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
source: model.avatar ? ("image://mxc/" + model.avatar) : ""
|
||||
name: name
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
level: 4
|
||||
text: name
|
||||
font.bold: true
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
Label {
|
||||
visible: isJoined || justJoined
|
||||
text: i18n("Joined")
|
||||
color: Kirigami.Theme.linkColor
|
||||
}
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
visible: text
|
||||
text: topic ? topic.replace(/(\r\n\t|\n|\r\t)/gm," ") : ""
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Icon {
|
||||
source: "user"
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
implicitHeight: Kirigami.Units.iconSizes.small
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
}
|
||||
Label {
|
||||
text: memberCount + " " + (alias ?? roomID)
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/qml/Page/LoadingPage.qml
Normal file
13
src/qml/Page/LoadingPage.qml
Normal file
@@ -0,0 +1,13 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Controls 2.12 as QQC2
|
||||
import org.kde.kirigami 2.19 as Kirigami
|
||||
|
||||
Kirigami.Page {
|
||||
Kirigami.LoadingPlaceholder {
|
||||
id: loadingIndicator
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
385
src/qml/Page/RoomListPage.qml
Normal file
385
src/qml/Page/RoomListPage.qml
Normal file
@@ -0,0 +1,385 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQml.Models 2.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
import org.kde.kitemmodels 1.0
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
|
||||
header: ColumnLayout {
|
||||
visible: !page.collapsedMode
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 0
|
||||
|
||||
ListView {
|
||||
id: spaceList
|
||||
property string activeSpaceId: ''
|
||||
|
||||
orientation: Qt.Horizontal
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
clip:true
|
||||
visible: spaceList.count > 0
|
||||
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
|
||||
Layout.fillWidth: true
|
||||
|
||||
model: SortFilterSpaceListModel {
|
||||
id: sortFilterSpaceListModel
|
||||
sourceModel: RoomListModel {
|
||||
id: spaceListModel
|
||||
connection: Controller.activeConnection
|
||||
}
|
||||
}
|
||||
|
||||
header: QQC2.Control {
|
||||
contentItem: QQC2.RoundButton {
|
||||
id: homeButton
|
||||
flat: true
|
||||
padding: Kirigami.Units.gridUnit / 2
|
||||
icon.name: "home"
|
||||
text: i18nc("@action:button", "Show All Rooms")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: {
|
||||
sortFilterRoomListModel.activeSpaceId = "";
|
||||
spaceList.activeSpaceId = '';
|
||||
listView.positionViewAtIndex(0, ListView.Beginning);
|
||||
}
|
||||
|
||||
QQC2.ToolTip {
|
||||
text: homeButton.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: QQC2.Control {
|
||||
required property string avatar
|
||||
required property var currentRoom
|
||||
required property int index
|
||||
required property string id
|
||||
implicitWidth: ListView.view.headerItem.implicitWidth
|
||||
implicitHeight: ListView.view.headerItem.implicitHeight
|
||||
|
||||
contentItem: Kirigami.Avatar {
|
||||
actions.main: Kirigami.Action {
|
||||
onTriggered: {
|
||||
spaceList.activeSpaceId = id;
|
||||
sortFilterRoomListModel.activeSpaceId = id;
|
||||
}
|
||||
}
|
||||
|
||||
name: currentRoom.displayName
|
||||
|
||||
QQC2.ToolTip {
|
||||
text: currentRoom.displayName
|
||||
}
|
||||
|
||||
source: avatar !== "" ? "image://mxc/" + avatar : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
id: page
|
||||
|
||||
title: i18n("Rooms")
|
||||
|
||||
property var enteredRoom
|
||||
property bool collapsedMode: Config.roomListPageWidth === applicationWindow().collapsedPageWidth && applicationWindow().shouldUseSidebars
|
||||
|
||||
verticalScrollBarPolicy: collapsedMode ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded
|
||||
|
||||
onCollapsedModeChanged: if (collapsedMode) {
|
||||
sortFilterRoomListModel.filterText = "";
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: RoomManager
|
||||
function onCurrentRoomChanged() {
|
||||
itemSelection.setCurrentIndex(roomListModel.index(roomListModel.indexForRoom(RoomManager.currentRoom), 0), ItemSelectionModel.SelectCurrent)
|
||||
}
|
||||
}
|
||||
|
||||
function goToNextRoom() {
|
||||
do {
|
||||
listView.incrementCurrentIndex();
|
||||
} while (!listView.currentItem.visible && listView.currentIndex === listView.count)
|
||||
listView.currentItem.action.trigger();
|
||||
}
|
||||
|
||||
function goToPreviousRoom() {
|
||||
do {
|
||||
listView.decrementCurrentIndex();
|
||||
} while (!listView.currentItem.visible && listView.currentIndex !== 0)
|
||||
listView.currentItem.action.trigger();
|
||||
}
|
||||
|
||||
titleDelegate: collapsedMode ? empty : searchField
|
||||
|
||||
Component {
|
||||
id: empty
|
||||
Item {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: searchField
|
||||
Kirigami.SearchField {
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
onTextChanged: sortFilterRoomListModel.filterText = text
|
||||
KeyNavigation.tab: listView
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
|
||||
activeFocusOnTab: true
|
||||
clip: accountList.count > 1
|
||||
|
||||
header: QQC2.ItemDelegate {
|
||||
visible: page.collapsedMode
|
||||
action: Kirigami.Action {
|
||||
id: enterRoomAction
|
||||
onTriggered: quickView.item.open();
|
||||
}
|
||||
topPadding: Kirigami.Units.largeSpacing
|
||||
leftPadding: Kirigami.Units.largeSpacing
|
||||
rightPadding: Kirigami.Units.largeSpacing
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
width: visible ? page.width : 0
|
||||
height: visible ? Kirigami.Units.gridUnit * 2 : 0
|
||||
|
||||
Kirigami.Icon {
|
||||
anchors.centerIn: parent
|
||||
width: 22
|
||||
height: 22
|
||||
source: "search"
|
||||
}
|
||||
Kirigami.Separator {
|
||||
width: parent.width
|
||||
anchors.bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - (Kirigami.Units.largeSpacing * 4)
|
||||
visible: listView.count == 0
|
||||
text: sortFilterRoomListModel.filterText.length > 0 ? i18n("No rooms found") : i18n("Join some rooms to get started")
|
||||
helpfulAction: Kirigami.Action {
|
||||
icon.name: sortFilterRoomListModel.filterText.length > 0 ? "search" : "list-add"
|
||||
text: sortFilterRoomListModel.filterText.length > 0 ? i18n("Search in room directory") : i18n("Explore rooms")
|
||||
onTriggered: pageStack.layers.push("qrc:/JoinRoomPage.qml", {
|
||||
connection: Controller.activeConnection,
|
||||
keyword: sortFilterRoomListModel.filterText
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ItemSelectionModel {
|
||||
id: itemSelection
|
||||
model: roomListModel
|
||||
onCurrentChanged: {
|
||||
listView.currentIndex = sortFilterRoomListModel.mapFromSource(current).row
|
||||
}
|
||||
}
|
||||
|
||||
model: SortFilterRoomListModel {
|
||||
id: sortFilterRoomListModel
|
||||
sourceModel: RoomListModel {
|
||||
id: roomListModel
|
||||
connection: Controller.activeConnection
|
||||
}
|
||||
roomSortOrder: Config.mergeRoomList ? SortFilterRoomListModel.LastActivity : SortFilterRoomListModel.Categories
|
||||
onLayoutChanged: {
|
||||
listView.currentIndex = sortFilterRoomListModel.mapFromSource(itemSelection.currentIndex).row
|
||||
}
|
||||
}
|
||||
|
||||
section.property: sortFilterRoomListModel.filterText.length === 0 && !Config.mergeRoomList ? "category" : null
|
||||
section.delegate: Kirigami.ListSectionHeader {
|
||||
id: sectionHeader
|
||||
height: implicitHeight
|
||||
action: Kirigami.Action {
|
||||
onTriggered: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
|
||||
}
|
||||
contentItem: RowLayout {
|
||||
implicitHeight: categoryName.implicitHeight
|
||||
Kirigami.Heading {
|
||||
id: categoryName
|
||||
level: 3
|
||||
text: roomListModel.categoryName(section)
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
visible: !page.collapsedMode
|
||||
}
|
||||
Kirigami.Icon {
|
||||
source: page.collapsedMode ? roomListModel.categoryIconName(section) : (roomListModel.categoryVisible(section) ? "go-up" : "go-down")
|
||||
implicitHeight: Kirigami.Units.iconSizes.small
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reuseItems: true
|
||||
currentIndex: -1 // we don't want any room highlighted by default
|
||||
|
||||
delegate: page.collapsedMode ? collapsedModeListComponent : normalModeListComponent
|
||||
|
||||
Component {
|
||||
id: collapsedModeListComponent
|
||||
|
||||
QQC2.ItemDelegate {
|
||||
action: Kirigami.Action {
|
||||
id: enterRoomAction
|
||||
onTriggered: {
|
||||
RoomManager.enterRoom(currentRoom);
|
||||
}
|
||||
}
|
||||
Keys.onEnterPressed: enterRoomAction.trigger()
|
||||
Keys.onReturnPressed: enterRoomAction.trigger()
|
||||
topPadding: Kirigami.Units.largeSpacing
|
||||
leftPadding: Kirigami.Units.largeSpacing
|
||||
rightPadding: Kirigami.Units.largeSpacing
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
width: ListView.view.width
|
||||
height: ListView.view.width
|
||||
|
||||
contentItem: Kirigami.Avatar {
|
||||
source: avatar ? "image://mxc/" + avatar : ""
|
||||
name: model.name || i18n("No Name")
|
||||
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
}
|
||||
|
||||
QQC2.ToolTip {
|
||||
enabled: text.length !== 0
|
||||
text: name ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomListContextMenu
|
||||
RoomListContextMenu {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: normalModeListComponent
|
||||
Kirigami.BasicListItem {
|
||||
id: roomListItem
|
||||
visible: model.categoryVisible || sortFilterRoomListModel.filterText.length > 0 || Config.mergeRoomList
|
||||
topPadding: Kirigami.Units.largeSpacing
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
highlighted: listView.currentIndex === index
|
||||
focus: true
|
||||
icon: undefined
|
||||
action: Kirigami.Action {
|
||||
id: enterRoomAction
|
||||
onTriggered: {
|
||||
RoomManager.enterRoom(currentRoom);
|
||||
}
|
||||
}
|
||||
Keys.onEnterPressed: enterRoomAction.trigger()
|
||||
Keys.onReturnPressed: enterRoomAction.trigger()
|
||||
bold: unreadCount > 0
|
||||
label: name ?? ""
|
||||
labelItem.textFormat: Text.PlainText
|
||||
subtitle: subtitleText
|
||||
subtitleItem.textFormat: Text.PlainText
|
||||
onPressAndHold: {
|
||||
createRoomListContextMenu()
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
acceptedDevices: PointerDevice.Mouse
|
||||
onTapped: createRoomListContextMenu()
|
||||
}
|
||||
|
||||
leading: Kirigami.Avatar {
|
||||
source: avatar ? "image://mxc/" + avatar : ""
|
||||
name: model.name || i18n("No Name")
|
||||
implicitWidth: visible ? height : 0
|
||||
visible: Config.showAvatarInTimeline
|
||||
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
}
|
||||
|
||||
trailing: RowLayout {
|
||||
QQC2.Label {
|
||||
text: notificationCount > 0 ? notificationCount : "●"
|
||||
visible: unreadCount > 0
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
color: Kirigami.Theme.textColor
|
||||
Layout.minimumWidth: height
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
background: Rectangle {
|
||||
visible: notificationCount > 0
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Button
|
||||
color: highlightCount > 0 ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.disabledTextColor
|
||||
opacity: highlightCount > 0 ? 1 : 0.3
|
||||
radius: height / 2
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
id: configButton
|
||||
visible: roomListItem.hovered
|
||||
Accessible.name: i18n("Configure room")
|
||||
|
||||
action: Kirigami.Action {
|
||||
id: optionAction
|
||||
icon.name: "configure"
|
||||
onTriggered: {
|
||||
createRoomListContextMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createRoomListContextMenu() {
|
||||
const menu = roomListContextMenu.createObject(page, {room: currentRoom})
|
||||
configButton.visible = true
|
||||
configButton.down = true
|
||||
menu.closed.connect(function() {
|
||||
configButton.down = undefined
|
||||
configButton.visible = Qt.binding(function() { return roomListItem.hovered || Kirigami.Settings.isMobile })
|
||||
})
|
||||
menu.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer: QQC2.ToolBar {
|
||||
visible: AccountRegistry.accountCount > 1 && !collapsedMode
|
||||
height: visible ? implicitHeight : 0
|
||||
|
||||
contentItem: QQC2.ComboBox {
|
||||
id: accountList
|
||||
|
||||
model: AccountRegistry
|
||||
textRole: "userId"
|
||||
valueRole: "connection"
|
||||
onActivated: Controller.activeConnection = currentValue
|
||||
Component.onCompleted: currentIndex = indexOfValue(Controller.activeConnection)
|
||||
}
|
||||
}
|
||||
}
|
||||
647
src/qml/Page/RoomPage.qml
Normal file
647
src/qml/Page/RoomPage.qml
Normal file
@@ -0,0 +1,647 @@
|
||||
// SPDX-FileCopyrightText: 2018-2020 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
import Qt.labs.platform 1.1 as Platform
|
||||
import Qt.labs.qmlmodels 1.0
|
||||
|
||||
import org.kde.kirigami 2.19 as Kirigami
|
||||
import org.kde.kitemmodels 1.0
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
id: page
|
||||
|
||||
/// It's not readonly because of the seperate window view.
|
||||
property var currentRoom: RoomManager.currentRoom
|
||||
property bool loading: page.currentRoom === null || (messageListView.count === 0 && !page.currentRoom.allHistoryLoaded && !page.currentRoom.isInvite)
|
||||
/// Used to determine if scrolling to the bottom should mark the message as unread
|
||||
property bool hasScrolledUpBefore: false;
|
||||
|
||||
/// Disable cancel shortcut. Used by the seperate window since it provide its own
|
||||
/// cancel implementation.
|
||||
property bool disableCancelShortcut: false
|
||||
|
||||
title: currentRoom.displayName
|
||||
|
||||
KeyNavigation.left: pageStack.get(0)
|
||||
|
||||
Connections {
|
||||
target: RoomManager
|
||||
function onCurrentRoomChanged() {
|
||||
if(!RoomManager.currentRoom) {
|
||||
if(pageStack.lastItem == page) {
|
||||
pageStack.pop()
|
||||
}
|
||||
} else if (page.currentRoom.isInvite) {
|
||||
page.currentRoom.clearInvitationNotification();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signal switchRoomUp()
|
||||
signal switchRoomDown()
|
||||
|
||||
onCurrentRoomChanged: {
|
||||
hasScrolledUpBefore = false;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: messageEventModel
|
||||
function onRowsInserted() {
|
||||
markReadIfVisibleTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: markReadIfVisibleTimer
|
||||
interval: 1000
|
||||
onTriggered: {
|
||||
if (loading || !currentRoom.readMarkerLoaded || !applicationWindow().active) {
|
||||
restart()
|
||||
} else {
|
||||
markReadIfVisible()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ActionsHandler {
|
||||
id: actionsHandler
|
||||
room: page.currentRoom
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Cancel
|
||||
onActivated: applicationWindow().pageStack.get(0).forceActiveFocus()
|
||||
enabled: !page.disableCancelShortcut
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Controller.activeConnection
|
||||
function onJoinedRoom(room, invited) {
|
||||
if(page.currentRoom.id === invited.id) {
|
||||
RoomManager.enterRoom(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: currentRoom
|
||||
function onShowMessage(messageType, message) {
|
||||
page.header.contentItem.text = message;
|
||||
page.header.contentItem.type = messageType === ActionsHandler.Error ? Kirigami.MessageType.Error : messageType === ActionsHandler.Positive ? Kirigami.MessageType.Positive : Kirigami.MessageType.Information;
|
||||
page.header.visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
header: QQC2.Control {
|
||||
height: visible ? implicitHeight : 0
|
||||
visible: false
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
contentItem: Kirigami.InlineMessage {
|
||||
showCloseButton: true
|
||||
visible: true
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
id: invitation
|
||||
|
||||
visible: currentRoom && currentRoom.isInvite
|
||||
anchors.centerIn: parent
|
||||
text: i18n("Accept this invitation?")
|
||||
RowLayout {
|
||||
QQC2.Button {
|
||||
Layout.alignment : Qt.AlignHCenter
|
||||
text: i18n("Reject")
|
||||
|
||||
onClicked: RoomManager.leaveRoom(page.currentRoom);
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
Layout.alignment : Qt.AlignHCenter
|
||||
text: i18n("Accept")
|
||||
|
||||
onClicked: {
|
||||
currentRoom.acceptInvitation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.LoadingPlaceholder {
|
||||
id: loadingIndicator
|
||||
anchors.centerIn: parent
|
||||
visible: loading
|
||||
}
|
||||
|
||||
focus: true
|
||||
|
||||
Keys.onTabPressed: {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
switchRoomDown();
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onBacktabPressed: {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
switchRoomUp();
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: {
|
||||
if (event.key === Qt.Key_PageDown && (event.modifiers & Qt.ControlModifier)) {
|
||||
switchRoomDown();
|
||||
} else if (event.key === Qt.Key_PageUp && (event.modifiers & Qt.ControlModifier)) {
|
||||
switchRoomUp();
|
||||
} else if (!(event.modifiers & Qt.ControlModifier) && event.key < Qt.Key_Escape) {
|
||||
event.accepted = true;
|
||||
chatBox.addText(event.text);
|
||||
chatBox.focusInputField();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// hover actions on a delegate, activated in TimelineContainer.qml
|
||||
Connections {
|
||||
target: page.flickable
|
||||
enabled: hoverActions.visible
|
||||
function onContentYChanged() {
|
||||
hoverActions.updateFunction();
|
||||
}
|
||||
}
|
||||
|
||||
CollapseStateProxyModel {
|
||||
id: collapseStateProxyModel
|
||||
sourceModel: sortedMessageEventModel
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: messageListView
|
||||
visible: !invitation.visible
|
||||
|
||||
readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1
|
||||
readonly property bool isLoaded: page.width * page.height > 10
|
||||
|
||||
spacing: 0
|
||||
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
highlightMoveDuration: 500
|
||||
|
||||
model: !isLoaded ? undefined : collapseStateProxyModel
|
||||
|
||||
MessageEventModel {
|
||||
id: messageEventModel
|
||||
|
||||
room: currentRoom
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: messageListView.atYBeginning
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
if (messageListView.atYBeginning && messageEventModel.canFetchMore(messageEventModel.index(0, 0))) {
|
||||
messageEventModel.fetchMore(messageEventModel.index(0, 0));
|
||||
}
|
||||
}
|
||||
repeat: true
|
||||
}
|
||||
|
||||
// HACK: The view should do this automatically but doesn't.
|
||||
onAtYBeginningChanged: if (atYBeginning && messageEventModel.canFetchMore(messageEventModel.index(0, 0))) {
|
||||
messageEventModel.fetchMore(messageEventModel.index(0, 0));
|
||||
}
|
||||
|
||||
onAtYEndChanged: if (atYEnd && hasScrolledUpBefore) {
|
||||
if (QQC2.ApplicationWindow.window && (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden)) {
|
||||
currentRoom.markAllMessagesAsRead();
|
||||
}
|
||||
hasScrolledUpBefore = false;
|
||||
} else if (!atYEnd) {
|
||||
hasScrolledUpBefore = true;
|
||||
}
|
||||
|
||||
QQC2.Popup {
|
||||
anchors.centerIn: parent
|
||||
|
||||
id: attachDialog
|
||||
|
||||
padding: 16
|
||||
|
||||
contentItem: RowLayout {
|
||||
QQC2.ToolButton {
|
||||
Layout.preferredWidth: 160
|
||||
Layout.fillHeight: true
|
||||
|
||||
icon.name: 'mail-attachment'
|
||||
|
||||
text: i18n("Choose local file")
|
||||
|
||||
onClicked: {
|
||||
attachDialog.close()
|
||||
|
||||
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
|
||||
|
||||
fileDialog.chosen.connect(function(path) {
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
currentRoom.chatBoxAttachmentPath = path;
|
||||
})
|
||||
|
||||
fileDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {}
|
||||
|
||||
QQC2.ToolButton {
|
||||
Layout.preferredWidth: 160
|
||||
Layout.fillHeight: true
|
||||
|
||||
padding: 16
|
||||
|
||||
icon.name: 'insert-image'
|
||||
text: i18n("Clipboard image")
|
||||
onClicked: {
|
||||
const localPath = Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/screenshots/" + (new Date()).getTime() + ".png"
|
||||
if (!Clipboard.saveImage(localPath)) {
|
||||
return;
|
||||
}
|
||||
currentRoom.chatBoxAttachmentPath = localPath;
|
||||
attachDialog.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: openFileDialog
|
||||
|
||||
OpenFileDialog {}
|
||||
}
|
||||
|
||||
|
||||
MessageFilterModel {
|
||||
id: sortedMessageEventModel
|
||||
|
||||
sourceModel: messageEventModel
|
||||
}
|
||||
|
||||
delegate: EventDelegate {}
|
||||
|
||||
QQC2.RoundButton {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Kirigami.Units.largeSpacing
|
||||
anchors.rightMargin: Kirigami.Units.largeSpacing
|
||||
implicitWidth: Kirigami.Units.gridUnit * 2
|
||||
implicitHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
id: goReadMarkerFab
|
||||
|
||||
visible: currentRoom && currentRoom.hasUnreadMessages && currentRoom.readMarkerLoaded
|
||||
action: Kirigami.Action {
|
||||
onTriggered: {
|
||||
messageListView.goToEvent(currentRoom.readMarkerEventId)
|
||||
}
|
||||
icon.name: "go-up"
|
||||
}
|
||||
|
||||
QQC2.ToolTip {
|
||||
text: i18n("Jump to first unread message")
|
||||
}
|
||||
}
|
||||
QQC2.RoundButton {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Kirigami.Units.largeSpacing + messageListView.headerItem.height
|
||||
anchors.rightMargin: Kirigami.Units.largeSpacing
|
||||
implicitWidth: Kirigami.Units.gridUnit * 2
|
||||
implicitHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
id: goMarkAsReadFab
|
||||
|
||||
visible: !messageListView.atYEnd
|
||||
action: Kirigami.Action {
|
||||
onTriggered: {
|
||||
goToLastMessage();
|
||||
currentRoom.markAllMessagesAsRead();
|
||||
}
|
||||
icon.name: "go-down"
|
||||
}
|
||||
|
||||
QQC2.ToolTip {
|
||||
text: i18n("Jump to latest message")
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
positionViewAtBeginning();
|
||||
}
|
||||
|
||||
DropArea {
|
||||
id: dropAreaFile
|
||||
anchors.fill: parent
|
||||
onDropped: currentRoom.chatBoxAttachmentPath = drop.urls[0];
|
||||
}
|
||||
|
||||
QQC2.Pane {
|
||||
visible: dropAreaFile.containsDrag
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: Kirigami.Units.gridUnit
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - (Kirigami.Units.largeSpacing * 4)
|
||||
text: i18n("Drag items here to share them")
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: messageDelegateContextMenu
|
||||
|
||||
MessageDelegateContextMenu {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fileDelegateContextMenu
|
||||
|
||||
FileDelegateContextMenu {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fullScreenImage
|
||||
|
||||
FullScreenImage {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: userDetailDialog
|
||||
|
||||
UserDetailDialog {}
|
||||
}
|
||||
|
||||
header: TypingPane {
|
||||
id: typingPane
|
||||
visible: !loadingIndicator.visible && currentRoom && currentRoom.usersTyping.length > 0
|
||||
labelText: visible ? i18ncp(
|
||||
"Message displayed when some users are typing", "%2 is typing", "%2 are typing",
|
||||
currentRoom.usersTyping.length,
|
||||
currentRoom.usersTyping.map(user => user.displayName).join(", ")
|
||||
) : ""
|
||||
anchors.left: parent.left
|
||||
height: visible ? implicitHeight : 0
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
property: "height"
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
z: 2
|
||||
}
|
||||
headerPositioning: ListView.OverlayHeader
|
||||
|
||||
function goToEvent(eventID) {
|
||||
const index = eventToIndex(eventID)
|
||||
messageListView.positionViewAtIndex(index, ListView.Center)
|
||||
itemAtIndex(index).isTemporaryHighlighted = true
|
||||
}
|
||||
|
||||
Item {
|
||||
id: hoverActions
|
||||
property var event: null
|
||||
property bool userMsg: event && event.author.id === Controller.activeConnection.localUserId
|
||||
property bool showEdit: event && (userMsg && (event.eventType === "emote" || event.eventType === "message"))
|
||||
property var delegate: null
|
||||
property var bubble: null
|
||||
property var hovered: bubble && bubble.hovered
|
||||
property var visibleDelayed: (hovered || hoverHandler.hovered) && !Kirigami.Settings.isMobile
|
||||
onVisibleDelayedChanged: if (visibleDelayed) {
|
||||
visible = true;
|
||||
} else {
|
||||
// HACK: delay disapearing by 200ms, otherwise this can create some glitches
|
||||
// See https://invent.kde.org/network/neochat/-/issues/333
|
||||
hoverActionsTimer.restart();
|
||||
}
|
||||
Timer {
|
||||
id: hoverActionsTimer
|
||||
interval: 200
|
||||
onTriggered: hoverActions.visible = hoverActions.visibleDelayed;
|
||||
}
|
||||
|
||||
property int childOffset: userMsg && Config.showLocalMessagesOnRight && !Config.compactLayout ? (bubble ? bubble.width : 0) - childWidth : Math.max((bubble ? bubble.width : 0) - childWidth, 0)
|
||||
x: delegate && bubble ? (delegate.x + bubble.x + Kirigami.Units.largeSpacing + childOffset - (Config.compactLayout ? Kirigami.Units.gridUnit * 3 + (delegate.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 : 0 ): 0) - (userMsg && !Config.compactLayout ? Kirigami.Units.gridUnit : 0)) : 0
|
||||
y: bubble ? bubble.mapToItem(parent, 0, 0).y - hoverActions.childHeight + Kirigami.Units.smallSpacing: 0;
|
||||
|
||||
visible: false
|
||||
|
||||
property var updateFunction
|
||||
|
||||
property alias childWidth: hoverActionsRow.width
|
||||
property alias childHeight: hoverActionsRow.height
|
||||
|
||||
RowLayout {
|
||||
id: hoverActionsRow
|
||||
z: 4
|
||||
spacing: 0
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
margin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
Kirigami.Icon {
|
||||
source: "security-high"
|
||||
width: height
|
||||
height: parent.height
|
||||
visible: hoverActions.event.verified
|
||||
HoverHandler {
|
||||
id: hover
|
||||
}
|
||||
QQC2.ToolTip.text: i18n("This message was sent from a verified device")
|
||||
QQC2.ToolTip.visible: hover.hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
QQC2.ToolTip.text: i18n("React")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
icon.name: "preferences-desktop-emoticons"
|
||||
onClicked: emojiDialog.open();
|
||||
EmojiDialog {
|
||||
id: emojiDialog
|
||||
onReact: {
|
||||
page.currentRoom.toggleReaction(hoverActions.event.eventId, emoji);
|
||||
chatBox.focusInputField();
|
||||
}
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
QQC2.ToolTip.text: i18n("Edit")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
visible: hoverActions.showEdit
|
||||
icon.name: "document-edit"
|
||||
onClicked: {
|
||||
currentRoom.chatBoxEditId = hoverActions.event.eventId;
|
||||
currentRoom.chatBoxReplyId = "";
|
||||
chatBox.focusInputField();
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
QQC2.ToolTip.text: i18n("Reply")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
icon.name: "mail-replied-symbolic"
|
||||
onClicked: {
|
||||
currentRoom.chatBoxReplyId = hoverActions.event.eventId;
|
||||
currentRoom.chatBoxEditId = "";
|
||||
chatBox.focusInputField();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
footer: ChatBox {
|
||||
id: chatBox
|
||||
visible: !invitation.visible && !(messageListView.count === 0 && !currentRoom.allHistoryLoaded)
|
||||
width: parent.width
|
||||
onMessageSent: {
|
||||
if (!messageListView.atYEnd) {
|
||||
goToLastMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: FancyEffectsContainer {
|
||||
id: fancyEffectsContainer
|
||||
z: 100
|
||||
|
||||
enabled: Config.showFancyEffects
|
||||
|
||||
function processFancyEffectsReason(fancyEffect) {
|
||||
if (fancyEffect === "snowflake") {
|
||||
fancyEffectsContainer.showSnowEffect()
|
||||
}
|
||||
if (fancyEffect === "fireworks") {
|
||||
fancyEffectsContainer.showFireworksEffect()
|
||||
}
|
||||
if (fancyEffect === "confetti") {
|
||||
fancyEffectsContainer.showConfettiEffect()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
enabled: Config.showFancyEffects
|
||||
target: messageEventModel
|
||||
function onFancyEffectsReasonFound(fancyEffect) {
|
||||
fancyEffectsContainer.processFancyEffectsReason(fancyEffect)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
enabled: Config.showFancyEffects
|
||||
target: actionsHandler
|
||||
function onShowEffect(fancyEffect) {
|
||||
fancyEffectsContainer.processFancyEffectsReason(fancyEffect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function warning(title, message) {
|
||||
page.header.contentItem.text = `${title}<br />${message}`;
|
||||
page.header.contentItem.type = Kirigami.MessageType.Warning;
|
||||
page.header.visible = true;
|
||||
}
|
||||
|
||||
function showUserDetail(user) {
|
||||
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
|
||||
room: currentRoom,
|
||||
user: user,
|
||||
}).open();
|
||||
}
|
||||
|
||||
function goToLastMessage() {
|
||||
currentRoom.markAllMessagesAsRead()
|
||||
// scroll to the very end, i.e to messageListView.YEnd
|
||||
messageListView.positionViewAtIndex(0, ListView.End)
|
||||
}
|
||||
|
||||
function eventToIndex(eventID) {
|
||||
const index = messageEventModel.eventIDToIndex(eventID)
|
||||
if (index === -1)
|
||||
return -1
|
||||
return sortedMessageEventModel.mapFromSource(messageEventModel.index(index, 0)).row
|
||||
}
|
||||
|
||||
function firstVisibleIndex() {
|
||||
let center = messageListView.x + messageListView.width / 2;
|
||||
let index = -1
|
||||
let i = 0
|
||||
while(index === -1 && i < 100) {
|
||||
index = messageListView.indexAt(center, messageListView.y + messageListView.contentY + i);
|
||||
i++;
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
function lastVisibleIndex() {
|
||||
let center = messageListView.x + messageListView.width / 2;
|
||||
let index = -1
|
||||
let i = 0
|
||||
while(index === -1 && i < 100) {
|
||||
index = messageListView.indexAt(center, messageListView.y + messageListView.contentY + messageListView.height - i);
|
||||
i++
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
// Mark all messages as read if all unread messages are visible to the user
|
||||
function markReadIfVisible() {
|
||||
let readMarkerRow = eventToIndex(currentRoom.readMarkerEventId)
|
||||
if (readMarkerRow > 0 && readMarkerRow < firstVisibleIndex()) {
|
||||
currentRoom.markAllMessagesAsRead()
|
||||
}
|
||||
}
|
||||
|
||||
/// Open message context dialog for file and videos
|
||||
function openFileContext(event, file) {
|
||||
const contextMenu = fileDelegateContextMenu.createObject(page, {
|
||||
author: event.author,
|
||||
message: event.message,
|
||||
eventId: event.eventId,
|
||||
source: event.source,
|
||||
file: file,
|
||||
mimeType: event.mimeType,
|
||||
progressInfo: event.progressInfo,
|
||||
plainMessage: event.message,
|
||||
});
|
||||
contextMenu.open();
|
||||
}
|
||||
|
||||
/// Open context menu for normal message
|
||||
function openMessageContext(event, selectedText, plainMessage) {
|
||||
const contextMenu = messageDelegateContextMenu.createObject(page, {
|
||||
selectedText: selectedText,
|
||||
author: event.author,
|
||||
message: event.display,
|
||||
eventId: event.eventId,
|
||||
formattedBody: event.formattedBody,
|
||||
source: event.source,
|
||||
eventType: event.eventType,
|
||||
plainMessage: plainMessage,
|
||||
});
|
||||
contextMenu.open();
|
||||
}
|
||||
}
|
||||
25
src/qml/Page/RoomWindow.qml
Normal file
25
src/qml/Page/RoomWindow.qml
Normal file
@@ -0,0 +1,25 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
Kirigami.ApplicationWindow {
|
||||
id: window
|
||||
required property var currentRoom
|
||||
minimumWidth: Kirigami.Units.gridUnit * 10
|
||||
minimumHeight: Kirigami.Units.gridUnit * 15
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Cancel
|
||||
onActivated: window.close()
|
||||
}
|
||||
pageStack.initialPage: RoomPage {
|
||||
visible: true
|
||||
currentRoom: window.currentRoom
|
||||
disableCancelShortcut: true
|
||||
}
|
||||
}
|
||||
137
src/qml/Page/StartChatPage.qml
Normal file
137
src/qml/Page/StartChatPage.qml
Normal file
@@ -0,0 +1,137 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
id: root
|
||||
|
||||
property var connection
|
||||
|
||||
title: i18n("Start a Chat")
|
||||
|
||||
header: Control {
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
contentItem: RowLayout {
|
||||
Kirigami.SearchField {
|
||||
id: identifierField
|
||||
|
||||
property bool isUserID: text.match(/@(.+):(.+)/g)
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
placeholderText: i18n("Find a user...")
|
||||
|
||||
onAccepted: userDictListModel.search()
|
||||
}
|
||||
|
||||
Button {
|
||||
visible: identifierField.isUserID
|
||||
|
||||
text: i18n("Chat")
|
||||
highlighted: true
|
||||
|
||||
onClicked: {
|
||||
connection.requestDirectChat(identifierField.text);
|
||||
applicationWindow().pageStack.layers.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: userDictListView
|
||||
|
||||
clip: true
|
||||
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
model: UserDirectoryListModel {
|
||||
id: userDictListModel
|
||||
|
||||
connection: root.connection
|
||||
keyword: identifierField.text
|
||||
}
|
||||
|
||||
delegate: Kirigami.AbstractListItem {
|
||||
width: userDictListView.width
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: height
|
||||
Layout.fillHeight: true
|
||||
|
||||
source: avatar
|
||||
name: name
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
text: name
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
text: userID
|
||||
color: Kirigami.Theme.disabledColor
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: joinChatButton
|
||||
Layout.alignment: Qt.AlignRight
|
||||
visible: directChats && directChats.length > 0
|
||||
|
||||
icon.name: "document-send"
|
||||
onClicked: {
|
||||
connection.requestDirectChat(userID);
|
||||
applicationWindow().pageStack.layers.pop();
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
icon.name: "irc-join-channel"
|
||||
// We wants to make sure an user can't start more than one
|
||||
// chat with someone.
|
||||
visible: !joinChatButton.visible
|
||||
|
||||
onClicked: {
|
||||
connection.requestDirectChat(userID);
|
||||
applicationWindow().pageStack.layers.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
visible: userDictListView.count < 1
|
||||
text: i18n("No users available")
|
||||
}
|
||||
}
|
||||
}
|
||||
103
src/qml/Page/WelcomePage.qml
Normal file
103
src/qml/Page/WelcomePage.qml
Normal file
@@ -0,0 +1,103 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
id: welcomePage
|
||||
|
||||
property alias currentStep: module.item
|
||||
|
||||
title: module.item.title ?? i18n("Welcome")
|
||||
|
||||
header: Controls.Control {
|
||||
contentItem: Kirigami.InlineMessage {
|
||||
id: headerMessage
|
||||
type: Kirigami.MessageType.Error
|
||||
showCloseButton: true
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: LoginHelper.init()
|
||||
|
||||
Connections {
|
||||
target: LoginHelper
|
||||
function onErrorOccured(message) {
|
||||
headerMessage.text = message;
|
||||
headerMessage.visible = true;
|
||||
headerMessage.type = Kirigami.MessageType.Error;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Controller
|
||||
function onInitiated() {
|
||||
pageStack.layers.pop();
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Kirigami.Icon {
|
||||
source: "org.kde.neochat"
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 16
|
||||
}
|
||||
Controls.Label {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: 25
|
||||
text: module.item.message ?? module.item.title ?? i18n("Welcome to Matrix")
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: module
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
source: "qrc:/Login.qml"
|
||||
onSourceChanged: {
|
||||
headerMessage.visible = false
|
||||
headerMessage.text = ""
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Controls.Button {
|
||||
text: i18nc("@action:button", "Back")
|
||||
|
||||
enabled: welcomePage.currentStep.previousUrl !== ""
|
||||
visible: welcomePage.currentStep.showBackButton
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: {
|
||||
module.source = welcomePage.currentStep.previousUrl
|
||||
}
|
||||
}
|
||||
|
||||
Controls.Button {
|
||||
id: continueButton
|
||||
enabled: welcomePage.currentStep.acceptable
|
||||
visible: welcomePage.currentStep.showContinueButton
|
||||
action: welcomePage.currentStep.action
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: currentStep
|
||||
|
||||
function onProcessed(nextUrl) {
|
||||
module.source = nextUrl;
|
||||
}
|
||||
function onShowMessage(message) {
|
||||
headerMessage.text = message;
|
||||
headerMessage.visible = true;
|
||||
headerMessage.type = Kirigami.MessageType.Information;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user