Rebrand files names Spectral -> NeoChat
This commit is contained in:
11
imports/NeoChat/Component/AutoListView.qml
Normal file
11
imports/NeoChat/Component/AutoListView.qml
Normal file
@@ -0,0 +1,11 @@
|
||||
import QtQuick 2.12
|
||||
|
||||
ListView {
|
||||
pixelAligned: true
|
||||
|
||||
ScrollHelper {
|
||||
anchors.fill: parent
|
||||
|
||||
flickable: parent
|
||||
}
|
||||
}
|
||||
13
imports/NeoChat/Component/AutoMouseArea.qml
Normal file
13
imports/NeoChat/Component/AutoMouseArea.qml
Normal file
@@ -0,0 +1,13 @@
|
||||
import QtQuick 2.12
|
||||
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
MouseArea {
|
||||
signal primaryClicked()
|
||||
signal secondaryClicked()
|
||||
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
||||
onClicked: mouse.button == Qt.RightButton ? secondaryClicked() : primaryClicked()
|
||||
onPressAndHold: secondaryClicked()
|
||||
}
|
||||
151
imports/NeoChat/Component/AutoRectangle.qml
Normal file
151
imports/NeoChat/Component/AutoRectangle.qml
Normal file
@@ -0,0 +1,151 @@
|
||||
import QtQuick 2.12
|
||||
|
||||
Rectangle {
|
||||
property alias topLeftRadius: topLeftRect.radius
|
||||
property alias topRightRadius: topRightRect.radius
|
||||
property alias bottomLeftRadius: bottomLeftRect.radius
|
||||
property alias bottomRightRadius: bottomRightRect.radius
|
||||
|
||||
property alias topLeftVisible: topLeftRect.visible
|
||||
property alias topRightVisible: topRightRect.visible
|
||||
property alias bottomLeftVisible: bottomLeftRect.visible
|
||||
property alias bottomRightVisible: bottomRightRect.visible
|
||||
|
||||
antialiasing: true
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
|
||||
id: topLeftRect
|
||||
|
||||
antialiasing: true
|
||||
|
||||
color: parent.color
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
|
||||
color: parent.color
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
|
||||
color: parent.color
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
|
||||
id: topRightRect
|
||||
|
||||
antialiasing: true
|
||||
|
||||
color: parent.color
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
|
||||
color: parent.color
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
|
||||
color: parent.color
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
|
||||
id: bottomLeftRect
|
||||
|
||||
antialiasing: true
|
||||
|
||||
color: parent.color
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
|
||||
color: parent.color
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
|
||||
color: parent.color
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
|
||||
id: bottomRightRect
|
||||
|
||||
antialiasing: true
|
||||
|
||||
color: parent.color
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
|
||||
color: parent.color
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
|
||||
color: parent.color
|
||||
}
|
||||
}
|
||||
}
|
||||
62
imports/NeoChat/Component/AutoTextField.qml
Normal file
62
imports/NeoChat/Component/AutoTextField.qml
Normal file
@@ -0,0 +1,62 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
import QtQuick.Controls.Material 2.3
|
||||
|
||||
TextField {
|
||||
id: textField
|
||||
|
||||
selectByMouse: true
|
||||
|
||||
topPadding: 8
|
||||
bottomPadding: 8
|
||||
|
||||
background: Item {
|
||||
Label {
|
||||
id: floatingPlaceholder
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.topMargin: textField.topPadding
|
||||
anchors.leftMargin: textField.leftPadding
|
||||
transformOrigin: Item.TopLeft
|
||||
visible: false
|
||||
color: Material.accent
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "shown"
|
||||
when: textField.text.length != 0 || textField.activeFocus
|
||||
PropertyChanges { target: floatingPlaceholder; scale: 0.8 }
|
||||
PropertyChanges { target: floatingPlaceholder; anchors.topMargin: -floatingPlaceholder.height * 0.4 }
|
||||
}
|
||||
]
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
to: "shown"
|
||||
SequentialAnimation {
|
||||
PropertyAction { target: floatingPlaceholder; property: "text"; value: textField.placeholderText }
|
||||
PropertyAction { target: floatingPlaceholder; property: "visible"; value: true }
|
||||
PropertyAction { target: textField; property: "placeholderTextColor"; value: "transparent" }
|
||||
ParallelAnimation {
|
||||
NumberAnimation { target: floatingPlaceholder; property: "scale"; duration: 250; easing.type: Easing.InOutQuad }
|
||||
NumberAnimation { target: floatingPlaceholder; property: "anchors.topMargin"; duration: 250; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "shown"
|
||||
SequentialAnimation {
|
||||
ParallelAnimation {
|
||||
NumberAnimation { target: floatingPlaceholder; property: "scale"; duration: 250; easing.type: Easing.InOutQuad }
|
||||
NumberAnimation { target: floatingPlaceholder; property: "anchors.topMargin"; duration: 250; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
PropertyAction { target: textField; property: "placeholderTextColor"; value: "grey" }
|
||||
PropertyAction { target: floatingPlaceholder; property: "visible"; value: false }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
468
imports/NeoChat/Component/ChatTextInput.qml
Normal file
468
imports/NeoChat/Component/ChatTextInput.qml
Normal file
@@ -0,0 +1,468 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Component.Emoji 2.0
|
||||
import NeoChat.Dialog 2.0
|
||||
import NeoChat.Effect 2.0
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
import org.kde.neochat 0.1
|
||||
|
||||
Control {
|
||||
id: root
|
||||
|
||||
property alias isReply: replyItem.visible
|
||||
property bool isReaction: false
|
||||
property var replyUser
|
||||
property string replyEventID
|
||||
property string replyContent
|
||||
|
||||
property alias isAutoCompleting: autoCompleteListView.visible
|
||||
property var autoCompleteModel
|
||||
property int autoCompleteBeginPosition
|
||||
property int autoCompleteEndPosition
|
||||
|
||||
property bool hasAttachment: false
|
||||
property url attachmentPath
|
||||
|
||||
padding: 0
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
Kirigami.Separator {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Kirigami.Theme.focusColor
|
||||
}
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 8
|
||||
|
||||
id: replyItem
|
||||
|
||||
visible: false
|
||||
|
||||
spacing: 8
|
||||
|
||||
Control {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
padding: 4
|
||||
|
||||
contentItem: RowLayout {
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: 24
|
||||
Layout.preferredHeight: 24
|
||||
|
||||
source: replyUser ? "image://mxc/" + replyUser.avatarMediaId: ""
|
||||
name: replyUser ? replyUser.displayName : "No name"
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: replyUser ? replyUser.displayName : "No name"
|
||||
rightPadding: 8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: "<style>a{color: " + color + ";} .user-pill{}</style>" + replyContent
|
||||
|
||||
selectByMouse: true
|
||||
readOnly: true
|
||||
wrapMode: Label.Wrap
|
||||
selectedTextColor: "white"
|
||||
textFormat: Text.RichText
|
||||
}
|
||||
}
|
||||
|
||||
EmojiPicker {
|
||||
Layout.fillWidth: true
|
||||
|
||||
id: emojiPicker
|
||||
|
||||
visible: false
|
||||
|
||||
textArea: inputField
|
||||
emojiModel: EmojiModel { id: emojiModel }
|
||||
}
|
||||
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 36
|
||||
Layout.margins: 8
|
||||
|
||||
id: autoCompleteListView
|
||||
|
||||
visible: false
|
||||
|
||||
model: autoCompleteModel
|
||||
|
||||
clip: true
|
||||
spacing: 4
|
||||
orientation: ListView.Horizontal
|
||||
highlightFollowsCurrentItem: true
|
||||
keyNavigationWraps: true
|
||||
|
||||
delegate: Control {
|
||||
property string autoCompleteText: modelData.displayName ?? modelData.unicode
|
||||
property bool isEmoji: modelData.unicode != null
|
||||
readonly property bool highlighted: autoCompleteListView.currentIndex == index
|
||||
|
||||
height: 36
|
||||
padding: 6
|
||||
|
||||
background: Rectangle {
|
||||
visible: !isEmoji
|
||||
color: highlighted ? border.color : "transparent"
|
||||
border.color: isEmoji ? MPalette.accent : modelData.color
|
||||
border.width: 2
|
||||
radius: height / 2
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
width: 24
|
||||
height: 24
|
||||
visible: isEmoji
|
||||
text: autoCompleteText
|
||||
font.pixelSize: 24
|
||||
font.family: "Emoji"
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: 24
|
||||
Layout.preferredHeight: 24
|
||||
|
||||
source: modelData.avatarMediaId ? "image://mxc/" + modelData.avatarMediaId : ""
|
||||
color: modelData.color ? Qt.darker(modelData.color, 1.1) : null
|
||||
}
|
||||
Label {
|
||||
Layout.fillHeight: true
|
||||
|
||||
visible: !isEmoji
|
||||
text: autoCompleteText
|
||||
color: highlighted ? Kirigami.Theme.highlightTextColor : Kirigami.Theme.textColor
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
rightPadding: 8
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
autoCompleteListView.currentIndex = index
|
||||
inputField.replaceAutoComplete(autoCompleteText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
Layout.leftMargin: 12
|
||||
Layout.rightMargin: 12
|
||||
|
||||
visible: emojiPicker.visible || replyItem.visible || autoCompleteListView.visible
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
ToolButton {
|
||||
id: uploadButton
|
||||
|
||||
Layout.preferredWidth: 48
|
||||
Layout.preferredHeight: 48
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
|
||||
visible: !isReply && !hasAttachment
|
||||
|
||||
icon.name: "mail-attachment"
|
||||
|
||||
onClicked: {
|
||||
if (Clipboard.hasImage) {
|
||||
attachDialog.open()
|
||||
} else {
|
||||
var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay)
|
||||
|
||||
fileDialog.chosen.connect(function(path) {
|
||||
if (!path) return
|
||||
|
||||
root.attach(path)
|
||||
})
|
||||
|
||||
fileDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
anchors.fill: parent
|
||||
|
||||
running: currentRoom && currentRoom.hasFileUploading
|
||||
}
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
Layout.preferredWidth: 48
|
||||
Layout.preferredHeight: 48
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
|
||||
id: cancelReplyButton
|
||||
|
||||
visible: isReply
|
||||
|
||||
icon.name: "dialog-cancel"
|
||||
|
||||
onClicked: clearReply()
|
||||
}
|
||||
|
||||
Control {
|
||||
Layout.margins: 6
|
||||
Layout.preferredHeight: 36
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
visible: hasAttachment
|
||||
|
||||
rightPadding: 8
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 0
|
||||
|
||||
ToolButton {
|
||||
Layout.preferredWidth: height
|
||||
Layout.fillHeight: true
|
||||
|
||||
id: cancelAttachmentButton
|
||||
|
||||
icon.name: "dialog-cancel"
|
||||
|
||||
onClicked: {
|
||||
hasAttachment = false;
|
||||
attachmentPath = "";
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
text: attachmentPath !== "" ? attachmentPath.toString().substring(attachmentPath.toString().lastIndexOf('/') + 1, attachmentPath.length) : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextArea {
|
||||
property real progress: 0
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 48
|
||||
|
||||
id: inputField
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
placeholderText: "Send a Message"
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
selectByMouse: true
|
||||
verticalAlignment: TextEdit.AlignVCenter
|
||||
|
||||
text: currentRoom != null ? currentRoom.cachedInput : ""
|
||||
|
||||
background: Item {}
|
||||
|
||||
Rectangle {
|
||||
width: currentRoom && currentRoom.hasFileUploading ? parent.width * currentRoom.fileUploadingProgress / 100 : 0
|
||||
height: parent.height
|
||||
|
||||
opacity: 0.2
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: timeoutTimer
|
||||
|
||||
repeat: false
|
||||
interval: 2000
|
||||
onTriggered: {
|
||||
repeatTimer.stop()
|
||||
currentRoom.sendTypingNotification(false)
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: repeatTimer
|
||||
|
||||
repeat: true
|
||||
interval: 5000
|
||||
triggeredOnStart: true
|
||||
onTriggered: currentRoom.sendTypingNotification(true)
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
if (event.modifiers & Qt.ShiftModifier) {
|
||||
insert(cursorPosition, "\n")
|
||||
} else {
|
||||
postMessage(text)
|
||||
text = ""
|
||||
clearReply()
|
||||
closeAll()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: closeAll()
|
||||
|
||||
Keys.onBacktabPressed: if (isAutoCompleting) autoCompleteListView.decrementCurrentIndex()
|
||||
|
||||
Keys.onTabPressed: {
|
||||
if (isAutoCompleting) {
|
||||
autoCompleteListView.incrementCurrentIndex()
|
||||
} else {
|
||||
autoCompleteBeginPosition = text.substring(0, cursorPosition).lastIndexOf(" ") + 1
|
||||
var autoCompletePrefix = text.substring(0, cursorPosition).split(" ").pop()
|
||||
if (!autoCompletePrefix) return
|
||||
if (autoCompletePrefix.startsWith(":")) {
|
||||
autoCompleteBeginPosition = text.substring(0, cursorPosition).lastIndexOf(" ") + 1
|
||||
autoCompleteModel = emojiModel.filterModel(autoCompletePrefix)
|
||||
if (autoCompleteModel.length === 0) return
|
||||
isAutoCompleting = true
|
||||
autoCompleteEndPosition = cursorPosition
|
||||
} else {
|
||||
autoCompleteModel = currentRoom.getUsers(autoCompletePrefix)
|
||||
if (autoCompleteModel.length === 0) return
|
||||
isAutoCompleting = true
|
||||
autoCompleteEndPosition = cursorPosition
|
||||
}
|
||||
}
|
||||
replaceAutoComplete(autoCompleteListView.currentItem.autoCompleteText)
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
timeoutTimer.restart()
|
||||
repeatTimer.start()
|
||||
currentRoom.cachedInput = text
|
||||
|
||||
if (cursorPosition !== autoCompleteBeginPosition && cursorPosition !== autoCompleteEndPosition) {
|
||||
isAutoCompleting = false
|
||||
autoCompleteListView.currentIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
function replaceAutoComplete(word) {
|
||||
remove(autoCompleteBeginPosition, autoCompleteEndPosition)
|
||||
autoCompleteEndPosition = autoCompleteBeginPosition + word.length
|
||||
insert(cursorPosition, word)
|
||||
}
|
||||
|
||||
function postMessage(text) {
|
||||
if(!currentRoom) { return }
|
||||
|
||||
if (hasAttachment) {
|
||||
currentRoom.uploadFile(attachmentPath, text)
|
||||
clearAttachment()
|
||||
return
|
||||
}
|
||||
|
||||
if (text.trim().length === 0) { return }
|
||||
|
||||
var PREFIX_ME = '/me '
|
||||
var PREFIX_NOTICE = '/notice '
|
||||
var PREFIX_RAINBOW = '/rainbow '
|
||||
|
||||
var messageEventType = RoomMessageEvent.Text
|
||||
|
||||
if (text.indexOf(PREFIX_RAINBOW) === 0) {
|
||||
text = text.substr(PREFIX_RAINBOW.length)
|
||||
|
||||
var parsedText = ""
|
||||
var rainbowColor = ["#ff2b00", "#ff5500", "#ff8000", "#ffaa00", "#ffd500", "#ffff00", "#d4ff00", "#aaff00", "#80ff00", "#55ff00", "#2bff00", "#00ff00", "#00ff2b", "#00ff55", "#00ff80", "#00ffaa", "#00ffd5", "#00ffff", "#00d4ff", "#00aaff", "#007fff", "#0055ff", "#002bff", "#0000ff", "#2a00ff", "#5500ff", "#7f00ff", "#aa00ff", "#d400ff", "#ff00ff", "#ff00d4", "#ff00aa", "#ff0080", "#ff0055", "#ff002b", "#ff0000"]
|
||||
for (var i = 0; i < text.length; i++) {
|
||||
parsedText = parsedText + "<font color='" + rainbowColor[i % rainbowColor.length] + "'>" + text.charAt(i) + "</font>"
|
||||
}
|
||||
currentRoom.postHtmlMessage(text, parsedText, RoomMessageEvent.Text, replyEventID)
|
||||
return
|
||||
}
|
||||
|
||||
if (text.indexOf(PREFIX_ME) === 0) {
|
||||
text = text.substr(PREFIX_ME.length)
|
||||
messageEventType = RoomMessageEvent.Emote
|
||||
} else if (text.indexOf(PREFIX_NOTICE) === 0) {
|
||||
text = text.substr(PREFIX_NOTICE.length)
|
||||
messageEventType = RoomMessageEvent.Notice
|
||||
}
|
||||
|
||||
console.log(replyContent, replyUser, replyEventID, messageEventType);
|
||||
currentRoom.postArbitaryMessage(text, messageEventType, replyEventID)
|
||||
}
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
Layout.preferredWidth: 48
|
||||
Layout.preferredHeight: 48
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
|
||||
id: emojiButton
|
||||
icon.name: "preferences-desktop-emoticons"
|
||||
|
||||
onClicked: emojiPicker.visible = !emojiPicker.visible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function insert(str) {
|
||||
inputField.insert(inputField.cursorPosition, str)
|
||||
}
|
||||
|
||||
function clear() {
|
||||
inputField.clear()
|
||||
}
|
||||
|
||||
function clearReply() {
|
||||
isReply = false
|
||||
replyUser = null;
|
||||
replyContent = "";
|
||||
replyEventID = ""
|
||||
}
|
||||
|
||||
function focus() {
|
||||
inputField.forceActiveFocus()
|
||||
}
|
||||
|
||||
function closeAll() {
|
||||
replyItem.visible = false
|
||||
autoCompleteListView.visible = false
|
||||
emojiPicker.visible = false
|
||||
}
|
||||
|
||||
function attach(localPath) {
|
||||
hasAttachment = true
|
||||
attachmentPath = localPath
|
||||
}
|
||||
|
||||
function clearAttachment() {
|
||||
hasAttachment = false
|
||||
attachmentPath = ""
|
||||
}
|
||||
}
|
||||
135
imports/NeoChat/Component/Emoji/EmojiPicker.qml
Normal file
135
imports/NeoChat/Component/Emoji/EmojiPicker.qml
Normal file
@@ -0,0 +1,135 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtQuick.Controls.Material 2.12
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
|
||||
import org.kde.neochat 0.1
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
ColumnLayout {
|
||||
property string emojiCategory: "history"
|
||||
property var textArea
|
||||
property var emojiModel
|
||||
|
||||
spacing: 0
|
||||
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 48
|
||||
Layout.leftMargin: 24
|
||||
Layout.rightMargin: 24
|
||||
|
||||
boundsBehavior: Flickable.DragOverBounds
|
||||
|
||||
clip: true
|
||||
|
||||
orientation: ListView.Horizontal
|
||||
|
||||
model: ListModel {
|
||||
ListElement { label: "⌛️"; category: "history" }
|
||||
ListElement { label: "😏"; category: "people" }
|
||||
ListElement { label: "🌲"; category: "nature" }
|
||||
ListElement { label: "🍛"; category: "food"}
|
||||
ListElement { label: "🚁"; category: "activity" }
|
||||
ListElement { label: "🚅"; category: "travel" }
|
||||
ListElement { label: "💡"; category: "objects" }
|
||||
ListElement { label: "🔣"; category: "symbols" }
|
||||
ListElement { label: "🏁"; category: "flags" }
|
||||
}
|
||||
|
||||
delegate: ItemDelegate {
|
||||
width: 64
|
||||
height: 48
|
||||
|
||||
contentItem: Label {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
font.pixelSize: 24
|
||||
font.family: 'emoji'
|
||||
text: label
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
width: parent.width
|
||||
height: 2
|
||||
|
||||
visible: emojiCategory === category
|
||||
|
||||
color: Material.accent
|
||||
}
|
||||
|
||||
onClicked: emojiCategory = category
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
Layout.leftMargin: 12
|
||||
Layout.rightMargin: 12
|
||||
|
||||
color: MSettings.darkTheme ? "#424242" : "#e7ebeb"
|
||||
}
|
||||
|
||||
GridView {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 180
|
||||
|
||||
cellWidth: 48
|
||||
cellHeight: 48
|
||||
|
||||
boundsBehavior: Flickable.DragOverBounds
|
||||
|
||||
clip: true
|
||||
|
||||
model: {
|
||||
switch (emojiCategory) {
|
||||
case "history":
|
||||
return emojiModel.history
|
||||
case "people":
|
||||
return emojiModel.people
|
||||
case "nature":
|
||||
return emojiModel.nature
|
||||
case "food":
|
||||
return emojiModel.food
|
||||
case "activity":
|
||||
return emojiModel.activity
|
||||
case "travel":
|
||||
return emojiModel.travel
|
||||
case "objects":
|
||||
return emojiModel.objects
|
||||
case "symbols":
|
||||
return emojiModel.symbols
|
||||
case "flags":
|
||||
return emojiModel.flags
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
delegate: ItemDelegate {
|
||||
width: 48
|
||||
height: 48
|
||||
|
||||
contentItem: Label {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
font.pixelSize: 32
|
||||
font.family: 'emoji'
|
||||
text: modelData.unicode
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
textArea.insert(textArea.cursorPosition, modelData.unicode)
|
||||
emojiModel.emojiUsed(modelData)
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
}
|
||||
}
|
||||
2
imports/NeoChat/Component/Emoji/qmldir
Normal file
2
imports/NeoChat/Component/Emoji/qmldir
Normal file
@@ -0,0 +1,2 @@
|
||||
module NeoChat.Component.Emoji
|
||||
EmojiPicker 2.0 EmojiPicker.qml
|
||||
46
imports/NeoChat/Component/FullScreenImage.qml
Normal file
46
imports/NeoChat/Component/FullScreenImage.qml
Normal file
@@ -0,0 +1,46 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
ApplicationWindow {
|
||||
property string filename
|
||||
property url localPath
|
||||
|
||||
id: root
|
||||
|
||||
flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground
|
||||
visible: true
|
||||
visibility: Qt.WindowFullScreen
|
||||
|
||||
title: i18n("Image View - %1", filename)
|
||||
|
||||
color: "#BB000000"
|
||||
|
||||
Shortcut {
|
||||
sequence: "Escape"
|
||||
onActivated: root.destroy()
|
||||
}
|
||||
|
||||
AnimatedImage {
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: Math.min(sourceSize.width, root.width)
|
||||
height: Math.min(sourceSize.height, root.height)
|
||||
|
||||
cache: false
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
source: localPath
|
||||
}
|
||||
|
||||
ItemDelegate {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
|
||||
id: closeButton
|
||||
|
||||
width: 64
|
||||
height: 64
|
||||
|
||||
onClicked: root.destroy()
|
||||
}
|
||||
}
|
||||
105
imports/NeoChat/Component/ScrollHelper.qml
Normal file
105
imports/NeoChat/Component/ScrollHelper.qml
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Michael Bohlender, <michael.bohlender@kdemail.net>
|
||||
* Copyright (C) 2017 Christian Mollekopf, <mollekopf@kolabsystems.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
/*
|
||||
* The MouseArea + interactive: false + maximumFlickVelocity are required
|
||||
* to fix scrolling for desktop systems where we don't want flicking behaviour.
|
||||
*
|
||||
* See also:
|
||||
* ScrollView.qml in qtquickcontrols
|
||||
* qquickwheelarea.cpp in qtquickcontrols
|
||||
*/
|
||||
MouseArea {
|
||||
id: root
|
||||
propagateComposedEvents: true
|
||||
|
||||
property Flickable flickable
|
||||
property alias enabled: root.enabled
|
||||
|
||||
//Place the mouse area under the flickable
|
||||
z: -1
|
||||
onFlickableChanged: {
|
||||
if (enabled) {
|
||||
flickable.interactive = false
|
||||
flickable.maximumFlickVelocity = 100000
|
||||
flickable.boundsBehavior = Flickable.StopAtBounds
|
||||
root.parent = flickable
|
||||
}
|
||||
}
|
||||
|
||||
function calculateNewPosition(flickableItem, wheel) {
|
||||
//Nothing to scroll
|
||||
if (flickableItem.contentHeight < flickableItem.height) {
|
||||
return flickableItem.contentY;
|
||||
}
|
||||
//Ignore 0 events (happens at least with Christians trackpad)
|
||||
if (wheel.pixelDelta.y == 0 && wheel.angleDelta.y == 0) {
|
||||
return flickableItem.contentY;
|
||||
}
|
||||
//pixelDelta seems to be the same as angleDelta/8
|
||||
var pixelDelta = 0
|
||||
//The pixelDelta is a smaller number if both are provided, so pixelDelta can be 0 while angleDelta is still something. So we check the angleDelta
|
||||
if (wheel.angleDelta.y) {
|
||||
var wheelScrollLines = 3 //Default value of QApplication wheelScrollLines property
|
||||
var pixelPerLine = 20 //Default value in Qt, originally comes from QTextEdit
|
||||
var ticks = (wheel.angleDelta.y / 8) / 15.0 //Divide by 8 gives us pixels typically come in 15pixel steps.
|
||||
pixelDelta = ticks * pixelPerLine * wheelScrollLines
|
||||
} else {
|
||||
pixelDelta = wheel.pixelDelta.y
|
||||
}
|
||||
|
||||
if (!pixelDelta) {
|
||||
return flickableItem.contentY;
|
||||
}
|
||||
|
||||
var minYExtent = flickableItem.originY + flickableItem.topMargin;
|
||||
var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height;
|
||||
|
||||
if (typeof(flickableItem.headerItem) !== "undefined" && flickableItem.headerItem) {
|
||||
minYExtent += flickableItem.headerItem.height
|
||||
}
|
||||
|
||||
//Avoid overscrolling
|
||||
return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta));
|
||||
}
|
||||
|
||||
onWheel: {
|
||||
var newPos = calculateNewPosition(flickable, wheel);
|
||||
// console.warn("Delta: ", wheel.pixelDelta.y);
|
||||
// console.warn("Old position: ", flickable.contentY);
|
||||
// console.warn("New position: ", newPos);
|
||||
|
||||
// Show the scrollbars
|
||||
flickable.flick(0, 0);
|
||||
flickable.contentY = newPos;
|
||||
cancelFlickStateTimer.start()
|
||||
}
|
||||
|
||||
|
||||
Timer {
|
||||
id: cancelFlickStateTimer
|
||||
//How long the scrollbar will remain visible
|
||||
interval: 500
|
||||
// Hide the scrollbars
|
||||
onTriggered: flickable.cancelFlick();
|
||||
}
|
||||
}
|
||||
211
imports/NeoChat/Component/Timeline/AudioDelegate.qml
Normal file
211
imports/NeoChat/Component/Timeline/AudioDelegate.qml
Normal file
@@ -0,0 +1,211 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtGraphicalEffects 1.0
|
||||
import Qt.labs.platform 1.0 as Platform
|
||||
import QtMultimedia 5.12
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
import org.kde.neochat 0.1
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Dialog 2.0
|
||||
import NeoChat.Menu.Timeline 2.0
|
||||
import NeoChat.Effect 2.0
|
||||
|
||||
RowLayout {
|
||||
readonly property bool avatarVisible: !sentByMe && showAuthor
|
||||
readonly property bool sentByMe: author.isLocalUser
|
||||
|
||||
id: root
|
||||
|
||||
spacing: 4
|
||||
|
||||
z: -5
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
|
||||
visible: avatarVisible
|
||||
name: author.displayName
|
||||
source: author.avatarMediaId ? "image://mxc/" + author.avatarMediaId : ""
|
||||
color: author.color
|
||||
|
||||
Component {
|
||||
id: userDetailDialog
|
||||
|
||||
UserDetailDialog {}
|
||||
}
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
circular: true
|
||||
|
||||
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
|
||||
visible: !(sentByMe || avatarVisible)
|
||||
}
|
||||
|
||||
Control {
|
||||
Layout.maximumWidth: messageListView.width - (!sentByMe ? 36 + root.spacing : 0) - 48
|
||||
|
||||
padding: 12
|
||||
|
||||
Audio {
|
||||
id: audio
|
||||
|
||||
source: currentRoom.urlToMxcUrl(content.url)
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
ToolButton {
|
||||
icon.name: audio.playbackState == Audio.PlayingState ? "media-playback-pause" : "media-playback-start"
|
||||
|
||||
onClicked: {
|
||||
if (audio.playbackState == Audio.PlayingState) {
|
||||
audio.pause()
|
||||
} else {
|
||||
audio.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: display
|
||||
color: MPalette.foreground
|
||||
wrapMode: Label.Wrap
|
||||
font.pixelSize: 18
|
||||
font.weight: Font.Medium
|
||||
font.capitalization: Font.AllUppercase
|
||||
}
|
||||
|
||||
Label {
|
||||
readonly property int duration: content.info.duration ?? audio.duration ?? 0
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: duration
|
||||
|
||||
text: humanSize(duration)
|
||||
color: MPalette.lighter
|
||||
wrapMode: Label.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: AutoRectangle {
|
||||
readonly property int minorRadius: 8
|
||||
|
||||
id: bubbleBackground
|
||||
|
||||
color: MPalette.background
|
||||
radius: 18
|
||||
|
||||
topLeftVisible: !sentByMe && (bubbleShape == 3 || bubbleShape == 2)
|
||||
topRightVisible: sentByMe && (bubbleShape == 3 || bubbleShape == 2)
|
||||
bottomLeftVisible: !sentByMe && (bubbleShape == 1 || bubbleShape == 2)
|
||||
bottomRightVisible: sentByMe && (bubbleShape == 1 || bubbleShape == 2)
|
||||
|
||||
topLeftRadius: minorRadius
|
||||
topRightRadius: minorRadius
|
||||
bottomLeftRadius: minorRadius
|
||||
bottomRightRadius: minorRadius
|
||||
|
||||
AutoMouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
id: messageMouseArea
|
||||
|
||||
onSecondaryClicked: {
|
||||
var contextMenu = fileDelegateContextMenu.createObject(root)
|
||||
contextMenu.viewSource.connect(function() {
|
||||
messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open()
|
||||
})
|
||||
contextMenu.downloadAndOpen.connect(downloadAndOpen)
|
||||
contextMenu.saveFileAs.connect(saveFileAs)
|
||||
contextMenu.reply.connect(function() {
|
||||
roomPanelInput.replyModel = Object.assign({}, model)
|
||||
roomPanelInput.isReply = true
|
||||
roomPanelInput.focus()
|
||||
})
|
||||
contextMenu.redact.connect(function() {
|
||||
currentRoom.redactEvent(eventId)
|
||||
})
|
||||
contextMenu.popup()
|
||||
}
|
||||
|
||||
Component {
|
||||
id: messageSourceDialog
|
||||
|
||||
MessageSourceDialog {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: openFolderDialog
|
||||
|
||||
OpenFolderDialog {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fileDelegateContextMenu
|
||||
|
||||
FileDelegateContextMenu {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveFileAs() {
|
||||
var folderDialog = openFolderDialog.createObject(ApplicationWindow.overlay)
|
||||
|
||||
folderDialog.chosen.connect(function(path) {
|
||||
if (!path) return
|
||||
|
||||
currentRoom.downloadFile(eventId, path + "/" + currentRoom.fileNameToDownload(eventId))
|
||||
})
|
||||
|
||||
folderDialog.open()
|
||||
}
|
||||
|
||||
function downloadAndOpen()
|
||||
{
|
||||
if (downloaded) openSavedFile()
|
||||
else
|
||||
{
|
||||
openOnFinished = true
|
||||
currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
|
||||
}
|
||||
}
|
||||
|
||||
function openSavedFile() {
|
||||
if (Qt.openUrlExternally(progressInfo.localPath)) return;
|
||||
if (Qt.openUrlExternally(progressInfo.localDir)) return;
|
||||
}
|
||||
|
||||
function humanSize(duration) {
|
||||
if (!duration)
|
||||
return qsTr("Unknown", "Unknown duration")
|
||||
if (duration < 1000)
|
||||
return qsTr("An instant")
|
||||
duration = Math.round(duration / 100) / 10
|
||||
if (duration < 60)
|
||||
return qsTr("%1 sec.").arg(duration)
|
||||
duration = Math.round(duration / 6) / 10
|
||||
if (duration < 60)
|
||||
return qsTr("%1 min.").arg(duration)
|
||||
return qsTr("%1 hrs.").arg(Math.round(duration / 6) / 10)
|
||||
}
|
||||
}
|
||||
240
imports/NeoChat/Component/Timeline/FileDelegate.qml
Normal file
240
imports/NeoChat/Component/Timeline/FileDelegate.qml
Normal file
@@ -0,0 +1,240 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtQuick.Controls.Material 2.12
|
||||
import QtGraphicalEffects 1.0
|
||||
import Qt.labs.platform 1.0 as Platform
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
import org.kde.neochat 0.1
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Dialog 2.0
|
||||
import NeoChat.Menu.Timeline 2.0
|
||||
import NeoChat.Effect 2.0
|
||||
|
||||
RowLayout {
|
||||
readonly property bool avatarVisible: !sentByMe && showAuthor
|
||||
readonly property bool sentByMe: author.isLocalUser
|
||||
|
||||
property bool openOnFinished: false
|
||||
readonly property bool downloaded: progressInfo && progressInfo.completed
|
||||
|
||||
id: root
|
||||
|
||||
spacing: 4
|
||||
|
||||
onDownloadedChanged: if (downloaded && openOnFinished) openSavedFile()
|
||||
|
||||
z: -5
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
|
||||
visible: avatarVisible
|
||||
name: author.displayName
|
||||
source: author.avatarMediaId ? "image://mxc/" + author.avatarMediaId : ""
|
||||
color: author.color
|
||||
|
||||
Component {
|
||||
id: userDetailDialog
|
||||
|
||||
UserDetailDialog {}
|
||||
}
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
circular: true
|
||||
|
||||
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
|
||||
visible: !(sentByMe || avatarVisible)
|
||||
}
|
||||
|
||||
Control {
|
||||
Layout.maximumWidth: messageListView.width - (!sentByMe ? 36 + root.spacing : 0) - 48
|
||||
|
||||
padding: 12
|
||||
|
||||
contentItem: RowLayout {
|
||||
ToolButton {
|
||||
icon.name: progressInfo.completed ? "document-open" : "document-save"
|
||||
|
||||
onClicked: progressInfo.completed ? openSavedFile() : saveFileAs()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: display
|
||||
color: MPalette.foreground
|
||||
wrapMode: Label.Wrap
|
||||
font.pixelSize: 18
|
||||
font.weight: Font.Medium
|
||||
font.capitalization: Font.AllUppercase
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: !progressInfo.completed && progressInfo.active ? (humanSize(progressInfo.progress) + "/" + humanSize(progressInfo.total)) : humanSize(content.info ? content.info.size : 0)
|
||||
color: MPalette.lighter
|
||||
wrapMode: Label.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: MPalette.background
|
||||
radius: 18
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
|
||||
visible: !sentByMe && (bubbleShape == 3 || bubbleShape == 2)
|
||||
|
||||
color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent
|
||||
radius: 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
|
||||
visible: sentByMe && (bubbleShape == 3 || bubbleShape == 2)
|
||||
|
||||
color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent
|
||||
radius: 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
|
||||
visible: !sentByMe && (bubbleShape == 1 || bubbleShape == 2)
|
||||
|
||||
color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent
|
||||
radius: 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
|
||||
width: parent.width / 2
|
||||
height: parent.height / 2
|
||||
|
||||
visible: sentByMe && (bubbleShape == 1 || bubbleShape == 2)
|
||||
|
||||
color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent
|
||||
radius: 2
|
||||
}
|
||||
|
||||
AutoMouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
id: messageMouseArea
|
||||
|
||||
onSecondaryClicked: {
|
||||
var contextMenu = fileDelegateContextMenu.createObject(root)
|
||||
contextMenu.viewSource.connect(function() {
|
||||
messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open()
|
||||
})
|
||||
contextMenu.downloadAndOpen.connect(downloadAndOpen)
|
||||
contextMenu.saveFileAs.connect(saveFileAs)
|
||||
contextMenu.reply.connect(function() {
|
||||
roomPanelInput.replyModel = Object.assign({}, model)
|
||||
roomPanelInput.isReply = true
|
||||
roomPanelInput.focus()
|
||||
})
|
||||
contextMenu.redact.connect(function() {
|
||||
currentRoom.redactEvent(eventId)
|
||||
})
|
||||
contextMenu.popup()
|
||||
}
|
||||
|
||||
Component {
|
||||
id: messageSourceDialog
|
||||
|
||||
MessageSourceDialog {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: openFolderDialog
|
||||
|
||||
OpenFolderDialog {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fileDelegateContextMenu
|
||||
|
||||
FileDelegateContextMenu {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveFileAs() {
|
||||
var folderDialog = openFolderDialog.createObject(ApplicationWindow.overlay)
|
||||
|
||||
folderDialog.chosen.connect(function(path) {
|
||||
if (!path) return
|
||||
|
||||
currentRoom.downloadFile(eventId, path + "/" + currentRoom.fileNameToDownload(eventId))
|
||||
})
|
||||
|
||||
folderDialog.open()
|
||||
}
|
||||
|
||||
function downloadAndOpen()
|
||||
{
|
||||
if (downloaded) openSavedFile()
|
||||
else
|
||||
{
|
||||
openOnFinished = true
|
||||
currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
|
||||
}
|
||||
}
|
||||
|
||||
function openSavedFile()
|
||||
{
|
||||
if (Qt.openUrlExternally(progressInfo.localPath)) return;
|
||||
if (Qt.openUrlExternally(progressInfo.localDir)) return;
|
||||
}
|
||||
|
||||
function humanSize(bytes)
|
||||
{
|
||||
if (!bytes)
|
||||
return qsTr("Unknown", "Unknown attachment size")
|
||||
if (bytes < 4000)
|
||||
return qsTr("%1 bytes").arg(bytes)
|
||||
bytes = Math.round(bytes / 100) / 10
|
||||
if (bytes < 2000)
|
||||
return qsTr("%1 KB").arg(bytes)
|
||||
bytes = Math.round(bytes / 100) / 10
|
||||
if (bytes < 2000)
|
||||
return qsTr("%1 MB").arg(bytes)
|
||||
return qsTr("%1 GB").arg(Math.round(bytes / 100) / 10)
|
||||
}
|
||||
}
|
||||
169
imports/NeoChat/Component/Timeline/ImageDelegate.qml
Normal file
169
imports/NeoChat/Component/Timeline/ImageDelegate.qml
Normal file
@@ -0,0 +1,169 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtGraphicalEffects 1.0
|
||||
import Qt.labs.platform 1.0 as Platform
|
||||
|
||||
import org.kde.neochat 0.1
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Dialog 2.0
|
||||
import NeoChat.Menu.Timeline 2.0
|
||||
import NeoChat.Effect 2.0
|
||||
|
||||
Image {
|
||||
readonly property bool isAnimated: contentType === "image/gif"
|
||||
|
||||
property bool openOnFinished: false
|
||||
readonly property bool downloaded: progressInfo && progressInfo.completed
|
||||
|
||||
readonly property bool isThumbnail: !(content.info.thumbnail_info == null || content.thumbnailMediaId == null)
|
||||
// readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info
|
||||
readonly property var info: content.info
|
||||
readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId
|
||||
|
||||
id: img
|
||||
|
||||
source: "image://mxc/" + mediaId
|
||||
|
||||
sourceSize.width: info.w
|
||||
sourceSize.height: info.h
|
||||
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: img.width
|
||||
height: img.height
|
||||
radius: 18
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 8
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 8
|
||||
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 4
|
||||
|
||||
contentItem: RowLayout {
|
||||
Label {
|
||||
text: Qt.formatTime(time)
|
||||
color: "white"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
Label {
|
||||
text: author.displayName
|
||||
color: "white"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
radius: height / 2
|
||||
color: "black"
|
||||
opacity: 0.3
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
|
||||
visible: progressInfo.active && !downloaded
|
||||
|
||||
color: "#BB000000"
|
||||
|
||||
ProgressBar {
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: parent.width * 0.8
|
||||
|
||||
from: 0
|
||||
to: progressInfo.total
|
||||
value: progressInfo.progress
|
||||
}
|
||||
}
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
id: messageMouseArea
|
||||
|
||||
onPrimaryClicked: fullScreenImage.createObject(parent, {"filename": eventId, "localPath": currentRoom.urlToDownload(eventId)}).showFullScreen()
|
||||
|
||||
onSecondaryClicked: {
|
||||
var contextMenu = imageDelegateContextMenu.createObject(root)
|
||||
contextMenu.viewSource.connect(function() {
|
||||
messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open()
|
||||
})
|
||||
contextMenu.downloadAndOpen.connect(downloadAndOpen)
|
||||
contextMenu.saveFileAs.connect(saveFileAs)
|
||||
contextMenu.reply.connect(function() {
|
||||
roomPanelInput.replyModel = Object.assign({}, model)
|
||||
roomPanelInput.isReply = true
|
||||
roomPanelInput.focus()
|
||||
})
|
||||
contextMenu.redact.connect(function() {
|
||||
currentRoom.redactEvent(eventId)
|
||||
})
|
||||
contextMenu.popup()
|
||||
}
|
||||
|
||||
Component {
|
||||
id: messageSourceDialog
|
||||
|
||||
MessageSourceDialog {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: openFolderDialog
|
||||
|
||||
OpenFolderDialog {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: imageDelegateContextMenu
|
||||
|
||||
FileDelegateContextMenu {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fullScreenImage
|
||||
|
||||
FullScreenImage {}
|
||||
}
|
||||
}
|
||||
|
||||
function saveFileAs() {
|
||||
var folderDialog = openFolderDialog.createObject(ApplicationWindow.overlay)
|
||||
|
||||
folderDialog.chosen.connect(function(path) {
|
||||
if (!path) return
|
||||
|
||||
currentRoom.downloadFile(eventId, path + "/" + currentRoom.fileNameToDownload(eventId))
|
||||
})
|
||||
|
||||
folderDialog.open()
|
||||
}
|
||||
|
||||
function downloadAndOpen()
|
||||
{
|
||||
if (downloaded) openSavedFile()
|
||||
else
|
||||
{
|
||||
openOnFinished = true
|
||||
currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
|
||||
}
|
||||
}
|
||||
|
||||
function openSavedFile()
|
||||
{
|
||||
if (Qt.openUrlExternally(progressInfo.localPath)) return;
|
||||
if (Qt.openUrlExternally(progressInfo.localDir)) return;
|
||||
}
|
||||
}
|
||||
125
imports/NeoChat/Component/Timeline/MessageDelegate.qml
Normal file
125
imports/NeoChat/Component/Timeline/MessageDelegate.qml
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
*
|
||||
* SPDX-LicenseIdentifier: GPL-3.0-or-later
|
||||
*/
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12 as QQC2
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtGraphicalEffects 1.12
|
||||
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
import org.kde.neochat 0.1
|
||||
import NeoChat.Setting 0.1
|
||||
import NeoChat.Component 2.0
|
||||
|
||||
RowLayout {
|
||||
default property alias innerObject : column.children
|
||||
|
||||
readonly property bool sentByMe: author.isLocalUser
|
||||
readonly property bool darkBackground: !sentByMe
|
||||
readonly property bool replyVisible: reply ?? false
|
||||
readonly property bool failed: marks == EventStatus.SendingFailed
|
||||
readonly property color authorColor: eventType == "notice" ? MPalette.primary : author.color
|
||||
readonly property color replyAuthorColor: replyVisible ? reply.author.color : MPalette.accent
|
||||
|
||||
property alias mouseArea: controlContainer.children
|
||||
|
||||
signal saveFileAs()
|
||||
signal openExternally()
|
||||
|
||||
id: root
|
||||
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
Layout.leftMargin: Kirigami.Units.smallSpacing
|
||||
Layout.rightMargin: Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.minimumWidth: Kirigami.Units.iconSizes.medium
|
||||
Layout.minimumHeight: Kirigami.Units.iconSizes.medium
|
||||
Layout.maximumWidth: Kirigami.Units.iconSizes.medium
|
||||
Layout.maximumHeight: Kirigami.Units.iconSizes.medium
|
||||
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
visible: showAuthor
|
||||
name: author.displayName
|
||||
source: author.avatarMediaId ? "image://mxc/" + author.avatarMediaId : ""
|
||||
color: author.color
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.minimumWidth: Kirigami.Units.iconSizes.medium
|
||||
Layout.preferredHeight: 1
|
||||
|
||||
visible: !showAuthor
|
||||
}
|
||||
|
||||
|
||||
QQC2.Control {
|
||||
id: controlContainer
|
||||
Layout.fillWidth: true
|
||||
contentItem: ColumnLayout {
|
||||
id: column
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: showAuthor
|
||||
|
||||
text: author.displayName
|
||||
font.bold: true
|
||||
color: author.color
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: replyVisible
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: Kirigami.Units.smallSpacing
|
||||
Layout.fillHeight: true
|
||||
|
||||
color: Kirigami.Theme.highlightColor
|
||||
}
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
source: replyVisible && reply.author.avatarMediaId ? "image://mxc/" + reply.author.avatarMediaId : ""
|
||||
name: replyVisible ? reply.author.displayName : "H"
|
||||
color: replyVisible ? reply.author.color : Kirigami.Theme.highlightColor
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: replyVisible ? reply.author.displayName : ""
|
||||
color: replyVisible ? reply.author.color: null
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: replyVisible ? reply.display : ""
|
||||
|
||||
color: Kirigami.Theme.textColor
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
textFormat: Text.RichText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
imports/NeoChat/Component/Timeline/ReactionDelegate.qml
Normal file
72
imports/NeoChat/Component/Timeline/ReactionDelegate.qml
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
*
|
||||
* SPDX-LicenseIdentifier: GPL-3.0-only
|
||||
*/
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
Flow {
|
||||
visible: (reaction && reaction.length > 0) ?? false
|
||||
|
||||
spacing: 8
|
||||
|
||||
Repeater {
|
||||
model: reaction
|
||||
|
||||
delegate: Control {
|
||||
width: Math.max(implicitWidth, Kirigami.Units.largeSpacing * 3)
|
||||
height: Kirigami.Units.largeSpacing * 3
|
||||
|
||||
horizontalPadding: 6
|
||||
verticalPadding: 0
|
||||
|
||||
contentItem: Label {
|
||||
height: Kirigami.Units.largeSpacing * 3
|
||||
text: modelData.reaction + (modelData.count > 1 ? " " + modelData.count : "")
|
||||
font.family: "emoji"
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
radius: height / 2
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
ToolTip {
|
||||
visible: parent.containsMouse
|
||||
font.family: Kirigami.Theme.defaultFont.family + ", emoji"
|
||||
text: {
|
||||
var text = "";
|
||||
|
||||
for (var i = 0; i < modelData.authors.length; i++) {
|
||||
if (i === modelData.authors.length - 1 && i !== 0) {
|
||||
text += i18nc("Seperate the usernames of users", " and ")
|
||||
} else if (i !== 0) {
|
||||
text += ", "
|
||||
}
|
||||
|
||||
text += modelData.authors[i].displayName
|
||||
}
|
||||
|
||||
text = i18ncp("%1 is the users who reacted and %2 the emoji that was given", "%2 reacted with %3", "%2 reacted with %3", modelData.authors.length, text, modelData.reaction)
|
||||
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: currentRoom.toggleReaction(eventId, modelData.reaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
imports/NeoChat/Component/Timeline/SectionDelegate.qml
Normal file
18
imports/NeoChat/Component/Timeline/SectionDelegate.qml
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
*
|
||||
* SPDX-LicenseIdentifier: GPL-3.0-or-later
|
||||
*/
|
||||
import QtQuick 2.12
|
||||
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
Kirigami.Heading {
|
||||
level: 4
|
||||
text: section + " • " + Qt.formatTime(time)
|
||||
font.capitalization: Font.AllUppercase
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
topPadding: Kirigami.Units.largeSpacing * 2
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
}
|
||||
59
imports/NeoChat/Component/Timeline/StateDelegate.qml
Normal file
59
imports/NeoChat/Component/Timeline/StateDelegate.qml
Normal file
@@ -0,0 +1,59 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtQuick.Controls.Material 2.12
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Dialog 2.0
|
||||
import NeoChat.Effect 2.0
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
|
||||
Item {
|
||||
Layout.minimumWidth: Kirigami.Units.iconSizes.medium
|
||||
Layout.preferredHeight: 1
|
||||
}
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
||||
|
||||
name: author.displayName
|
||||
source: author.avatarMediaId ? "image://mxc/" + author.avatarMediaId : ""
|
||||
color: author.color
|
||||
|
||||
Component {
|
||||
id: userDetailDialog
|
||||
|
||||
UserDetailDialog {}
|
||||
}
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
circular: true
|
||||
|
||||
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: author.displayName
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: display
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
font.weight: Font.Medium
|
||||
|
||||
wrapMode: Label.Wrap
|
||||
onLinkActivated: Qt.openUrlExternally(link)
|
||||
}
|
||||
}
|
||||
14
imports/NeoChat/Component/Timeline/TextDelegate.qml
Normal file
14
imports/NeoChat/Component/Timeline/TextDelegate.qml
Normal file
@@ -0,0 +1,14 @@
|
||||
import QtQuick 2.12
|
||||
|
||||
import org.kde.kirigami 2.4 as Kirigami
|
||||
|
||||
Text {
|
||||
text: "<style>pre {white-space: pre-wrap} a{color: " + Kirigami.Theme.linkColor + ";} .user-pill{}</style>" + display
|
||||
|
||||
font.family: Kirigami.Theme.defaultFont.family + ", emoji"
|
||||
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
textFormat: Text.RichText
|
||||
}
|
||||
|
||||
24
imports/NeoChat/Component/Timeline/TimelineContainer.qml
Normal file
24
imports/NeoChat/Component/Timeline/TimelineContainer.qml
Normal file
@@ -0,0 +1,24 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12 as Controls
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
import org.kde.kirigami 2.4 as Kirigami
|
||||
|
||||
Item {
|
||||
default property alias innerObject : column.children
|
||||
|
||||
height: column.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: column
|
||||
x: horizontalPadding
|
||||
width: parent.width - Kirigami.Units.largeSpacing * 2
|
||||
|
||||
SectionDelegate {
|
||||
Layout.maximumWidth: parent.width
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
visible: showSection
|
||||
}
|
||||
}
|
||||
}
|
||||
302
imports/NeoChat/Component/Timeline/VideoDelegate.qml
Normal file
302
imports/NeoChat/Component/Timeline/VideoDelegate.qml
Normal file
@@ -0,0 +1,302 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtQuick.Controls.Material 2.12
|
||||
import QtGraphicalEffects 1.0
|
||||
import QtMultimedia 5.12
|
||||
import Qt.labs.platform 1.0 as Platform
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
import org.kde.neochat 0.1
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Dialog 2.0
|
||||
import NeoChat.Menu.Timeline 2.0
|
||||
import NeoChat.Effect 2.0
|
||||
import NeoChat.Font 0.1
|
||||
|
||||
RowLayout {
|
||||
readonly property bool avatarVisible: showAuthor && !sentByMe
|
||||
readonly property bool sentByMe: author.isLocalUser
|
||||
|
||||
property bool openOnFinished: false
|
||||
property bool playOnFinished: false
|
||||
readonly property bool downloaded: progressInfo && progressInfo.completed
|
||||
|
||||
property bool supportStreaming: true
|
||||
|
||||
id: root
|
||||
|
||||
spacing: 4
|
||||
|
||||
z: -5
|
||||
|
||||
onDownloadedChanged: {
|
||||
if (downloaded) {
|
||||
vid.source = progressInfo.localPath
|
||||
}
|
||||
|
||||
if (downloaded && openOnFinished) {
|
||||
openSavedFile()
|
||||
openOnFinished = false
|
||||
}
|
||||
if (downloaded && playOnFinished) {
|
||||
playSavedFile()
|
||||
playOnFinished = false
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
|
||||
visible: avatarVisible
|
||||
name: author.displayName
|
||||
source: author.avatarMediaId ? "image://mxc/" + author.avatarMediaId : ""
|
||||
color: author.color
|
||||
|
||||
Component {
|
||||
id: userDetailDialog
|
||||
|
||||
UserDetailDialog {}
|
||||
}
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
circular: true
|
||||
|
||||
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
|
||||
visible: !(sentByMe || avatarVisible)
|
||||
}
|
||||
|
||||
Video {
|
||||
readonly property int maxWidth: messageListView.width - (!sentByMe ? 36 + root.spacing : 0) - 48
|
||||
|
||||
Layout.preferredWidth: content.info.w > maxWidth ? maxWidth : content.info.w
|
||||
Layout.preferredHeight: content.info.w > maxWidth ? (content.info.h / content.info.w * maxWidth) : content.info.h
|
||||
|
||||
id: vid
|
||||
|
||||
loops: MediaPlayer.Infinite
|
||||
|
||||
fillMode: VideoOutput.PreserveAspectFit
|
||||
|
||||
Component.onCompleted: {
|
||||
if (downloaded) {
|
||||
source = progressInfo.localPath
|
||||
} else {
|
||||
source = currentRoom.urlToMxcUrl(content.url)
|
||||
}
|
||||
}
|
||||
|
||||
onDurationChanged: {
|
||||
if (!duration) {
|
||||
supportStreaming = false;
|
||||
}
|
||||
}
|
||||
|
||||
onErrorChanged: {
|
||||
if (error != MediaPlayer.NoError) {
|
||||
supportStreaming = false;
|
||||
}
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: vid.width
|
||||
height: vid.height
|
||||
radius: 18
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
readonly property bool isThumbnail: content.info.thumbnail_info && content.thumbnailMediaId
|
||||
readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
visible: isThumbnail && (vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError)
|
||||
|
||||
source: "image://mxc/" + (isThumbnail ? content.thumbnailMediaId : "")
|
||||
|
||||
sourceSize.width: info.w
|
||||
sourceSize.height: info.h
|
||||
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
|
||||
visible: vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError
|
||||
color: "white"
|
||||
text: "Video"
|
||||
font.pixelSize: 16
|
||||
|
||||
padding: 8
|
||||
|
||||
background: Rectangle {
|
||||
radius: height / 2
|
||||
color: "black"
|
||||
opacity: 0.3
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 8
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 8
|
||||
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 4
|
||||
|
||||
contentItem: RowLayout {
|
||||
Label {
|
||||
text: Qt.formatTime(time)
|
||||
color: "white"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
Label {
|
||||
text: author.displayName
|
||||
color: "white"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
radius: height / 2
|
||||
color: "black"
|
||||
opacity: 0.3
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
|
||||
visible: progressInfo.active && !downloaded
|
||||
|
||||
color: "#BB000000"
|
||||
|
||||
ProgressBar {
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: parent.width * 0.8
|
||||
|
||||
from: 0
|
||||
to: progressInfo.total
|
||||
value: progressInfo.progress
|
||||
}
|
||||
}
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
id: messageMouseArea
|
||||
|
||||
onPrimaryClicked: {
|
||||
if (supportStreaming || progressInfo.completed) {
|
||||
if (vid.playbackState == MediaPlayer.PlayingState) {
|
||||
vid.pause()
|
||||
} else {
|
||||
vid.play()
|
||||
}
|
||||
} else {
|
||||
downloadAndPlay()
|
||||
}
|
||||
}
|
||||
|
||||
onSecondaryClicked: {
|
||||
var contextMenu = imageDelegateContextMenu.createObject(root)
|
||||
contextMenu.viewSource.connect(function() {
|
||||
messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open()
|
||||
})
|
||||
contextMenu.downloadAndOpen.connect(downloadAndOpen)
|
||||
contextMenu.saveFileAs.connect(saveFileAs)
|
||||
contextMenu.reply.connect(function() {
|
||||
roomPanelInput.replyModel = Object.assign({}, model)
|
||||
roomPanelInput.isReply = true
|
||||
roomPanelInput.focus()
|
||||
})
|
||||
contextMenu.redact.connect(function() {
|
||||
currentRoom.redactEvent(eventId)
|
||||
})
|
||||
contextMenu.popup()
|
||||
}
|
||||
|
||||
Component {
|
||||
id: messageSourceDialog
|
||||
|
||||
MessageSourceDialog {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: openFolderDialog
|
||||
|
||||
OpenFolderDialog {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: imageDelegateContextMenu
|
||||
|
||||
FileDelegateContextMenu {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveFileAs() {
|
||||
var folderDialog = openFolderDialog.createObject(ApplicationWindow.overlay)
|
||||
|
||||
folderDialog.chosen.connect(function(path) {
|
||||
if (!path) return
|
||||
|
||||
currentRoom.downloadFile(eventId, path + "/" + currentRoom.fileNameToDownload(eventId))
|
||||
})
|
||||
|
||||
folderDialog.open()
|
||||
}
|
||||
|
||||
function downloadAndOpen()
|
||||
{
|
||||
if (downloaded) openSavedFile()
|
||||
else
|
||||
{
|
||||
openOnFinished = true
|
||||
currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
|
||||
}
|
||||
}
|
||||
|
||||
function downloadAndPlay()
|
||||
{
|
||||
if (downloaded) playSavedFile()
|
||||
else
|
||||
{
|
||||
playOnFinished = true
|
||||
currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
|
||||
}
|
||||
}
|
||||
|
||||
function openSavedFile()
|
||||
{
|
||||
if (Qt.openUrlExternally(progressInfo.localPath)) return;
|
||||
if (Qt.openUrlExternally(progressInfo.localDir)) return;
|
||||
}
|
||||
|
||||
function playSavedFile()
|
||||
{
|
||||
vid.stop()
|
||||
vid.play()
|
||||
}
|
||||
}
|
||||
11
imports/NeoChat/Component/Timeline/qmldir
Normal file
11
imports/NeoChat/Component/Timeline/qmldir
Normal file
@@ -0,0 +1,11 @@
|
||||
module NeoChat.Component.Timeline
|
||||
TimelineContainer 2.0 TimelineContainer.qml
|
||||
MessageDelegate 2.0 MessageDelegate.qml
|
||||
TextDelegate 2.0 TextDelegate.qml
|
||||
StateDelegate 2.0 StateDelegate.qml
|
||||
SectionDelegate 2.0 SectionDelegate.qml
|
||||
ImageDelegate 2.0 ImageDelegate.qml
|
||||
FileDelegate 2.0 FileDelegate.qml
|
||||
VideoDelegate 2.0 VideoDelegate.qml
|
||||
ReactionDelegate 2.0 ReactionDelegate.qml
|
||||
AudioDelegate 2.0 AudioDelegate.qml
|
||||
10
imports/NeoChat/Component/qmldir
Normal file
10
imports/NeoChat/Component/qmldir
Normal file
@@ -0,0 +1,10 @@
|
||||
module NeoChat.Component
|
||||
AutoMouseArea 2.0 AutoMouseArea.qml
|
||||
MaterialIcon 2.0 MaterialIcon.qml
|
||||
SideNavButton 2.0 SideNavButton.qml
|
||||
ScrollHelper 2.0 ScrollHelper.qml
|
||||
AutoListView 2.0 AutoListView.qml
|
||||
AutoTextField 2.0 AutoTextField.qml
|
||||
FullScreenImage 2.0 FullScreenImage.qml
|
||||
AutoRectangle 2.0 AutoRectangle.qml
|
||||
ChatTextInput 2.0 ChatTextInput.qml
|
||||
50
imports/NeoChat/Dialog/AcceptInvitationDialog.qml
Normal file
50
imports/NeoChat/Dialog/AcceptInvitationDialog.qml
Normal file
@@ -0,0 +1,50 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
Dialog {
|
||||
property var room
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: 360
|
||||
|
||||
id: root
|
||||
|
||||
title: "Invitation Received"
|
||||
modal: true
|
||||
|
||||
contentItem: Label {
|
||||
text: "Accept this invitation?"
|
||||
}
|
||||
|
||||
footer: DialogButtonBox {
|
||||
Button {
|
||||
text: "Accept"
|
||||
flat: true
|
||||
|
||||
onClicked: {
|
||||
room.acceptInvitation()
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Reject"
|
||||
flat: true
|
||||
|
||||
onClicked: {
|
||||
room.forget()
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Cancel"
|
||||
flat: true
|
||||
|
||||
onClicked: close()
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: destroy()
|
||||
}
|
||||
|
||||
350
imports/NeoChat/Dialog/AccountDetailDialog.qml
Normal file
350
imports/NeoChat/Dialog/AccountDetailDialog.qml
Normal file
@@ -0,0 +1,350 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Effect 2.0
|
||||
|
||||
import org.kde.neochat 0.1
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
Dialog {
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: 480
|
||||
|
||||
id: root
|
||||
|
||||
contentItem: Column {
|
||||
id: detailColumn
|
||||
|
||||
spacing: 0
|
||||
|
||||
ListView {
|
||||
width: parent.width
|
||||
height: 48
|
||||
|
||||
clip: true
|
||||
|
||||
orientation: ListView.Horizontal
|
||||
|
||||
spacing: 16
|
||||
|
||||
model: AccountListModel{ }
|
||||
|
||||
delegate: Kirigami.Avatar {
|
||||
width: 48
|
||||
height: 48
|
||||
|
||||
source: urser.avatarMediaId ? "image://mxc/" + user.avatarMediaId : ""
|
||||
name: user.displayName ?? ""
|
||||
|
||||
Menu {
|
||||
id: contextMenu
|
||||
|
||||
MenuItem {
|
||||
text: "Mark all as read"
|
||||
|
||||
onClicked: Controller.markAllMessagesAsRead(connection)
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: "Logout"
|
||||
|
||||
onClicked: Controller.logout(connection)
|
||||
}
|
||||
}
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
circular: true
|
||||
|
||||
onPrimaryClicked: Controller.connection = connection
|
||||
onSecondaryClicked: contextMenu.popup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
|
||||
MenuSeparator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
Layout.preferredWidth: 48
|
||||
Layout.preferredHeight: 48
|
||||
|
||||
contentItem: MaterialIcon {
|
||||
icon: "\ue145"
|
||||
color: MPalette.lighter
|
||||
}
|
||||
|
||||
onClicked: loginDialog.createObject(ApplicationWindow.overlay).open()
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
width: parent.width
|
||||
|
||||
contentItem: RowLayout {
|
||||
MaterialIcon {
|
||||
Layout.preferredWidth: 48
|
||||
Layout.preferredHeight: 48
|
||||
|
||||
color: MPalette.foreground
|
||||
icon: "\ue5d2"
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: MPalette.foreground
|
||||
text: "Explore Rooms"
|
||||
}
|
||||
}
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
onPrimaryClicked: {
|
||||
joinRoomDialog.createObject(ApplicationWindow.overlay, {"connection": Controller.connection}).open()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
width: parent.width
|
||||
|
||||
contentItem: RowLayout {
|
||||
MaterialIcon {
|
||||
Layout.preferredWidth: 48
|
||||
Layout.preferredHeight: 48
|
||||
|
||||
color: MPalette.foreground
|
||||
icon: "\ue7ff"
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: MPalette.foreground
|
||||
text: "Start a Chat"
|
||||
}
|
||||
}
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
onPrimaryClicked: {
|
||||
startChatDialog.createObject(ApplicationWindow.overlay, {"connection": Controller.connection}).open()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
width: parent.width
|
||||
|
||||
contentItem: RowLayout {
|
||||
MaterialIcon {
|
||||
Layout.preferredWidth: 48
|
||||
Layout.preferredHeight: 48
|
||||
|
||||
color: MPalette.foreground
|
||||
icon: "\ue7fc"
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: MPalette.foreground
|
||||
text: "Create a Room"
|
||||
}
|
||||
}
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
onPrimaryClicked: createRoomDialog.createObject(ApplicationWindow.overlay).open()
|
||||
}
|
||||
}
|
||||
|
||||
MenuSeparator {
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Control {
|
||||
width: parent.width
|
||||
|
||||
contentItem: RowLayout {
|
||||
MaterialIcon {
|
||||
Layout.preferredWidth: 48
|
||||
Layout.preferredHeight: 48
|
||||
|
||||
color: MPalette.foreground
|
||||
icon: "\ue3a9"
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: MPalette.foreground
|
||||
text: "Night Mode"
|
||||
}
|
||||
|
||||
Switch {
|
||||
id: darkThemeSwitch
|
||||
|
||||
checked: MSettings.darkTheme
|
||||
onCheckedChanged: MSettings.darkTheme = checked
|
||||
}
|
||||
}
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
onPrimaryClicked: darkThemeSwitch.checked = !darkThemeSwitch.checked
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
width: parent.width
|
||||
|
||||
contentItem: RowLayout {
|
||||
MaterialIcon {
|
||||
Layout.preferredWidth: 48
|
||||
Layout.preferredHeight: 48
|
||||
|
||||
color: MPalette.foreground
|
||||
icon: "\ue8f8"
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: MPalette.foreground
|
||||
text: "Show Join/Leave"
|
||||
}
|
||||
|
||||
Switch {
|
||||
id: showJoinLeaveSwitch
|
||||
|
||||
checked: MSettings.value("UI/show_joinleave", true)
|
||||
onCheckedChanged: MSettings.setValue("UI/show_joinleave", checked)
|
||||
}
|
||||
}
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
onPrimaryClicked: showJoinLeaveSwitch.checked = !showJoinLeaveSwitch.checked
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
width: parent.width
|
||||
|
||||
contentItem: RowLayout {
|
||||
MaterialIcon {
|
||||
Layout.preferredWidth: 48
|
||||
Layout.preferredHeight: 48
|
||||
|
||||
color: MPalette.foreground
|
||||
icon: "\ue5d2"
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: MPalette.foreground
|
||||
text: "Enable System Tray"
|
||||
}
|
||||
|
||||
Switch {
|
||||
id: trayIconSwitch
|
||||
|
||||
checked: MSettings.showTray
|
||||
onCheckedChanged: MSettings.showTray = checked
|
||||
}
|
||||
}
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
onPrimaryClicked: trayIconSwitch.checked = !trayIconSwitch.checked
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
width: parent.width
|
||||
|
||||
contentItem: RowLayout {
|
||||
MaterialIcon {
|
||||
Layout.preferredWidth: 48
|
||||
Layout.preferredHeight: 48
|
||||
|
||||
color: MPalette.foreground
|
||||
icon: "\ue7f5"
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: MPalette.foreground
|
||||
text: "Enable Notifications"
|
||||
}
|
||||
|
||||
Switch {
|
||||
id: notificationsSwitch
|
||||
|
||||
checked: MSettings.showNotification
|
||||
onCheckedChanged: MSettings.showNotification = checked
|
||||
}
|
||||
}
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
onPrimaryClicked: notificationsSwitch.checked = !notificationsSwitch.checked
|
||||
}
|
||||
}
|
||||
|
||||
MenuSeparator {
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Control {
|
||||
width: parent.width
|
||||
|
||||
contentItem: RowLayout {
|
||||
MaterialIcon {
|
||||
Layout.preferredWidth: 48
|
||||
Layout.preferredHeight: 48
|
||||
|
||||
color: MPalette.foreground
|
||||
icon: "\ue167"
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: MPalette.foreground
|
||||
text: "Font Family"
|
||||
}
|
||||
}
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
onPrimaryClicked: fontFamilyDialog.createObject(ApplicationWindow.overlay).open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: destroy()
|
||||
}
|
||||
40
imports/NeoChat/Dialog/CreateRoomDialog.qml
Normal file
40
imports/NeoChat/Dialog/CreateRoomDialog.qml
Normal file
@@ -0,0 +1,40 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
|
||||
import org.kde.neochat 0.1
|
||||
|
||||
Dialog {
|
||||
anchors.centerIn: parent
|
||||
width: 360
|
||||
|
||||
id: root
|
||||
|
||||
title: "Create a Room"
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
AutoTextField {
|
||||
Layout.fillWidth: true
|
||||
|
||||
id: roomNameField
|
||||
|
||||
placeholderText: "Room Name"
|
||||
}
|
||||
|
||||
AutoTextField {
|
||||
Layout.fillWidth: true
|
||||
|
||||
id: roomTopicField
|
||||
|
||||
placeholderText: "Room Topic"
|
||||
}
|
||||
}
|
||||
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
|
||||
onAccepted: Controller.createRoom(Controller.connection, roomNameField.text, roomTopicField.text)
|
||||
|
||||
onClosed: destroy()
|
||||
}
|
||||
30
imports/NeoChat/Dialog/FontFamilyDialog.qml
Normal file
30
imports/NeoChat/Dialog/FontFamilyDialog.qml
Normal file
@@ -0,0 +1,30 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
Dialog {
|
||||
anchors.centerIn: parent
|
||||
width: 360
|
||||
|
||||
id: root
|
||||
|
||||
title: "Enter Font Family"
|
||||
|
||||
contentItem: AutoTextField {
|
||||
Layout.fillWidth: true
|
||||
|
||||
id:fontFamilyField
|
||||
|
||||
text: MSettings.fontFamily
|
||||
placeholderText: "Font Family"
|
||||
}
|
||||
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
|
||||
onAccepted: MSettings.fontFamily = fontFamilyField.text
|
||||
|
||||
onClosed: destroy()
|
||||
}
|
||||
170
imports/NeoChat/Dialog/InviteUserDialog.qml
Normal file
170
imports/NeoChat/Dialog/InviteUserDialog.qml
Normal file
@@ -0,0 +1,170 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Effect 2.0
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
import org.kde.neochat 0.1
|
||||
|
||||
Dialog {
|
||||
property var room
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: 360
|
||||
height: Math.min(window.height - 100, 640)
|
||||
|
||||
id: root
|
||||
|
||||
title: "Invite a User"
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
AutoTextField {
|
||||
property bool isUserID: text.match(/@(.+):(.+)/g)
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
id: identifierField
|
||||
|
||||
placeholderText: "Find a user..."
|
||||
|
||||
onAccepted: {
|
||||
userDictListModel.search()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
visible: identifierField.isUserID
|
||||
|
||||
text: "Add"
|
||||
highlighted: true
|
||||
|
||||
onClicked: {
|
||||
room.inviteToRoom(identifierField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MenuSeparator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
AutoListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
id: userDictListView
|
||||
|
||||
clip: true
|
||||
|
||||
spacing: 4
|
||||
|
||||
model: UserDirectoryListModel {
|
||||
id: userDictListModel
|
||||
|
||||
connection: root.room.connection
|
||||
keyword: identifierField.text
|
||||
}
|
||||
|
||||
delegate: Control {
|
||||
property bool inRoom: room && room.containsUser(userID)
|
||||
|
||||
width: userDictListView.width
|
||||
height: 48
|
||||
|
||||
id: delegate
|
||||
|
||||
padding: 8
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 8
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: height
|
||||
Layout.fillHeight: true
|
||||
|
||||
source: author.avatarMediaId ? "image://mxc/" + author.avatarMediaId : ""
|
||||
name: name
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
text: name
|
||||
color: MPalette.foreground
|
||||
font.pixelSize: 13
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
text: userID
|
||||
color: MPalette.lighter
|
||||
font.pixelSize: 10
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
Layout.preferredWidth: 32
|
||||
Layout.preferredHeight: 32
|
||||
|
||||
visible: inRoom
|
||||
|
||||
background: RippleEffect {
|
||||
circular: true
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
Layout.preferredWidth: 32
|
||||
Layout.preferredHeight: 32
|
||||
|
||||
visible: !inRoom
|
||||
|
||||
background: RippleEffect {
|
||||
circular: true
|
||||
|
||||
onClicked: {
|
||||
room.inviteToRoom(userID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
|
||||
visible: userDictListView.count < 1
|
||||
|
||||
text: "No users available"
|
||||
color: MPalette.foreground
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: destroy()
|
||||
}
|
||||
269
imports/NeoChat/Dialog/JoinRoomDialog.qml
Normal file
269
imports/NeoChat/Dialog/JoinRoomDialog.qml
Normal file
@@ -0,0 +1,269 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Effect 2.0
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
import org.kde.neochat 0.1
|
||||
|
||||
Dialog {
|
||||
property var connection
|
||||
|
||||
property string keyword
|
||||
property string server
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: 480
|
||||
height: Math.min(window.height - 100, 800)
|
||||
|
||||
id: root
|
||||
|
||||
title: "Explore Rooms"
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
AutoTextField {
|
||||
property bool isRoomAlias: text.match(/#(.+):(.+)/g)
|
||||
property var room: isRoomAlias ? connection.roomByAlias(text) : null
|
||||
property bool isJoined: room != null
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
id: identifierField
|
||||
|
||||
placeholderText: "Find a room..."
|
||||
|
||||
onEditingFinished: {
|
||||
keyword = text
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: joinButton
|
||||
|
||||
visible: identifierField.isRoomAlias
|
||||
|
||||
text: identifierField.isJoined ? "View" : "Join"
|
||||
highlighted: true
|
||||
flat: identifierField.isJoined
|
||||
|
||||
onClicked: {
|
||||
if (identifierField.isJoined) {
|
||||
roomListForm.joinRoom(identifierField.room)
|
||||
} else {
|
||||
Controller.joinRoom(connection, identifierField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
Layout.maximumWidth: 120
|
||||
|
||||
id: serverField
|
||||
|
||||
editable: currentIndex == 1
|
||||
|
||||
model: ["Local", "Global", "matrix.org"]
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
if (currentIndex == 0) {
|
||||
server = ""
|
||||
} else if (currentIndex == 2) {
|
||||
server = "matrix.org"
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
if (currentIndex == 1) {
|
||||
server = editText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MenuSeparator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
AutoListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
id: publicRoomsListView
|
||||
|
||||
clip: true
|
||||
|
||||
spacing: 4
|
||||
|
||||
model: PublicRoomListModel {
|
||||
id: publicRoomListModel
|
||||
|
||||
connection: root.connection
|
||||
server: root.server
|
||||
keyword: root.keyword
|
||||
}
|
||||
|
||||
delegate: Control {
|
||||
width: publicRoomsListView.width
|
||||
height: 48
|
||||
|
||||
padding: 8
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 8
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: height
|
||||
Layout.fillHeight: true
|
||||
|
||||
source: model.avatarMediaId ? "image://mxc/" + model.avatarMediaId : ""
|
||||
hint: name
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
spacing: 4
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
text: name
|
||||
color: MPalette.foreground
|
||||
font.pixelSize: 13
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: allowGuests
|
||||
|
||||
text: "GUESTS CAN JOIN"
|
||||
color: MPalette.lighter
|
||||
font.pixelSize: 10
|
||||
padding: 4
|
||||
|
||||
background: Rectangle {
|
||||
color: MPalette.banner
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: worldReadable
|
||||
|
||||
text: "WORLD READABLE"
|
||||
color: MPalette.lighter
|
||||
font.pixelSize: 10
|
||||
padding: 4
|
||||
|
||||
background: Rectangle {
|
||||
color: MPalette.banner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
visible: text
|
||||
|
||||
text: topic ? topic.replace(/(\r\n\t|\n|\r\t)/gm," ") : ""
|
||||
color: MPalette.lighter
|
||||
font.pixelSize: 10
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
Layout.preferredWidth: 16
|
||||
Layout.preferredHeight: 16
|
||||
|
||||
icon: "\ue7fc"
|
||||
color: MPalette.lighter
|
||||
font.pixelSize: 16
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.preferredWidth: 36
|
||||
|
||||
text: memberCount
|
||||
color: MPalette.lighter
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
Control {
|
||||
Layout.preferredWidth: 32
|
||||
Layout.preferredHeight: 32
|
||||
|
||||
visible: isJoined
|
||||
|
||||
contentItem: MaterialIcon {
|
||||
icon: "\ue89e"
|
||||
color: MPalette.lighter
|
||||
font.pixelSize: 20
|
||||
}
|
||||
|
||||
background: RippleEffect {
|
||||
circular: true
|
||||
|
||||
onClicked: {
|
||||
roomListForm.joinRoom(connection.room(roomID))
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
Layout.preferredWidth: 32
|
||||
Layout.preferredHeight: 32
|
||||
|
||||
visible: !isJoined
|
||||
|
||||
contentItem: MaterialIcon {
|
||||
icon: "\ue7f0"
|
||||
color: MPalette.lighter
|
||||
font.pixelSize: 20
|
||||
}
|
||||
|
||||
background: RippleEffect {
|
||||
circular: true
|
||||
|
||||
onClicked: {
|
||||
Controller.joinRoom(connection, roomID)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
|
||||
onContentYChanged: {
|
||||
if(publicRoomListModel.hasMore && contentHeight - contentY < publicRoomsListView.height + 200)
|
||||
publicRoomListModel.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: destroy()
|
||||
}
|
||||
27
imports/NeoChat/Dialog/MessageSourceDialog.qml
Normal file
27
imports/NeoChat/Dialog/MessageSourceDialog.qml
Normal file
@@ -0,0 +1,27 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
Popup {
|
||||
property string sourceText
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: 480
|
||||
|
||||
id: root
|
||||
|
||||
modal: true
|
||||
padding: 16
|
||||
|
||||
closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside
|
||||
|
||||
contentItem: ScrollView {
|
||||
clip: true
|
||||
|
||||
Label {
|
||||
text: sourceText
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: destroy()
|
||||
}
|
||||
|
||||
12
imports/NeoChat/Dialog/OpenFileDialog.qml
Normal file
12
imports/NeoChat/Dialog/OpenFileDialog.qml
Normal file
@@ -0,0 +1,12 @@
|
||||
import QtQuick 2.12
|
||||
import Qt.labs.platform 1.1
|
||||
|
||||
FileDialog {
|
||||
signal chosen(string path)
|
||||
|
||||
id: root
|
||||
|
||||
title: "Please choose a file"
|
||||
|
||||
onAccepted: chosen(file)
|
||||
}
|
||||
12
imports/NeoChat/Dialog/OpenFolderDialog.qml
Normal file
12
imports/NeoChat/Dialog/OpenFolderDialog.qml
Normal file
@@ -0,0 +1,12 @@
|
||||
import QtQuick 2.12
|
||||
import Qt.labs.platform 1.1
|
||||
|
||||
FolderDialog {
|
||||
signal chosen(string path)
|
||||
|
||||
id: root
|
||||
|
||||
title: "Please choose a folder"
|
||||
|
||||
onAccepted: chosen(folder)
|
||||
}
|
||||
298
imports/NeoChat/Dialog/RoomSettingsDialog.qml
Normal file
298
imports/NeoChat/Dialog/RoomSettingsDialog.qml
Normal file
@@ -0,0 +1,298 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Effect 2.0
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
import org.kde.neochat 0.1
|
||||
|
||||
Dialog {
|
||||
property var room
|
||||
|
||||
readonly property bool canChangeAvatar: room.canSendState("m.room.avatar")
|
||||
readonly property bool canChangeName: room.canSendState("m.room.name")
|
||||
readonly property bool canChangeTopic: room.canSendState("m.room.topic")
|
||||
readonly property bool canChangeCanonicalAlias: room.canSendState("m.room.canonical_alias")
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: 480
|
||||
height: window.height * 0.9
|
||||
|
||||
id: root
|
||||
|
||||
title: "Room Settings - " + room.displayName
|
||||
modal: true
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 16
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: 72
|
||||
Layout.preferredHeight: 72
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
name: room.displayName
|
||||
source: room.avatarMediaId ? "image://mxc/" + room.avatarMediaId : ""
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
circular: true
|
||||
|
||||
enabled: canChangeAvatar
|
||||
|
||||
onClicked: {
|
||||
var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay)
|
||||
|
||||
fileDialog.chosen.connect(function(path) {
|
||||
if (!path) return
|
||||
|
||||
room.changeAvatar(path)
|
||||
})
|
||||
|
||||
fileDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 4
|
||||
|
||||
AutoTextField {
|
||||
Layout.fillWidth: true
|
||||
|
||||
id: roomNameField
|
||||
|
||||
text: room.name
|
||||
placeholderText: "Room Name"
|
||||
|
||||
enabled: canChangeName
|
||||
}
|
||||
|
||||
AutoTextField {
|
||||
Layout.fillWidth: true
|
||||
|
||||
id: roomTopicField
|
||||
|
||||
text: room.topic
|
||||
placeholderText: "Room Topic"
|
||||
|
||||
enabled: canChangeTopic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
visible: canChangeName || canChangeTopic
|
||||
|
||||
text: "Save"
|
||||
highlighted: true
|
||||
|
||||
onClicked: {
|
||||
if (room.name != roomNameField.text) {
|
||||
room.setName(roomNameField.text)
|
||||
}
|
||||
|
||||
if (room.topic != roomTopicField.text) {
|
||||
room.setTopic(roomTopicField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MenuSeparator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
id: scrollview
|
||||
|
||||
clip: true
|
||||
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
ColumnLayout {
|
||||
width: scrollview.width
|
||||
|
||||
Control {
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: room.predecessorId && room.connection.room(room.predecessorId)
|
||||
|
||||
padding: 8
|
||||
|
||||
contentItem: RowLayout {
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
font.bold: true
|
||||
color: MPalette.foreground
|
||||
text: "This room continues another conversation."
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: MPalette.lighter
|
||||
text: "Click here to see older messages."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: MPalette.banner
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: {
|
||||
roomListForm.enteredRoom = Controller.connection.room(room.predecessorId)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: room.successorId && room.connection.room(room.successorId)
|
||||
|
||||
padding: 8
|
||||
|
||||
contentItem: RowLayout {
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
font.bold: true
|
||||
color: MPalette.foreground
|
||||
text: "This room has been replaced."
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: MPalette.lighter
|
||||
text: "The conversation continues here."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: MPalette.banner
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: {
|
||||
roomListForm.enteredRoom = Controller.connection.room(room.successorId)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Label {
|
||||
Layout.preferredWidth: 100
|
||||
|
||||
wrapMode: Label.Wrap
|
||||
text: "Canonical Alias"
|
||||
color: MPalette.lighter
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
Layout.fillWidth: true
|
||||
|
||||
id: canonicalAliasComboBox
|
||||
|
||||
enabled: canChangeCanonicalAlias
|
||||
|
||||
model: room.aliases
|
||||
|
||||
currentIndex: room.aliases.indexOf(room.canonicalAlias)
|
||||
onCurrentIndexChanged: {
|
||||
if (room.canonicalAlias != room.aliases[currentIndex]) {
|
||||
room.setCanonicalAlias(room.aliases[currentIndex])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: room.altAliases && room.altAliases.length
|
||||
|
||||
Label {
|
||||
Layout.preferredWidth: 100
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
wrapMode: Label.Wrap
|
||||
text: "Alt Aliases"
|
||||
color: MPalette.lighter
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: room.altAliases
|
||||
|
||||
delegate: RowLayout {
|
||||
Layout.maximumWidth: parent.width
|
||||
|
||||
Label {
|
||||
text: modelData
|
||||
|
||||
font.pixelSize: 12
|
||||
color: MPalette.lighter
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
icon.name: ""
|
||||
onClicked: room.removeLocalAlias(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: openFileDialog
|
||||
|
||||
OpenFileDialog {}
|
||||
}
|
||||
|
||||
onClosed: destroy()
|
||||
}
|
||||
|
||||
181
imports/NeoChat/Dialog/StartChatDialog.qml
Normal file
181
imports/NeoChat/Dialog/StartChatDialog.qml
Normal file
@@ -0,0 +1,181 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Effect 2.0
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
import org.kde.neochat 0.1
|
||||
|
||||
Dialog {
|
||||
property var connection
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: 360
|
||||
height: Math.min(window.height - 100, 640)
|
||||
|
||||
id: root
|
||||
|
||||
title: "Start a Chat"
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
AutoTextField {
|
||||
property bool isUserID: text.match(/@(.+):(.+)/g)
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
id: identifierField
|
||||
|
||||
placeholderText: "Find a user..."
|
||||
|
||||
onAccepted: {
|
||||
userDictListModel.search()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
visible: identifierField.isUserID
|
||||
|
||||
text: "Chat"
|
||||
highlighted: true
|
||||
|
||||
onClicked: {
|
||||
Controller.createDirectChat(connection, identifierField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MenuSeparator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
AutoListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
id: userDictListView
|
||||
|
||||
clip: true
|
||||
|
||||
spacing: 4
|
||||
|
||||
model: UserDirectoryListModel {
|
||||
id: userDictListModel
|
||||
|
||||
connection: root.connection
|
||||
keyword: identifierField.text
|
||||
}
|
||||
|
||||
delegate: Control {
|
||||
width: userDictListView.width
|
||||
height: 48
|
||||
|
||||
padding: 8
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 8
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: height
|
||||
Layout.fillHeight: true
|
||||
|
||||
source: avatar
|
||||
name: name
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
text: name
|
||||
color: MPalette.foreground
|
||||
font.pixelSize: 13
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
text: userID
|
||||
color: MPalette.lighter
|
||||
font.pixelSize: 10
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
Layout.preferredWidth: 32
|
||||
Layout.preferredHeight: 32
|
||||
|
||||
visible: directChats != null
|
||||
|
||||
contentItem: MaterialIcon {
|
||||
icon: "\ue89e"
|
||||
color: MPalette.lighter
|
||||
font.pixelSize: 20
|
||||
}
|
||||
|
||||
background: RippleEffect {
|
||||
circular: true
|
||||
|
||||
onClicked: {
|
||||
roomListForm.joinRoom(connection.room(directChats[0]))
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
Layout.preferredWidth: 32
|
||||
Layout.preferredHeight: 32
|
||||
|
||||
contentItem: MaterialIcon {
|
||||
icon: "\ue7f0"
|
||||
color: MPalette.lighter
|
||||
font.pixelSize: 20
|
||||
}
|
||||
|
||||
background: RippleEffect {
|
||||
circular: true
|
||||
|
||||
onClicked: {
|
||||
Controller.createDirectChat(connection, userID)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
|
||||
visible: userDictListView.count < 1
|
||||
|
||||
text: "No users available"
|
||||
color: MPalette.foreground
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: destroy()
|
||||
}
|
||||
160
imports/NeoChat/Dialog/UserDetailDialog.qml
Normal file
160
imports/NeoChat/Dialog/UserDetailDialog.qml
Normal file
@@ -0,0 +1,160 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Effect 2.0
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
Dialog {
|
||||
property var room
|
||||
property var user
|
||||
|
||||
property string displayName: user.displayName
|
||||
property string avatarMediaId: user.avatarMediaId
|
||||
property string avatarUrl: user.avatarUrl
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: 360
|
||||
|
||||
id: root
|
||||
|
||||
modal: true
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 16
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: 72
|
||||
Layout.preferredHeight: 72
|
||||
|
||||
name: displayName
|
||||
source: avatarMediaId ? "image://mxc/" + avatarMediaId : ""
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
circular: true
|
||||
|
||||
onPrimaryClicked: {
|
||||
if (avatarMediaId) {
|
||||
fullScreenImage.createObject(parent, {"filename": displayName, "localPath": room.urlToMxcUrl(avatarUrl)}).showFullScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
text: displayName
|
||||
color: MPalette.foreground
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: "Online"
|
||||
color: MPalette.lighter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MenuSeparator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 8
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
text: user.id
|
||||
color: MPalette.accent
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
wrapMode: Label.Wrap
|
||||
text: "User ID"
|
||||
color: MPalette.lighter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MenuSeparator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Control {
|
||||
Layout.fillWidth: true
|
||||
|
||||
contentItem: RowLayout {
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
wrapMode: Label.Wrap
|
||||
text: room.connection.isIgnored(user) ? "Unignore this user" : "Ignore this user"
|
||||
|
||||
color: MPalette.accent
|
||||
}
|
||||
}
|
||||
|
||||
background: RippleEffect {
|
||||
onPrimaryClicked: {
|
||||
root.close()
|
||||
room.connection.isIgnored(user) ? room.connection.removeFromIgnoredUsers(user) : room.connection.addToIgnoredUsers(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
Layout.fillWidth: true
|
||||
|
||||
contentItem: RowLayout {
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
wrapMode: Label.Wrap
|
||||
text: "Kick this user"
|
||||
|
||||
color: MPalette.accent
|
||||
}
|
||||
}
|
||||
|
||||
background: RippleEffect {
|
||||
onPrimaryClicked: room.kickMember(user.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fullScreenImage
|
||||
|
||||
FullScreenImage {}
|
||||
}
|
||||
|
||||
onClosed: destroy()
|
||||
}
|
||||
|
||||
15
imports/NeoChat/Dialog/qmldir
Normal file
15
imports/NeoChat/Dialog/qmldir
Normal file
@@ -0,0 +1,15 @@
|
||||
module NeoChat.Dialog
|
||||
RoomSettingsDialog 2.0 RoomSettingsDialog.qml
|
||||
UserDetailDialog 2.0 UserDetailDialog.qml
|
||||
MessageSourceDialog 2.0 MessageSourceDialog.qml
|
||||
LoginDialog 2.0 LoginDialog.qml
|
||||
CreateRoomDialog 2.0 CreateRoomDialog.qml
|
||||
JoinRoomDialog 2.0 JoinRoomDialog.qml
|
||||
InviteUserDialog 2.0 InviteUserDialog.qml
|
||||
AcceptInvitationDialog 2.0 AcceptInvitationDialog.qml
|
||||
FontFamilyDialog 2.0 FontFamilyDialog.qml
|
||||
AccountDetailDialog 2.0 AccountDetailDialog.qml
|
||||
OpenFileDialog 2.0 OpenFileDialog.qml
|
||||
OpenFolderDialog 2.0 OpenFolderDialog.qml
|
||||
ImageClipboardDialog 2.0 ImageClipboardDialog.qml
|
||||
StartChatDialog 2.0 StartChatDialog.qml
|
||||
29
imports/NeoChat/Effect/CircleMask.qml
Normal file
29
imports/NeoChat/Effect/CircleMask.qml
Normal file
@@ -0,0 +1,29 @@
|
||||
import QtQuick 2.12
|
||||
import QtGraphicalEffects 1.0
|
||||
|
||||
Item {
|
||||
id: item
|
||||
|
||||
property alias source: mask.source
|
||||
|
||||
Rectangle {
|
||||
id: circleMask
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
smooth: true
|
||||
visible: false
|
||||
|
||||
radius: Math.max(width/2, height/2)
|
||||
}
|
||||
|
||||
OpacityMask {
|
||||
id: mask
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
maskSource: circleMask
|
||||
}
|
||||
}
|
||||
146
imports/NeoChat/Effect/ElevationEffect.qml
Normal file
146
imports/NeoChat/Effect/ElevationEffect.qml
Normal file
@@ -0,0 +1,146 @@
|
||||
import QtQuick 2.12
|
||||
import QtGraphicalEffects 1.0
|
||||
|
||||
/*!
|
||||
An effect for standard Material Design elevation shadows
|
||||
*/
|
||||
Item {
|
||||
id: effect
|
||||
|
||||
property var source
|
||||
readonly property Item sourceItem: source.sourceItem
|
||||
|
||||
property int elevation: 0
|
||||
|
||||
// Shadow details follow Material Design (taken from Angular Material)
|
||||
readonly property var _shadows: [
|
||||
[{offset: 0, blur: 0, spread: 0},
|
||||
{offset: 0, blur: 0, spread: 0},
|
||||
{offset: 0, blur: 0, spread: 0}],
|
||||
|
||||
[{offset: 1, blur: 3, spread: 0},
|
||||
{offset: 1, blur: 1, spread: 0},
|
||||
{offset: 2, blur: 1, spread: -1}],
|
||||
|
||||
[{offset: 1, blur: 5, spread: 0},
|
||||
{offset: 2, blur: 2, spread: 0},
|
||||
{offset: 3, blur: 1, spread: -2}],
|
||||
|
||||
[{offset: 1, blur: 8, spread: 0},
|
||||
{offset: 3, blur: 4, spread: 0},
|
||||
{offset: 3, blur: 3, spread: -2}],
|
||||
|
||||
[{offset: 2, blur: 4, spread: -1},
|
||||
{offset: 4, blur: 5, spread: 0},
|
||||
{offset: 1, blur: 10, spread: 0}],
|
||||
|
||||
[{offset: 3, blur: 5, spread: -1},
|
||||
{offset: 5, blur: 8, spread: 0},
|
||||
{offset: 1, blur: 14, spread: 0}],
|
||||
|
||||
[{offset: 3, blur: 5, spread: -1},
|
||||
{offset: 6, blur: 10, spread: 0},
|
||||
{offset: 1, blur: 18, spread: 0}],
|
||||
|
||||
[{offset: 4, blur: 5, spread: -2},
|
||||
{offset: 7, blur: 10, spread: 1},
|
||||
{offset: 2, blur: 16, spread: 1}],
|
||||
|
||||
[{offset: 5, blur: 5, spread: -3},
|
||||
{offset: 8, blur: 10, spread: 1},
|
||||
{offset: 3, blur: 14, spread: 2}],
|
||||
|
||||
[{offset: 5, blur: 6, spread: -3},
|
||||
{offset: 9, blur: 12, spread: 1},
|
||||
{offset: 3, blur: 16, spread: 2}],
|
||||
|
||||
[{offset: 6, blur: 6, spread: -3},
|
||||
{offset: 10, blur: 14, spread: 1},
|
||||
{offset: 4, blur: 18, spread: 3}],
|
||||
|
||||
[{offset: 6, blur: 7, spread: -4},
|
||||
{offset: 11, blur: 15, spread: 1},
|
||||
{offset: 4, blur: 20, spread: 3}],
|
||||
|
||||
[{offset: 7, blur: 8, spread: -4},
|
||||
{offset: 12, blur: 17, spread: 2},
|
||||
{offset: 5, blur: 22, spread: 4}],
|
||||
|
||||
[{offset: 7, blur: 8, spread: -4},
|
||||
{offset: 13, blur: 19, spread: 2},
|
||||
{offset: 5, blur: 24, spread: 4}],
|
||||
|
||||
[{offset: 7, blur: 9, spread: -4},
|
||||
{offset: 14, blur: 21, spread: 2},
|
||||
{offset: 5, blur: 26, spread: 4}],
|
||||
|
||||
[{offset: 8, blur: 9, spread: -5},
|
||||
{offset: 15, blur: 22, spread: 2},
|
||||
{offset: 6, blur: 28, spread: 5}],
|
||||
|
||||
[{offset: 8, blur: 10, spread: -5},
|
||||
{offset: 16, blur: 24, spread: 2},
|
||||
{offset: 6, blur: 30, spread: 5}],
|
||||
|
||||
[{offset: 8, blur: 11, spread: -5},
|
||||
{offset: 17, blur: 26, spread: 2},
|
||||
{offset: 6, blur: 32, spread: 5}],
|
||||
|
||||
[{offset: 9, blur: 11, spread: -5},
|
||||
{offset: 18, blur: 28, spread: 2},
|
||||
{offset: 7, blur: 34, spread: 6}],
|
||||
|
||||
[{offset: 9, blur: 12, spread: -6},
|
||||
{offset: 19, blur: 29, spread: 2},
|
||||
{offset: 7, blur: 36, spread: 6}],
|
||||
|
||||
[{offset: 10, blur: 13, spread: -6},
|
||||
{offset: 20, blur: 31, spread: 3},
|
||||
{offset: 8, blur: 38, spread: 7}],
|
||||
|
||||
[{offset: 10, blur: 13, spread: -6},
|
||||
{offset: 21, blur: 33, spread: 3},
|
||||
{offset: 8, blur: 40, spread: 7}],
|
||||
|
||||
[{offset: 10, blur: 14, spread: -6},
|
||||
{offset: 22, blur: 35, spread: 3},
|
||||
{offset: 8, blur: 42, spread: 7}],
|
||||
|
||||
[{offset: 11, blur: 14, spread: -7},
|
||||
{offset: 23, blur: 36, spread: 3},
|
||||
{offset: 9, blur: 44, spread: 8}],
|
||||
|
||||
[{offset: 11, blur: 15, spread: -7},
|
||||
{offset: 24, blur: 38, spread: 3},
|
||||
{offset: 9, blur: 46, spread: 8}]
|
||||
]
|
||||
|
||||
readonly property var _shadowColors: [
|
||||
Qt.rgba(0,0,0, 0.2),
|
||||
Qt.rgba(0,0,0, 0.14),
|
||||
Qt.rgba(0,0,0, 0.12)
|
||||
]
|
||||
|
||||
Repeater {
|
||||
model: _shadows[elevation]
|
||||
|
||||
delegate: RectangularGlow {
|
||||
anchors {
|
||||
centerIn: parent
|
||||
verticalCenterOffset: modelData.offset
|
||||
}
|
||||
|
||||
width: parent.width + 2 * modelData.spread
|
||||
height: parent.height + 2 * modelData.spread
|
||||
glowRadius: modelData.blur/2
|
||||
spread: 0.05
|
||||
color: _shadowColors[index]
|
||||
cornerRadius: modelData.blur + (effect.sourceItem.radius ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
ShaderEffect {
|
||||
anchors.fill: parent
|
||||
property alias source: effect.source;
|
||||
}
|
||||
}
|
||||
239
imports/NeoChat/Effect/RippleEffect.qml
Normal file
239
imports/NeoChat/Effect/RippleEffect.qml
Normal file
@@ -0,0 +1,239 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtGraphicalEffects 1.0
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
AutoMouseArea {
|
||||
id: ripple
|
||||
|
||||
property color color: MSettings.darkTheme ? Qt.rgba(255, 255, 255, 0.16) : Qt.rgba(0, 0, 0, 0.08)
|
||||
property bool circular: false
|
||||
property bool centered: false
|
||||
property bool focused
|
||||
property color focusColor: "transparent"
|
||||
property int focusWidth: width - 32
|
||||
property Item control
|
||||
|
||||
clip: true
|
||||
|
||||
Connections {
|
||||
target: control
|
||||
|
||||
onPressedChanged: {
|
||||
if (!control.pressed)
|
||||
__private.removeLastCircle()
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: {
|
||||
__private.createTapCircle(mouse.x, mouse.y)
|
||||
|
||||
if (control)
|
||||
mouse.accepted = false
|
||||
}
|
||||
|
||||
onReleased: __private.removeLastCircle()
|
||||
onCanceled: __private.removeLastCircle()
|
||||
|
||||
QtObject {
|
||||
id: __private
|
||||
|
||||
property int startRadius: 0
|
||||
property int endRadius
|
||||
property bool showFocus: true
|
||||
|
||||
property Item lastCircle
|
||||
|
||||
function createTapCircle(x, y) {
|
||||
endRadius = centered ? width/2 : radius(x, y) + 5
|
||||
showFocus = false
|
||||
|
||||
lastCircle = tapCircle.createObject(ripple, {
|
||||
"circleX": centered ? width/2 : x,
|
||||
"circleY": centered ? height/2 : y
|
||||
})
|
||||
}
|
||||
|
||||
function removeLastCircle() {
|
||||
if (lastCircle)
|
||||
lastCircle.removeCircle()
|
||||
}
|
||||
|
||||
function radius(x, y) {
|
||||
var dist1 = Math.max(dist(x, y, 0, 0), dist(x, y, width, height))
|
||||
var dist2 = Math.max(dist(x, y, width, 0), dist(x, y, 0, height))
|
||||
|
||||
return Math.max(dist1, dist2)
|
||||
}
|
||||
|
||||
function dist(x1, y1, x2, y2) {
|
||||
var distX = x2 - x1
|
||||
var distY = y2 - y1
|
||||
|
||||
return Math.sqrt(distX * distX + distY * distY)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: focusBackground
|
||||
objectName: "focusBackground"
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
color: Qt.rgba(0,0,0,0.2)
|
||||
|
||||
opacity: __private.showFocus && focused ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 500; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: focusCircle
|
||||
objectName: "focusRipple"
|
||||
|
||||
property bool focusedState
|
||||
|
||||
x: (parent.width - width)/2
|
||||
y: (parent.height - height)/2
|
||||
|
||||
width: focused
|
||||
? focusedState ? focusWidth
|
||||
: Math.min(parent.width - 8, focusWidth + 12)
|
||||
: parent.width/5
|
||||
height: width
|
||||
|
||||
radius: width/2
|
||||
|
||||
opacity: __private.showFocus && focused ? 1 : 0
|
||||
|
||||
color: focusColor.a === 0 ? Qt.rgba(1,1,1,0.4) : focusColor
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 500; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: focusTimer.interval; }
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: focusTimer
|
||||
running: focused
|
||||
repeat: true
|
||||
interval: 800
|
||||
|
||||
onTriggered: focusCircle.focusedState = !focusCircle.focusedState
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: tapCircle
|
||||
|
||||
Item {
|
||||
id: circleItem
|
||||
objectName: "tapRipple"
|
||||
|
||||
property bool done
|
||||
|
||||
property real circleX
|
||||
property real circleY
|
||||
|
||||
property bool closed
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
function removeCircle() {
|
||||
done = true
|
||||
|
||||
if (fillSizeAnimation.running) {
|
||||
fillOpacityAnimation.stop()
|
||||
closeAnimation.start()
|
||||
|
||||
circleItem.destroy(500);
|
||||
} else {
|
||||
__private.showFocus = true
|
||||
fadeAnimation.start();
|
||||
|
||||
circleItem.destroy(300);
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: circleParent
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
visible: !circular
|
||||
|
||||
Rectangle {
|
||||
id: circleRectangle
|
||||
|
||||
x: circleItem.circleX - radius
|
||||
y: circleItem.circleY - radius
|
||||
|
||||
width: radius * 2
|
||||
height: radius * 2
|
||||
|
||||
opacity: 0
|
||||
color: ripple.color
|
||||
|
||||
NumberAnimation {
|
||||
id: fillSizeAnimation
|
||||
running: true
|
||||
|
||||
target: circleRectangle; property: "radius"; duration: 500;
|
||||
from: __private.startRadius; to: __private.endRadius;
|
||||
easing.type: Easing.InOutQuad
|
||||
|
||||
onStopped: {
|
||||
if (done)
|
||||
__private.showFocus = true
|
||||
}
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: fillOpacityAnimation
|
||||
running: true
|
||||
|
||||
target: circleRectangle; property: "opacity"; duration: 300;
|
||||
from: 0; to: 1; easing.type: Easing.InOutQuad
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: fadeAnimation
|
||||
|
||||
target: circleRectangle; property: "opacity"; duration: 300;
|
||||
from: 1; to: 0; easing.type: Easing.InOutQuad
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: closeAnimation
|
||||
|
||||
NumberAnimation {
|
||||
target: circleRectangle; property: "opacity"; duration: 250;
|
||||
to: 1; easing.type: Easing.InOutQuad
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: circleRectangle; property: "opacity"; duration: 250;
|
||||
from: 1; to: 0; easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CircleMask {
|
||||
anchors.fill: parent
|
||||
source: circleParent
|
||||
visible: circular
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
imports/NeoChat/Effect/qmldir
Normal file
3
imports/NeoChat/Effect/qmldir
Normal file
@@ -0,0 +1,3 @@
|
||||
module NeoChat.Effect
|
||||
ElevationEffect 2.0 ElevationEffect.qml
|
||||
RippleEffect 2.0 RippleEffect.qml
|
||||
47
imports/NeoChat/Menu/RoomListContextMenu.qml
Normal file
47
imports/NeoChat/Menu/RoomListContextMenu.qml
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
*
|
||||
* SPDX-LicenseIdentifier: GPL-3.0-only
|
||||
*/
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
/**
|
||||
* Context menu when clicking on a room in the room list
|
||||
*/
|
||||
Menu {
|
||||
id: root
|
||||
property var room
|
||||
|
||||
MenuItem {
|
||||
text: i18n("Favourite")
|
||||
checkable: true
|
||||
checked: room.isFavourite
|
||||
|
||||
onTriggered: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: i18n("Deprioritize")
|
||||
checkable: true
|
||||
checked: room.isLowPriority
|
||||
|
||||
onTriggered: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0)
|
||||
}
|
||||
|
||||
MenuSeparator {}
|
||||
|
||||
MenuItem {
|
||||
text: i18n("Mark as Read")
|
||||
|
||||
onTriggered: room.markAllMessagesAsRead()
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: i18n("Leave Room")
|
||||
onTriggered: room.forget()
|
||||
}
|
||||
|
||||
onClosed: destroy()
|
||||
}
|
||||
46
imports/NeoChat/Menu/Timeline/FileDelegateContextMenu.qml
Normal file
46
imports/NeoChat/Menu/Timeline/FileDelegateContextMenu.qml
Normal file
@@ -0,0 +1,46 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
import NeoChat.Dialog 2.0
|
||||
|
||||
Menu {
|
||||
signal viewSource()
|
||||
signal downloadAndOpen()
|
||||
signal saveFileAs()
|
||||
signal reply()
|
||||
signal redact()
|
||||
|
||||
id: root
|
||||
|
||||
MenuItem {
|
||||
text: "View Source"
|
||||
|
||||
onTriggered: viewSource()
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: "Open Externally"
|
||||
|
||||
onTriggered: downloadAndOpen()
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: "Save As"
|
||||
|
||||
onTriggered: saveFileAs()
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: "Reply"
|
||||
|
||||
onTriggered: reply()
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: "Redact"
|
||||
|
||||
onTriggered: redact()
|
||||
}
|
||||
|
||||
onClosed: destroy()
|
||||
}
|
||||
116
imports/NeoChat/Menu/Timeline/MessageDelegateContextMenu.qml
Normal file
116
imports/NeoChat/Menu/Timeline/MessageDelegateContextMenu.qml
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
*
|
||||
* SPDX-LicenseIdentifier: GPL-3.0-only
|
||||
*/
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12 as QQC2
|
||||
import QtQuick.Layouts 1.12
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
import NeoChat.Dialog 2.0
|
||||
|
||||
Kirigami.OverlaySheet {
|
||||
id: root
|
||||
|
||||
required property var author;
|
||||
required property string message;
|
||||
required property string eventId;
|
||||
|
||||
signal viewSource()
|
||||
signal reply(var author, string message)
|
||||
signal remove()
|
||||
|
||||
parent: applicationWindow().overlay
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
QQC2.Control {
|
||||
leftPadding: Kirigami.Units.largeSpacing
|
||||
rightPadding: Kirigami.Units.largeSpacing
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: headerLayout.implicitHeight
|
||||
contentItem: RowLayout {
|
||||
id: headerLayout
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
Kirigami.Avatar {
|
||||
source: author.avatarMediaId ? "image://mxc/" + author.avatarMediaId : ""
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.large
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.large
|
||||
Layout.alignment: Qt.AlignTop
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Heading {
|
||||
level: 3
|
||||
Layout.fillWidth: true
|
||||
text: author.displayName
|
||||
}
|
||||
QQC2.Label {
|
||||
text: message
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Row {
|
||||
spacing: 0
|
||||
Repeater {
|
||||
model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"]
|
||||
delegate: QQC2.ItemDelegate {
|
||||
width: 32
|
||||
height: 32
|
||||
|
||||
contentItem: QQC2.Label {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
font.pixelSize: 16
|
||||
font.family: "emoji"
|
||||
text: modelData
|
||||
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
currentRoom.toggleReaction(eventId, modelData)
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Kirigami.BasicListItem {
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Reply")
|
||||
icon.name: "reply"
|
||||
onTriggered: reply(author, message)
|
||||
}
|
||||
}
|
||||
Kirigami.BasicListItem {
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Remove")
|
||||
icon.name: "edit-delete-remove"
|
||||
icon.color: "red"
|
||||
onTriggered: remove()
|
||||
}
|
||||
}
|
||||
Kirigami.BasicListItem {
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Copy")
|
||||
icon.name: "copy"
|
||||
onTriggered: Clibpoard.setText(message)
|
||||
}
|
||||
}
|
||||
Kirigami.BasicListItem {
|
||||
action: Kirigami.Action {
|
||||
text: i18n("View Source")
|
||||
icon.name: "code-context"
|
||||
onTriggered: viewSource()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
imports/NeoChat/Menu/Timeline/qmldir
Normal file
3
imports/NeoChat/Menu/Timeline/qmldir
Normal file
@@ -0,0 +1,3 @@
|
||||
module NeoChat.Menu.Timeline
|
||||
MessageDelegateContextMenu 2.0 MessageDelegateContextMenu.qml
|
||||
FileDelegateContextMenu 2.0 FileDelegateContextMenu.qml
|
||||
2
imports/NeoChat/Menu/qmldir
Normal file
2
imports/NeoChat/Menu/qmldir
Normal file
@@ -0,0 +1,2 @@
|
||||
module NeoChat.Menu
|
||||
RoomListContextMenu 2.0 RoomListContextMenu.qml
|
||||
134
imports/NeoChat/Page/AccountsPage.qml
Normal file
134
imports/NeoChat/Page/AccountsPage.qml
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
|
||||
*
|
||||
* SPDX-LicenseIdentifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14 as Controls
|
||||
import QtQuick.Layouts 1.14
|
||||
|
||||
import org.kde.kirigami 2.12 as Kirigami
|
||||
|
||||
import NeoChat 0.1
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
title: i18n("Accounts")
|
||||
|
||||
ListView {
|
||||
model: AccountListModel { }
|
||||
delegate: Kirigami.SwipeListItem {
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
Kirigami.BasicListItem {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
text: model.user.defaultName
|
||||
subtitle: model.user.id
|
||||
icon: model.connection.user.avatarMediaId ? "image://mxc/" + model.connection.user.avatarMediaId : "im-user"
|
||||
|
||||
onClicked: {
|
||||
Controller.connection = model.connection
|
||||
pageStack.layers.pop()
|
||||
}
|
||||
}
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
text: i18n("Edit this account")
|
||||
iconName: "document-edit"
|
||||
onTriggered: {
|
||||
userEditSheet.connection = model.connection
|
||||
userEditSheet.open()
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Logout")
|
||||
iconName: "im-kick-user"
|
||||
onTriggered: {
|
||||
Controller.logout(model.connection)
|
||||
if(Controller.accountCount === 1)
|
||||
pageStack.layers.pop()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Controller
|
||||
function onConnectionAdded() {
|
||||
if (pageStack.layers.depth > 2)
|
||||
pageStack.layers.pop()
|
||||
}
|
||||
function onPasswordStatus(status) {
|
||||
if(status == Controller.Success)
|
||||
showPassiveNotification(i18n("Password changed successfully"))
|
||||
else if(status == Controller.Wrong)
|
||||
showPassiveNotification(i18n("Wrong password entered"))
|
||||
else
|
||||
showPassiveNotification(i18n("Unknown problem while trying to change password"))
|
||||
}
|
||||
}
|
||||
|
||||
actions.main: Kirigami.Action {
|
||||
text: i18n("Add an account")
|
||||
iconName: "list-add-user"
|
||||
onTriggered: pageStack.layers.push("qrc:/qml/LoginPage.qml")
|
||||
}
|
||||
|
||||
Kirigami.OverlaySheet {
|
||||
id: userEditSheet
|
||||
|
||||
property var connection
|
||||
|
||||
header: Kirigami.Heading {
|
||||
text: i18n("Edit Account")
|
||||
}
|
||||
|
||||
Kirigami.FormLayout {
|
||||
anchors.top: passwordsMessage.bottom
|
||||
Controls.TextField {
|
||||
id: name
|
||||
text: userEditSheet.connection.localUser.defaultName
|
||||
Kirigami.FormData.label: i18n("Name:")
|
||||
}
|
||||
Controls.TextField {
|
||||
id: currentPassword
|
||||
Kirigami.FormData.label: i18n("Current Password:")
|
||||
echoMode: TextInput.Password
|
||||
}
|
||||
Controls.TextField {
|
||||
id: newPassword
|
||||
Kirigami.FormData.label: i18n("New Password:")
|
||||
echoMode: TextInput.Password
|
||||
|
||||
}
|
||||
Controls.TextField {
|
||||
id: confirmPassword
|
||||
Kirigami.FormData.label: i18n("Confirm new Password:")
|
||||
echoMode: TextInput.Password
|
||||
}
|
||||
|
||||
Controls.Button {
|
||||
text: i18n("Save")
|
||||
onClicked: {
|
||||
if(userEditSheet.connection.localUser.defaultName !== name.text)
|
||||
userEditSheet.connection.localUser.user.defaultName = name.text
|
||||
if(currentPassword.text !== "" && newPassword.text !== "" && confirmPassword.text !== "") {
|
||||
if(newPassword.text === confirmPassword.text) {
|
||||
Controller.changePassword(userEditSheet.connection, currentPassword.text, newPassword.text)
|
||||
} else {
|
||||
showPassiveNotification(i18n("Passwords do not match"))
|
||||
return
|
||||
}
|
||||
}
|
||||
userEditSheet.close()
|
||||
currentPassword.text = ""
|
||||
newPassword.text = ""
|
||||
confirmPassword.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
imports/NeoChat/Page/LoadingPage.qml
Normal file
10
imports/NeoChat/Page/LoadingPage.qml
Normal file
@@ -0,0 +1,10 @@
|
||||
import org.kde.kirigami 2.12 as Kirigami
|
||||
import QtQuick.Controls 2.12 as QQC2
|
||||
|
||||
Kirigami.Page {
|
||||
title: i18n("Loading")
|
||||
|
||||
QQC2.BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
63
imports/NeoChat/Page/LoginPage.qml
Normal file
63
imports/NeoChat/Page/LoginPage.qml
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
*
|
||||
* SPDX-LicenseIdentifier: GPL-3.0-or-later
|
||||
*/
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12 as QQC2
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
import NeoChat 0.1
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
|
||||
import org.kde.kirigami 2.12 as Kirigami
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
id: root
|
||||
|
||||
title: i18n("Login")
|
||||
|
||||
Kirigami.FormLayout {
|
||||
id: formLayout
|
||||
QQC2.TextField {
|
||||
id: serverField
|
||||
Kirigami.FormData.label: i18n("Server Address")
|
||||
text: "https://matrix.org"
|
||||
onAccepted: usernameField.forceActiveFocus()
|
||||
}
|
||||
QQC2.TextField {
|
||||
id: usernameField
|
||||
Kirigami.FormData.label: i18n("Username")
|
||||
onAccepted: passwordField.forceActiveFocus()
|
||||
}
|
||||
Kirigami.PasswordField {
|
||||
id: passwordField
|
||||
Kirigami.FormData.label: i18n("Password")
|
||||
onAccepted: accessTokenField.forceActiveFocus()
|
||||
}
|
||||
QQC2.TextField {
|
||||
id: accessTokenField
|
||||
Kirigami.FormData.label: i18n("Access Token (Optional)")
|
||||
onAccepted: deviceNameField.forceActiveFocus()
|
||||
}
|
||||
QQC2.TextField {
|
||||
id: deviceNameField
|
||||
Kirigami.FormData.label: i18n("Device Name (Optional)")
|
||||
onAccepted: doLogin()
|
||||
}
|
||||
QQC2.Button {
|
||||
text: i18n("Login")
|
||||
onClicked: doLogin()
|
||||
}
|
||||
}
|
||||
|
||||
function doLogin() {
|
||||
if (accessTokenField.text.length > 0) {
|
||||
Controller.loginWithAccessToken(serverField.text, usernameField.text, accessTokenField.text, deviceNameField.text)
|
||||
} else {
|
||||
Controller.loginWithCredentials(serverField.text, usernameField.text, passwordField.text, deviceNameField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
145
imports/NeoChat/Page/RoomListPage.qml
Normal file
145
imports/NeoChat/Page/RoomListPage.qml
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
*
|
||||
* SPDX-LicenseIdentifier: GPL-3.0-only
|
||||
*/
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12 as QQC2
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
import org.kde.kitemmodels 1.0
|
||||
import org.kde.neochat 0.1
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Menu 2.0
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
id: page
|
||||
|
||||
property var roomListModel
|
||||
property var enteredRoom
|
||||
property var searchText: ""
|
||||
|
||||
onSearchTextChanged: sortFilterRoomListModel.setFilterText(searchText)
|
||||
|
||||
signal enterRoom(var room)
|
||||
signal leaveRoom(var room)
|
||||
|
||||
title: i18n("Rooms")
|
||||
|
||||
titleDelegate: Kirigami.SearchField {
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
onTextChanged: page.searchText = text
|
||||
}
|
||||
|
||||
ListView {
|
||||
model: SortFilterRoomListModel {
|
||||
id: sortFilterRoomListModel
|
||||
sourceModel: roomListModel
|
||||
roomSortOrder: SortFilterRoomListModel.Categories
|
||||
}
|
||||
|
||||
section.property: "category"
|
||||
section.delegate: Kirigami.ListSectionHeader {
|
||||
id: sectionHeader
|
||||
action: Kirigami.Action {
|
||||
onTriggered: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
|
||||
}
|
||||
contentItem: Item {
|
||||
implicitHeight: categoryName.implicitHeight
|
||||
Kirigami.Heading {
|
||||
id: categoryName
|
||||
level: 3
|
||||
text: roomListModel.categoryName(section)
|
||||
}
|
||||
Kirigami.Icon {
|
||||
source: roomListModel.categoryVisible(section) ? "go-up" : "go-down"
|
||||
implicitHeight: Kirigami.Units.iconSizes.small
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
anchors.left: categoryName.right
|
||||
anchors.leftMargin: Kirigami.Units.largeSpacing
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Kirigami.AbstractListItem {
|
||||
visible: model.categoryVisible
|
||||
topPadding: Kirigami.Units.largeSpacing
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
|
||||
contentItem: Item {
|
||||
implicitHeight: roomLayout.implicitHeight
|
||||
RowLayout {
|
||||
id: roomLayout
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
anchors.fill: parent
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: height
|
||||
Layout.fillHeight: true
|
||||
|
||||
source: avatar ? "image://mxc/" + avatar : ""
|
||||
name: model.name || i18n("No Name")
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
text: name ?? ""
|
||||
font.pixelSize: 15
|
||||
font.bold: unreadCount >= 0
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
text: (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm," ")
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
console.log(mouse.button)
|
||||
if (mouse.button == Qt.RightButton) {
|
||||
roomListContextMenu.createObject(parent, {"room": currentRoom}).popup()
|
||||
} else {
|
||||
if (enteredRoom) {
|
||||
leaveRoom(enteredRoom)
|
||||
}
|
||||
|
||||
enteredRoom = currentRoom
|
||||
|
||||
enterRoom(enteredRoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: roomListContextMenu
|
||||
RoomListContextMenu {}
|
||||
}
|
||||
}
|
||||
}
|
||||
398
imports/NeoChat/Page/RoomPage.qml
Normal file
398
imports/NeoChat/Page/RoomPage.qml
Normal file
@@ -0,0 +1,398 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12 as QQC2
|
||||
import QtQuick.Layouts 1.12
|
||||
import Qt.labs.qmlmodels 1.0
|
||||
import QtQuick.Controls.Material 2.12
|
||||
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
import org.kde.kitemmodels 1.0
|
||||
import org.kde.neochat 0.1
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Component.Timeline 2.0
|
||||
import NeoChat.Dialog 2.0
|
||||
import NeoChat.Effect 2.0
|
||||
import NeoChat.Menu.Timeline 2.0
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
id: page
|
||||
|
||||
property var currentRoom
|
||||
|
||||
title: i18n("Messages")
|
||||
|
||||
MessageEventModel {
|
||||
id: messageEventModel
|
||||
|
||||
room: currentRoom
|
||||
}
|
||||
|
||||
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(ApplicationWindow.overlay)
|
||||
|
||||
fileDialog.chosen.connect(function(path) {
|
||||
if (!path) return
|
||||
|
||||
chatTextInput.attach(path)
|
||||
})
|
||||
|
||||
fileDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {}
|
||||
|
||||
QQC2.ToolButton {
|
||||
Layout.preferredWidth: 160
|
||||
Layout.fillHeight: true
|
||||
|
||||
padding: 16
|
||||
|
||||
icon.name: 'insert-image'
|
||||
text: i18n("Clipboard image")
|
||||
onClicked: {
|
||||
var localPath = StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/screenshots/" + (new Date()).getTime() + ".png"
|
||||
if (!Clipboard.saveImage(localPath)) return
|
||||
chatTextInput.attach(localPath)
|
||||
attachDialog.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: openFileDialog
|
||||
|
||||
OpenFileDialog {}
|
||||
}
|
||||
|
||||
|
||||
KSortFilterProxyModel {
|
||||
id: sortedMessageEventModel
|
||||
|
||||
sourceModel: messageEventModel
|
||||
|
||||
filterRowCallback: function(row, parent) {
|
||||
return messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.MessageRole) !== 0x10 && messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "other"
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1
|
||||
readonly property bool noNeedMoreContent: !currentRoom || currentRoom.eventsHistoryJob || currentRoom.allHistoryLoaded
|
||||
|
||||
readonly property bool isLoaded: page.width * page.height > 10
|
||||
|
||||
id: messageListView
|
||||
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
clip: true
|
||||
|
||||
displayMarginBeginning: Kirigami.Units.gridUnit
|
||||
displayMarginEnd: typingNotification.visible ? typingNotification.height : Kirigami.Units.gridUnit
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
highlightMoveDuration: 500
|
||||
|
||||
model: !isLoaded ? undefined : sortedMessageEventModel
|
||||
|
||||
onContentYChanged: {
|
||||
if(!noNeedMoreContent && contentY - 5000 < originY)
|
||||
currentRoom.getPreviousContent(20);
|
||||
}
|
||||
|
||||
// populate: Transition {
|
||||
// NumberAnimation {
|
||||
// property: "opacity"; from: 0; to: 1
|
||||
// duration: 200
|
||||
// }
|
||||
// }
|
||||
|
||||
// add: Transition {
|
||||
// NumberAnimation {
|
||||
// property: "opacity"; from: 0; to: 1
|
||||
// duration: 200
|
||||
// }
|
||||
// }
|
||||
|
||||
// move: Transition {
|
||||
// NumberAnimation {
|
||||
// property: "y"; duration: 200
|
||||
// }
|
||||
// NumberAnimation {
|
||||
// property: "opacity"; to: 1
|
||||
// }
|
||||
// }
|
||||
|
||||
// displaced: Transition {
|
||||
// NumberAnimation {
|
||||
// property: "y"; duration: 200
|
||||
// easing.type: Easing.OutQuad
|
||||
// }
|
||||
// NumberAnimation {
|
||||
// property: "opacity"; to: 1
|
||||
// }
|
||||
// }
|
||||
|
||||
delegate: DelegateChooser {
|
||||
id: timelineDelegateChooser
|
||||
role: "eventType"
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "state"
|
||||
delegate: TimelineContainer {
|
||||
width: messageListView.width
|
||||
|
||||
innerObject: StateDelegate {
|
||||
Layout.maximumWidth: parent.width
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "emote"
|
||||
delegate: TimelineContainer {
|
||||
width: messageListView.width
|
||||
|
||||
innerObject: StateDelegate {
|
||||
Layout.maximumWidth: parent.width
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "message"
|
||||
delegate: TimelineContainer {
|
||||
width: messageListView.width
|
||||
innerObject: MessageDelegate {
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: messageListView.width
|
||||
mouseArea: MouseArea {
|
||||
acceptedButtons: Qt.RightButton
|
||||
anchors.fill: parent
|
||||
onClicked: openMessageContext(author, display, eventId, toolTip);
|
||||
}
|
||||
innerObject: [
|
||||
TextDelegate {
|
||||
Layout.fillWidth: true
|
||||
},
|
||||
ReactionDelegate {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 8
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "notice"
|
||||
delegate: TimelineContainer {
|
||||
width: messageListView.width
|
||||
|
||||
innerObject: MessageDelegate {
|
||||
Layout.fillWidth: true
|
||||
|
||||
innerObject: TextDelegate {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "image"
|
||||
delegate: TimelineContainer {
|
||||
width: messageListView.width
|
||||
|
||||
innerObject: MessageDelegate {
|
||||
Layout.fillWidth: true
|
||||
|
||||
innerObject: [
|
||||
ImageDelegate {
|
||||
Layout.maximumWidth: parent.width
|
||||
Layout.preferredWidth: Math.min(320, info.w)
|
||||
Layout.preferredHeight: Math.min(320, info.h)
|
||||
},
|
||||
ReactionDelegate {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 8
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "audio"
|
||||
delegate: TimelineContainer {
|
||||
width: messageListView.width
|
||||
|
||||
innerObject: MessageDelegate {
|
||||
Layout.fillWidth: true
|
||||
|
||||
innerObject: AudioDelegate {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "video"
|
||||
delegate: TimelineContainer {
|
||||
width: messageListView.width
|
||||
|
||||
innerObject: MessageDelegate {
|
||||
Layout.fillWidth: true
|
||||
|
||||
innerObject: AudioDelegate {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "file"
|
||||
delegate: TimelineContainer {
|
||||
width: messageListView.width
|
||||
|
||||
innerObject: MessageDelegate {
|
||||
Layout.fillWidth: true
|
||||
|
||||
innerObject: FileDelegate {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "other"
|
||||
delegate: Item {}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 16
|
||||
|
||||
padding: 8
|
||||
|
||||
id: goReadMarkerFab
|
||||
|
||||
visible: currentRoom && currentRoom.hasUnreadMessages || !messageListView.atYEnd
|
||||
action: Kirigami.Action {
|
||||
onTriggered: {
|
||||
if (currentRoom && currentRoom.hasUnreadMessages) {
|
||||
goToEvent(currentRoom.readMarkerEventId)
|
||||
} else {
|
||||
currentRoom.markAllMessagesAsRead()
|
||||
messageListView.positionViewAtBeginning()
|
||||
}
|
||||
}
|
||||
icon.name: currentRoom && currentRoom.hasUnreadMessages ? "go-up" : "go-down"
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Control {
|
||||
id: typingNotification
|
||||
anchors.left: parent.left
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
visible: currentRoom && currentRoom.usersTyping.length > 0
|
||||
padding: 4
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
QQC2.BusyIndicator {
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
|
||||
}
|
||||
QQC2.Label {
|
||||
text: i18ncp("Message displayed when some users are typing", "%2 is typing", "%2 are typing", currentRoom.usersTyping.length, currentRoom.usersTyping.join(", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Component.onCompleted: {
|
||||
if (currentRoom) {
|
||||
if (currentRoom.timelineSize < 20)
|
||||
currentRoom.getPreviousContent(50)
|
||||
}
|
||||
|
||||
positionViewAtBeginning()
|
||||
}
|
||||
}
|
||||
|
||||
footer: ChatTextInput {
|
||||
id: chatTextInput
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
background: Item {}
|
||||
|
||||
function openMessageContext(author, message, eventId, toolTip, model) {
|
||||
const contextMenu = messageDelegateContextMenu.createObject(root, {
|
||||
'author': author,
|
||||
'message': message,
|
||||
'eventId': eventId,
|
||||
});
|
||||
contextMenu.viewSource.connect(function() {
|
||||
messageSourceDialog.createObject(root, {
|
||||
'sourceText': toolTip,
|
||||
}).open();
|
||||
contextMenu.close();
|
||||
});
|
||||
contextMenu.reply.connect(function(replyUser, replyContent) {
|
||||
chatTextInput.replyUser = replyUser;
|
||||
chatTextInput.replyEventID = eventId;
|
||||
chatTextInput.replyContent = replyContent;
|
||||
chatTextInput.isReply = true;
|
||||
chatTextInput.focus();
|
||||
contextMenu.close();
|
||||
})
|
||||
contextMenu.remove.connect(function() {
|
||||
currentRoom.redactEvent(eventId);
|
||||
contextMenu.close();
|
||||
})
|
||||
contextMenu.open()
|
||||
}
|
||||
|
||||
Component {
|
||||
id: messageDelegateContextMenu
|
||||
|
||||
MessageDelegateContextMenu {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: messageSourceDialog
|
||||
|
||||
MessageSourceDialog {}
|
||||
}
|
||||
}
|
||||
5
imports/NeoChat/Page/qmldir
Normal file
5
imports/NeoChat/Page/qmldir
Normal file
@@ -0,0 +1,5 @@
|
||||
module NeoChat.Page
|
||||
LoadingPage 2.0 LoadingPage.qml
|
||||
LoginPage 2.0 LoginPage.qml
|
||||
RoomListPage 2.0 RoomListPage.qml
|
||||
RoomPage 2.0 RoomPage.qml
|
||||
20
imports/NeoChat/Panel/NeoChatSidebar.qml
Normal file
20
imports/NeoChat/Panel/NeoChatSidebar.qml
Normal file
@@ -0,0 +1,20 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12 as Controls
|
||||
import QtQuick.Layouts 1.12
|
||||
import Qt.labs.qmlmodels 1.0
|
||||
|
||||
import org.kde.kirigami 2.12 as Kirigami
|
||||
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Component.Timeline 2.0
|
||||
import org.kde.neochat 0.1
|
||||
|
||||
Kirigami.GlobalDrawer {
|
||||
id: root
|
||||
|
||||
modal: true
|
||||
collapsible: true
|
||||
collapsed: Kirigami.Settings.isMobile
|
||||
}
|
||||
222
imports/NeoChat/Panel/RoomDrawer.qml
Normal file
222
imports/NeoChat/Panel/RoomDrawer.qml
Normal file
@@ -0,0 +1,222 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Controls.Material 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Dialog 2.0
|
||||
import NeoChat.Effect 2.0
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
import org.kde.neochat 0.1
|
||||
|
||||
Kirigami.OverlayDrawer {
|
||||
property var room
|
||||
|
||||
id: roomDrawer
|
||||
enabled: true
|
||||
|
||||
edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge
|
||||
|
||||
padding: 0
|
||||
contentItem: ColumnLayout {
|
||||
implicitWidth: Kirigami.Units.gridUnit * 15 // TODO FIXME
|
||||
|
||||
Component {
|
||||
id: fullScreenImage
|
||||
|
||||
FullScreenImage {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 16
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: 72
|
||||
Layout.preferredHeight: 72
|
||||
|
||||
name: room ? room.displayName : "No name"
|
||||
source: room ? "image://mxc/" + room.avatarMediaId : null
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
wrapMode: Label.Wrap
|
||||
text: room ? room.displayName : "No Name"
|
||||
color: MPalette.foreground
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
wrapMode: Label.Wrap
|
||||
text: room ? room.totalMemberCount + " Members" : "No Member Count"
|
||||
color: MPalette.lighter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Control {
|
||||
Layout.fillWidth: true
|
||||
contentItem: Kirigami.FormLayout {
|
||||
Label {
|
||||
Kirigami.FormData.label: "Main Alias"
|
||||
text: room && room.canonicalAlias ? room.canonicalAlias : "No Canonical Alias"
|
||||
}
|
||||
Label {
|
||||
Kirigami.FormData.label: "Topic"
|
||||
text: room && room.topic ? room.topic : "No Topic"
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
background: AutoMouseArea {
|
||||
onPrimaryClicked: roomSettingDialog.createObject(ApplicationWindow.overlay, {"room": room}).open()
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 8
|
||||
Kirigami.Icon {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
source: "user-others"
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
wrapMode: Label.Wrap
|
||||
text: room ? room.totalMemberCount + " Members" : "No Member Count"
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit
|
||||
icon.name: "list-user-add"
|
||||
|
||||
onClicked: inviteUserDialog.createObject(ApplicationWindow.overlay, {"room": room}).open()
|
||||
}
|
||||
}
|
||||
|
||||
AutoListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
id: userListView
|
||||
|
||||
clip: true
|
||||
|
||||
boundsBehavior: Flickable.DragOverBounds
|
||||
|
||||
model: UserListModel {
|
||||
room: roomDrawer.room
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
width: userListView.width
|
||||
height: 48
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 12
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: height
|
||||
Layout.fillHeight: true
|
||||
|
||||
source: "image://mxc/" + avatar
|
||||
name: name
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: name
|
||||
color: MPalette.foreground
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: perm != UserType.Member
|
||||
|
||||
text: {
|
||||
if (perm == UserType.Owner) {
|
||||
return "Owner"
|
||||
}
|
||||
if (perm == UserType.Admin) {
|
||||
return "Admin"
|
||||
}
|
||||
if (perm == UserType.Moderator) {
|
||||
return "Mod"
|
||||
}
|
||||
if (perm == UserType.Muted) {
|
||||
return "Muted"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
color: perm == UserType.Muted ? MPalette.lighter : MPalette.accent
|
||||
font.pixelSize: 12
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
RippleEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
onPrimaryClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": room, "user": user}).open()
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
}
|
||||
}
|
||||
|
||||
onRoomChanged: {
|
||||
if (room == null) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomSettingDialog
|
||||
|
||||
RoomSettingsDialog {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: userDetailDialog
|
||||
|
||||
UserDetailDialog {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: inviteUserDialog
|
||||
|
||||
InviteUserDialog {}
|
||||
}
|
||||
}
|
||||
55
imports/NeoChat/Panel/RoomHeader.qml
Normal file
55
imports/NeoChat/Panel/RoomHeader.qml
Normal file
@@ -0,0 +1,55 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtQuick.Controls.Material 2.12
|
||||
|
||||
import org.kde.neochat 0.1
|
||||
import NeoChat.Effect 2.0
|
||||
import NeoChat.Component 2.0
|
||||
import NeoChat.Setting 0.1
|
||||
|
||||
Control {
|
||||
signal clicked()
|
||||
|
||||
id: header
|
||||
|
||||
background: Rectangle {
|
||||
color: MPalette.background
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: ElevationEffect {
|
||||
elevation: 2
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 18
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
spacing: 12
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: currentRoom ? currentRoom.displayName : ""
|
||||
color: MPalette.foreground
|
||||
font.pixelSize: 18
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
Layout.preferredWidth: height
|
||||
Layout.fillHeight: true
|
||||
|
||||
contentItem: MaterialIcon {
|
||||
icon: "\ue5d4"
|
||||
color: MPalette.lighter
|
||||
}
|
||||
|
||||
onClicked: header.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
108
imports/NeoChat/Panel/RoomListPanel.qml
Normal file
108
imports/NeoChat/Panel/RoomListPanel.qml
Normal file
@@ -0,0 +1,108 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12 as QQC2
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
import org.kde.kitemmodels 1.0
|
||||
import NeoChat.Component 2.0
|
||||
import org.kde.neochat 0.1
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
id: page
|
||||
|
||||
property var roomListModel
|
||||
property var enteredRoom
|
||||
property var searchText: ""
|
||||
|
||||
onSearchTextChanged: sortedFilteredRoomListModel.invalidateFilter()
|
||||
|
||||
signal enterRoom(var room)
|
||||
signal leaveRoom(var room)
|
||||
|
||||
title: "Rooms"
|
||||
|
||||
titleDelegate: Kirigami.SearchField {
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
onTextChanged: page.searchText = text
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: messageListView
|
||||
|
||||
model: KSortFilterProxyModel {
|
||||
id: sortedFilteredRoomListModel
|
||||
sourceModel: roomListModel
|
||||
|
||||
sortRole: "name"
|
||||
sortOrder: Qt.AscendingOrder
|
||||
filterRowCallback: function(row, parent) {
|
||||
return (roomListModel.data(roomListModel.index(row, 0), RoomListModel.JoinStateRole) !== "upgraded") && roomListModel.data(roomListModel.index(row, 0), RoomListModel.NameRole).toLowerCase().includes(page.searchText.toLowerCase())
|
||||
}
|
||||
}
|
||||
|
||||
section.property: "categoryName"
|
||||
section.delegate: Kirigami.ListSectionHeader {
|
||||
label: section
|
||||
}
|
||||
|
||||
delegate: Kirigami.AbstractListItem {
|
||||
topPadding: Kirigami.Units.largeSpacing
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Kirigami.Avatar {
|
||||
Layout.preferredWidth: height
|
||||
Layout.fillHeight: true
|
||||
|
||||
source: avatar ? "image://mxc/" + avatar : ""
|
||||
name: model.name || "No Name"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
text: name ?? ""
|
||||
font.pixelSize: 15
|
||||
font.bold: unreadCount >= 0
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
text: (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm," ")
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (enteredRoom) {
|
||||
leaveRoom(enteredRoom)
|
||||
}
|
||||
|
||||
enteredRoom = currentRoom
|
||||
|
||||
enterRoom(enteredRoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
imports/NeoChat/Panel/qmldir
Normal file
5
imports/NeoChat/Panel/qmldir
Normal file
@@ -0,0 +1,5 @@
|
||||
module NeoChat.Panel
|
||||
RoomPanel 2.0 RoomPanel.qml
|
||||
RoomListPanel 2.0 RoomListPanel.qml
|
||||
RoomDrawer 2.0 RoomDrawer.qml
|
||||
NeoChatSidebar 2.0 NeoChatSidebar.qml
|
||||
14
imports/NeoChat/Setting/Palette.qml
Normal file
14
imports/NeoChat/Setting/Palette.qml
Normal file
@@ -0,0 +1,14 @@
|
||||
pragma Singleton
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls.Material 2.12
|
||||
|
||||
QtObject {
|
||||
readonly property int theme: MSettings.darkTheme ? Material.Dark : Material.Light
|
||||
|
||||
readonly property color primary: "#344955"
|
||||
readonly property color accent: "#4286F5"
|
||||
readonly property color foreground: MSettings.darkTheme ? "#FFFFFF" : "#1D333E"
|
||||
readonly property color background: MSettings.darkTheme ? "#303030" : "#FFFFFF"
|
||||
readonly property color lighter: MSettings.darkTheme ? "#FFFFFF" : "#5B7480"
|
||||
readonly property color banner: MSettings.darkTheme ? "#404040" : "#F2F3F4"
|
||||
}
|
||||
13
imports/NeoChat/Setting/Setting.qml
Normal file
13
imports/NeoChat/Setting/Setting.qml
Normal file
@@ -0,0 +1,13 @@
|
||||
pragma Singleton
|
||||
import QtQuick 2.12
|
||||
import Qt.labs.settings 1.0
|
||||
|
||||
Settings {
|
||||
property bool showNotification: true
|
||||
|
||||
property bool showTray: true
|
||||
|
||||
property bool darkTheme
|
||||
|
||||
property string fontFamily: "Roboto,Noto Sans,Noto Color Emoji"
|
||||
}
|
||||
3
imports/NeoChat/Setting/qmldir
Normal file
3
imports/NeoChat/Setting/qmldir
Normal file
@@ -0,0 +1,3 @@
|
||||
module NeoChat.Setting
|
||||
singleton MSettings 0.1 Setting.qml
|
||||
singleton MPalette 0.1 Palette.qml
|
||||
Reference in New Issue
Block a user