Previously, when there was no avatar set, the source property of Avatar was still set to 'image://mxc/',
which caused Avatar to load that from the imageprovider. The imageprovider can't provide an empty image and aborts with error
(cherry picked from commit 724f10a895)
583 lines
18 KiB
QML
583 lines
18 KiB
QML
/**
|
|
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
|
*/
|
|
import QtQuick 2.12
|
|
import QtQuick.Controls 2.12
|
|
import QtQuick.Layouts 1.12
|
|
import Qt.labs.platform 1.0 as Platform
|
|
import org.kde.kirigami 2.13 as Kirigami
|
|
|
|
import NeoChat.Component 1.0
|
|
import NeoChat.Component.Emoji 1.0
|
|
import NeoChat.Dialog 1.0
|
|
import NeoChat.Page 1.0
|
|
|
|
import org.kde.neochat 1.0
|
|
|
|
ToolBar {
|
|
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
|
|
|
|
position: ToolBar.Footer
|
|
|
|
function addText(text) {
|
|
inputField.insert(inputField.length, text)
|
|
}
|
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
|
|
|
Action {
|
|
id: pasteAction
|
|
shortcut: StandardKey.Paste
|
|
onTriggered: {
|
|
if (Clipboard.hasImage) {
|
|
root.pasteImage();
|
|
}
|
|
activeFocusItem.paste();
|
|
}
|
|
}
|
|
|
|
contentItem: ColumnLayout {
|
|
id: layout
|
|
spacing: 0
|
|
EmojiPicker {
|
|
id: emojiPicker
|
|
|
|
Layout.fillWidth: true
|
|
|
|
visible: false
|
|
|
|
textArea: inputField
|
|
emojiModel: EmojiModel { id: emojiModel }
|
|
onChosen: {
|
|
textArea.insert(textArea.cursorPosition, emoji);
|
|
textArea.forceActiveFocus();
|
|
}
|
|
}
|
|
|
|
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: Kirigami.Units.gridUnit
|
|
Layout.preferredHeight: Kirigami.Units.gridUnit
|
|
|
|
source: replyUser ? ("image://mxc/" + replyUser.avatarMediaId) : ""
|
|
name: replyUser ? replyUser.displayName : i18n("No name")
|
|
}
|
|
|
|
Label {
|
|
Layout.alignment: Qt.AlignVCenter
|
|
text: replyUser ? replyUser.displayName : i18n("No name")
|
|
rightPadding: 8
|
|
}
|
|
}
|
|
}
|
|
|
|
TextEdit {
|
|
Layout.fillWidth: true
|
|
|
|
text: "<style>a{color: " + color + ";} .user-pill{}</style>" + replyContent
|
|
color: Kirigami.Theme.textColor
|
|
|
|
selectByMouse: true
|
|
readOnly: true
|
|
wrapMode: Label.Wrap
|
|
textFormat: Text.RichText
|
|
selectedTextColor: "white"
|
|
}
|
|
}
|
|
|
|
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 {
|
|
readonly property string displayText: modelData.displayName ?? modelData.unicode
|
|
readonly property bool isEmoji: modelData.unicode != null
|
|
readonly property bool highlighted: autoCompleteListView.currentIndex == index
|
|
|
|
padding: Kirigami.Units.smallSpacing
|
|
|
|
contentItem: RowLayout {
|
|
spacing: Kirigami.Units.largeSpacing
|
|
|
|
Label {
|
|
width: Kirigami.Units.gridUnit
|
|
height: Kirigami.Units.gridUnit
|
|
visible: isEmoji
|
|
text: displayText
|
|
font.family: "Emoji"
|
|
font.pointSize: 20
|
|
verticalAlignment: Text.AlignVCenter
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
|
|
Kirigami.Avatar {
|
|
Layout.preferredWidth: Kirigami.Units.gridUnit
|
|
Layout.preferredHeight: Kirigami.Units.gridUnit
|
|
source: modelData.avatarMediaId ? ("image://mxc/" + modelData.avatarMediaId) : ""
|
|
color: modelData.color ? Qt.darker(modelData.color, 1.1) : null
|
|
visible: !isEmoji
|
|
}
|
|
Label {
|
|
Layout.fillHeight: true
|
|
|
|
visible: !isEmoji
|
|
text: displayText
|
|
color: highlighted ? Kirigami.Theme.highlightTextColor : Kirigami.Theme.textColor
|
|
font.underline: highlighted
|
|
verticalAlignment: Text.AlignVCenter
|
|
rightPadding: Kirigami.Units.largeSpacing
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: {
|
|
autoCompleteListView.currentIndex = index
|
|
inputField.autoComplete();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Kirigami.Separator {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 1
|
|
visible: emojiPicker.visible || replyItem.visible || autoCompleteListView.visible
|
|
}
|
|
|
|
Image {
|
|
Layout.preferredHeight: Kirigami.Units.gridUnit * 10
|
|
source: attachmentPath
|
|
visible: hasAttachment && (attachmentPath.toString().endsWith('.png') || attachmentPath.toString().endsWith('.jpg'))
|
|
fillMode: Image.PreserveAspectFit
|
|
Layout.preferredWidth: paintedWidth
|
|
RowLayout {
|
|
anchors.right: parent.right
|
|
Button {
|
|
visible: isImage
|
|
icon.name: "document-edit"
|
|
|
|
// HACK: Use a component because an url doesn't work
|
|
Component {
|
|
id: imageEditorPage
|
|
ImageEditorPage {
|
|
imagePath: attachmentPath
|
|
}
|
|
}
|
|
onClicked: {
|
|
let imageEditor = applicationWindow().pageStack.layers.push(imageEditorPage, {
|
|
imagePath: attachmentPath
|
|
});
|
|
imageEditor.newPathChanged.connect(function(newPath) {
|
|
applicationWindow().pageStack.layers.pop();
|
|
attachmentPath = newPath;
|
|
});
|
|
}
|
|
ToolTip {
|
|
text: i18n("Edit")
|
|
}
|
|
}
|
|
Button {
|
|
icon.name: "dialog-cancel"
|
|
onClicked: {
|
|
hasAttachment = false;
|
|
attachmentPath = "";
|
|
}
|
|
ToolTip {
|
|
text: i18n("Cancel")
|
|
}
|
|
}
|
|
}
|
|
Rectangle {
|
|
color: rgba(255, 255, 255, 40)
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.bottom: parent.bottom
|
|
implicitHeight: fileLabel.implicitHeight
|
|
|
|
Label {
|
|
id: fileLabel
|
|
Layout.alignment: Qt.AlignVCenter
|
|
text: attachmentPath !== "" ? attachmentPath.toString().substring(attachmentPath.toString().lastIndexOf('/') + 1, attachmentPath.length) : ""
|
|
}
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
visible: hasAttachment && !(attachmentPath.toString().endsWith('.png') || attachmentPath.toString().endsWith('.jpg'))
|
|
ToolButton {
|
|
icon.name: "dialog-cancel"
|
|
onClicked: {
|
|
hasAttachment = false;
|
|
attachmentPath = "";
|
|
}
|
|
}
|
|
|
|
Label {
|
|
Layout.alignment: Qt.AlignVCenter
|
|
text: attachmentPath !== "" ? attachmentPath.toString().substring(attachmentPath.toString().lastIndexOf('/') + 1, attachmentPath.length) : ""
|
|
}
|
|
}
|
|
|
|
Kirigami.Separator {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 1
|
|
visible: hasAttachment
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
|
|
spacing: 0 //Kirigami.Units.smallSpacing
|
|
|
|
Button {
|
|
id: cancelReplyButton
|
|
|
|
visible: isReply
|
|
|
|
icon.name: "dialog-cancel"
|
|
|
|
onClicked: clearReply()
|
|
}
|
|
|
|
|
|
TextArea {
|
|
id: inputField
|
|
property real progress: 0
|
|
property bool autoAppeared: false
|
|
|
|
ChatDocumentHandler {
|
|
id: documentHandler
|
|
document: inputField.textDocument
|
|
cursorPosition: inputField.cursorPosition
|
|
selectionStart: inputField.selectionStart
|
|
selectionEnd: inputField.selectionEnd
|
|
room: currentRoom ?? null
|
|
}
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
wrapMode: Text.Wrap
|
|
placeholderText: i18n("Write your message...")
|
|
topPadding: 0
|
|
bottomPadding: 0
|
|
leftPadding: Kirigami.Units.smallSpacing
|
|
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 (isAutoCompleting) {
|
|
inputField.autoComplete();
|
|
|
|
isAutoCompleting = false;
|
|
return;
|
|
}
|
|
if (event.modifiers & Qt.ShiftModifier) {
|
|
insert(cursorPosition, "\n")
|
|
} else {
|
|
postMessage()
|
|
text = ""
|
|
clearReply()
|
|
closeAll()
|
|
}
|
|
}
|
|
|
|
Keys.onEscapePressed: {
|
|
clearReply();
|
|
closeAll();
|
|
}
|
|
|
|
Keys.onPressed: {
|
|
if (event.key === Qt.Key_PageDown) {
|
|
switchRoomDown();
|
|
} else if (event.key === Qt.Key_PageUp) {
|
|
switchRoomUp();
|
|
} else if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
|
|
root.pasteImage();
|
|
}
|
|
}
|
|
|
|
Keys.onBacktabPressed: {
|
|
if (event.modifiers & Qt.ControlModifier) {
|
|
switchRoomUp();
|
|
return;
|
|
}
|
|
if (isAutoCompleting) {
|
|
autoCompleteListView.decrementCurrentIndex();
|
|
}
|
|
}
|
|
|
|
Keys.onTabPressed: {
|
|
if (event.modifiers & Qt.ControlModifier) {
|
|
switchRoomDown();
|
|
return;
|
|
}
|
|
if (!isAutoCompleting) {
|
|
return;
|
|
}
|
|
|
|
// TODO detect moved cursor
|
|
|
|
// ignore first time tab was clicked so that user can select
|
|
// first emoji/user
|
|
if (autoAppeared === false) {
|
|
autoCompleteListView.incrementCurrentIndex()
|
|
} else {
|
|
autoAppeared = false;
|
|
}
|
|
|
|
inputField.autoComplete();
|
|
}
|
|
|
|
onTextChanged: {
|
|
timeoutTimer.restart()
|
|
repeatTimer.start()
|
|
currentRoom.cachedInput = text
|
|
autoAppeared = false;
|
|
|
|
const autocompletionInfo = documentHandler.getAutocompletionInfo();
|
|
|
|
if (autocompletionInfo.type === ChatDocumentHandler.Ignore) {
|
|
return;
|
|
}
|
|
if (autocompletionInfo.type === ChatDocumentHandler.None) {
|
|
isAutoCompleting = false;
|
|
autoCompleteListView.currentIndex = 0;
|
|
return;
|
|
}
|
|
|
|
if (autocompletionInfo.type === ChatDocumentHandler.User) {
|
|
autoCompleteModel = currentRoom.getUsers(autocompletionInfo.keyword);
|
|
} else {
|
|
autoCompleteModel = emojiModel.filterModel(autocompletionInfo.keyword);
|
|
}
|
|
|
|
if (autoCompleteModel.length === 0) {
|
|
isAutoCompleting = false;
|
|
autoCompleteListView.currentIndex = 0;
|
|
return;
|
|
}
|
|
isAutoCompleting = true
|
|
autoAppeared = true;
|
|
autoCompleteEndPosition = cursorPosition
|
|
}
|
|
|
|
function postMessage() {
|
|
// Qt wraps lines so we need to use a small hack
|
|
// to remove the wrapped lines but not break the empty
|
|
// lines.
|
|
const updatedText = inputField.text.trim()
|
|
.replace(/@([^: ]*):([^ ]*\.[^ ]*)/, "[@$1:$2](https://matrix.to/#/@$1:$2)");
|
|
documentHandler.postMessage(updatedText, attachmentPath, replyEventID);
|
|
clearAttachment();
|
|
currentRoom.markAllMessagesAsRead();
|
|
clear();
|
|
text = Qt.binding(function() {
|
|
return currentRoom != null ? currentRoom.cachedInput : "";
|
|
});
|
|
}
|
|
|
|
function autoComplete() {
|
|
documentHandler.replaceAutoComplete(autoCompleteListView.currentItem.displayText)
|
|
}
|
|
}
|
|
|
|
|
|
ToolButton {
|
|
id: emojiButton
|
|
icon.name: "preferences-desktop-emoticons"
|
|
icon.color: "transparent"
|
|
|
|
checkable: true
|
|
checked: emojiPicker.visible
|
|
onToggled: emojiPicker.visible = !emojiPicker.visible
|
|
|
|
ToolTip {
|
|
text: i18n("Add an Emoji")
|
|
}
|
|
}
|
|
|
|
ToolButton {
|
|
id: uploadButton
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
ToolTip {
|
|
text: i18n("Attach an image or file")
|
|
}
|
|
|
|
BusyIndicator {
|
|
anchors.fill: parent
|
|
|
|
running: currentRoom && currentRoom.hasFileUploading
|
|
}
|
|
}
|
|
|
|
ToolButton {
|
|
icon.name: "document-send"
|
|
icon.color: "transparent"
|
|
|
|
onClicked: {
|
|
inputField.postMessage()
|
|
inputField.text = ""
|
|
root.clearReply()
|
|
root.closeAll()
|
|
}
|
|
|
|
ToolTip {
|
|
text: i18n("Send message")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
background: Rectangle {
|
|
implicitHeight: 40
|
|
color: Kirigami.Theme.backgroundColor
|
|
Kirigami.Separator {
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
top: parent.top
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
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 = ""
|
|
}
|
|
|
|
function pasteImage() {
|
|
var localPath = Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/screenshots/" + (new Date()).getTime() + ".png";
|
|
if (!Clipboard.saveImage(localPath)) {
|
|
return;
|
|
}
|
|
root.attach(localPath);
|
|
}
|
|
}
|