Refactor Composer
- Have a simple HTextArea component instead of HScrollTextArea - Split composer parts between multiple files
This commit is contained in:
parent
a87cbd3bac
commit
a91a0c18f7
6
TODO.md
6
TODO.md
@ -7,12 +7,12 @@
|
|||||||
- Use new default/reset controls system
|
- Use new default/reset controls system
|
||||||
- Display name field text should be colored
|
- Display name field text should be colored
|
||||||
|
|
||||||
- Split `HScrollableTextArea` into `HTextArea` and `HScrollView` components
|
|
||||||
- Refactor `Composer`
|
|
||||||
|
|
||||||
- Drop the `HBox` `buttonModel`/`buttonCallbacks` `HBox` approach,
|
- Drop the `HBox` `buttonModel`/`buttonCallbacks` `HBox` approach,
|
||||||
be more declarative
|
be more declarative
|
||||||
|
|
||||||
|
- Reorder QML object declarations,
|
||||||
|
conform to https://doc-snapshots.qt.io/qt5-dev/qml-codingconventions.html
|
||||||
|
|
||||||
## Issues
|
## Issues
|
||||||
|
|
||||||
- SSL error on python 3.7
|
- SSL error on python 3.7
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Controls 2.12
|
import QtQuick.Controls 2.12
|
||||||
|
|
||||||
@ -25,7 +24,6 @@ ScrollView {
|
|||||||
property var saveId: "ALL"
|
property var saveId: "ALL"
|
||||||
property var saveProperties: ["text"]
|
property var saveProperties: ["text"]
|
||||||
|
|
||||||
property alias backgroundColor: textAreaBackground.color
|
|
||||||
property alias placeholderText: textArea.placeholderText
|
property alias placeholderText: textArea.placeholderText
|
||||||
property alias placeholderTextColor: textArea.placeholderTextColor
|
property alias placeholderTextColor: textArea.placeholderTextColor
|
||||||
property alias area: textArea
|
property alias area: textArea
|
||||||
|
108
src/gui/Base/HTextArea.qml
Normal file
108
src/gui/Base/HTextArea.qml
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.12
|
||||||
|
import QtQuick.Controls 2.12
|
||||||
|
|
||||||
|
TextArea {
|
||||||
|
id: textArea
|
||||||
|
|
||||||
|
|
||||||
|
property string saveName: ""
|
||||||
|
property var saveId: "ALL"
|
||||||
|
property var saveProperties: ["text"]
|
||||||
|
|
||||||
|
property var focusItemOnTab: null
|
||||||
|
property var disabledText: null
|
||||||
|
property string defaultText: ""
|
||||||
|
readonly property bool changed: text !== defaultText
|
||||||
|
|
||||||
|
property alias backgroundColor: textAreaBackground.color
|
||||||
|
|
||||||
|
|
||||||
|
function reset() { clear(); text = defaultText }
|
||||||
|
function append(text) { insert(cursorPosition, text) }
|
||||||
|
|
||||||
|
|
||||||
|
text: defaultText
|
||||||
|
opacity: enabled ? 1 : theme.disabledElementsOpacity
|
||||||
|
selectByMouse: true
|
||||||
|
leftPadding: theme.spacing
|
||||||
|
rightPadding: leftPadding
|
||||||
|
topPadding: theme.spacing / 1.5
|
||||||
|
bottomPadding: topPadding
|
||||||
|
readOnly: ! visible
|
||||||
|
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
font.family: theme.fontFamily.sans
|
||||||
|
font.pixelSize: theme.fontSize.normal
|
||||||
|
font.pointSize: -1
|
||||||
|
|
||||||
|
placeholderTextColor: theme.controls.textArea.placeholderText
|
||||||
|
color: theme.controls.textArea.text
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
id: textAreaBackground
|
||||||
|
color: theme.controls.textArea.background
|
||||||
|
radius: theme.radius
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set it only on component creation to avoid binding loops
|
||||||
|
Component.onCompleted: if (! text) {
|
||||||
|
text = window.getState(this, "text", "")
|
||||||
|
textArea.cursorPosition = text.length
|
||||||
|
}
|
||||||
|
|
||||||
|
onTextChanged: window.saveState(this)
|
||||||
|
|
||||||
|
Keys.onPressed: if (
|
||||||
|
event.modifiers & Qt.AltModifier ||
|
||||||
|
event.modifiers & Qt.MetaModifier
|
||||||
|
) event.accepted = true
|
||||||
|
|
||||||
|
KeyNavigation.priority: KeyNavigation.BeforeItem
|
||||||
|
KeyNavigation.tab: focusItemOnTab
|
||||||
|
|
||||||
|
|
||||||
|
Binding on color {
|
||||||
|
value: "transparent"
|
||||||
|
when: disabledText !== null && ! textArea.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
Binding on placeholderTextColor {
|
||||||
|
value: "transparent"
|
||||||
|
when: disabledText !== null && ! textArea.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
Binding on implicitHeight {
|
||||||
|
value: disabledTextLabel.implicitHeight
|
||||||
|
when: disabledText !== null && ! textArea.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity { HNumberAnimation {} }
|
||||||
|
Behavior on color { HColorAnimation {} }
|
||||||
|
Behavior on placeholderTextColor { HColorAnimation {} }
|
||||||
|
|
||||||
|
HLabel {
|
||||||
|
id: disabledTextLabel
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: opacity > 0
|
||||||
|
opacity: disabledText !== null && parent.enabled ? 0 : 1
|
||||||
|
text: disabledText || ""
|
||||||
|
|
||||||
|
leftPadding: parent.leftPadding
|
||||||
|
rightPadding: parent.rightPadding
|
||||||
|
topPadding: parent.topPadding
|
||||||
|
bottomPadding: parent.bottomPadding
|
||||||
|
|
||||||
|
wrapMode:
|
||||||
|
parent.wrapMode === TextEdit.Wrap ? Text.Wrap :
|
||||||
|
parent.wrapMode === TextEdit.WordWrap ? Text.WordWrap :
|
||||||
|
parent.wrapMode === TextEdit.WrapAnywhere ? Text.WrapAnywhere :
|
||||||
|
Text.NoWrap
|
||||||
|
|
||||||
|
font.family: parent.font.family
|
||||||
|
font.pixelSize: parent.font.pixelSize
|
||||||
|
|
||||||
|
Behavior on opacity { HNumberAnimation {} }
|
||||||
|
}
|
||||||
|
}
|
@ -4,8 +4,9 @@ import QtQuick 2.12
|
|||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
import "../../Base"
|
import "../../Base"
|
||||||
import "Banners"
|
import "Banners"
|
||||||
import "Timeline"
|
import "Composer"
|
||||||
import "FileTransfer"
|
import "FileTransfer"
|
||||||
|
import "Timeline"
|
||||||
|
|
||||||
HColumnPage {
|
HColumnPage {
|
||||||
id: chatPage
|
id: chatPage
|
||||||
@ -101,6 +102,7 @@ HColumnPage {
|
|||||||
LeftBanner {
|
LeftBanner {
|
||||||
id: leftBanner
|
id: leftBanner
|
||||||
visible: chat.roomInfo.left
|
visible: chat.roomInfo.left
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,6 +111,8 @@ HColumnPage {
|
|||||||
eventList: loadEventList ? eventListLoader.item.eventList : null
|
eventList: loadEventList ? eventListLoader.item.eventList : null
|
||||||
visible:
|
visible:
|
||||||
! chat.roomInfo.left && ! chat.roomInfo.inviter_id
|
! chat.roomInfo.left && ! chat.roomInfo.inviter_id
|
||||||
}
|
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumHeight: parent.height / 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,276 +0,0 @@
|
|||||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
||||||
|
|
||||||
import QtQuick 2.12
|
|
||||||
import QtQuick.Layouts 1.12
|
|
||||||
import Clipboard 0.1
|
|
||||||
import CppUtils 0.1
|
|
||||||
import "../.."
|
|
||||||
import "../../Base"
|
|
||||||
import "../../Dialogs"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: composer
|
|
||||||
color: theme.chat.composer.background
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.minimumHeight: theme.baseElementsHeight
|
|
||||||
Layout.preferredHeight: areaScrollView.implicitHeight
|
|
||||||
Layout.maximumHeight: pageLoader.height / 2
|
|
||||||
|
|
||||||
|
|
||||||
property HListView eventList
|
|
||||||
|
|
||||||
property string indent: " "
|
|
||||||
|
|
||||||
property var aliases: {
|
|
||||||
const obj = {}
|
|
||||||
|
|
||||||
for (const [id, alia] of Object.entries(window.settings.writeAliases)){
|
|
||||||
const room = ModelStore.get(id, "rooms").find(chat.roomId)
|
|
||||||
if (room &&
|
|
||||||
! room.inviter_id && ! room.left && room.can_send_messages)
|
|
||||||
obj[id] = alia
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
property string toSend: ""
|
|
||||||
|
|
||||||
property string writingUserId: chat.userId
|
|
||||||
property QtObject writingUserInfo:
|
|
||||||
ModelStore.get("accounts").find(writingUserId)
|
|
||||||
|
|
||||||
property bool textChangedSinceLostFocus: false
|
|
||||||
|
|
||||||
property alias textArea: areaScrollView.area
|
|
||||||
|
|
||||||
readonly property int cursorPosition:
|
|
||||||
textArea.cursorPosition
|
|
||||||
|
|
||||||
readonly property int cursorY:
|
|
||||||
textArea.text.substring(0, cursorPosition).split("\n").length - 1
|
|
||||||
|
|
||||||
readonly property int cursorX:
|
|
||||||
cursorPosition - lines.slice(0, cursorY).join("").length - cursorY
|
|
||||||
|
|
||||||
readonly property var lines: textArea.text.split("\n")
|
|
||||||
readonly property string lineText: lines[cursorY] || ""
|
|
||||||
|
|
||||||
readonly property string lineTextUntilCursor:
|
|
||||||
lineText.substring(0, cursorX)
|
|
||||||
|
|
||||||
// readonly property int deleteCharsOnBackspace:
|
|
||||||
// lineTextUntilCursor.match(/^ +$/) ?
|
|
||||||
// lineTextUntilCursor.match(/ {1,4}/g).slice(-1)[0].length :
|
|
||||||
// 1
|
|
||||||
|
|
||||||
|
|
||||||
function takeFocus() { areaScrollView.forceActiveFocus() }
|
|
||||||
|
|
||||||
|
|
||||||
HRowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
HUserAvatar {
|
|
||||||
id: avatar
|
|
||||||
userId: writingUserId
|
|
||||||
displayName: writingUserInfo ? writingUserInfo.display_name : ""
|
|
||||||
mxc: writingUserInfo ? writingUserInfo.avatar_url : ""
|
|
||||||
radius: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
HScrollableTextArea {
|
|
||||||
id: areaScrollView
|
|
||||||
saveName: "composer"
|
|
||||||
saveId: [chat.roomId, writingUserId]
|
|
||||||
|
|
||||||
enabled: chat.roomInfo.can_send_messages
|
|
||||||
disabledText:
|
|
||||||
qsTr("You do not have permission to post in this room")
|
|
||||||
placeholderText: qsTr("Type a message...")
|
|
||||||
|
|
||||||
backgroundColor: "transparent"
|
|
||||||
area.tabStopDistance: 4 * 4 // 4 spaces
|
|
||||||
area.focus: true
|
|
||||||
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
|
|
||||||
function setTyping(typing) {
|
|
||||||
py.callClientCoro(
|
|
||||||
writingUserId,
|
|
||||||
"room_typing",
|
|
||||||
[chat.roomId, typing, 5000]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
onTextChanged: {
|
|
||||||
if (utils.isEmptyObject(aliases)) {
|
|
||||||
writingUserId = Qt.binding(() => chat.userId)
|
|
||||||
toSend = text
|
|
||||||
setTyping(Boolean(text))
|
|
||||||
textChangedSinceLostFocus = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let foundAlias = null
|
|
||||||
|
|
||||||
for (const [user, writing_alias] of Object.entries(aliases)) {
|
|
||||||
if (text.startsWith(writing_alias + " ")) {
|
|
||||||
writingUserId = user
|
|
||||||
foundAlias = new RegExp("^" + writing_alias + " ")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundAlias) {
|
|
||||||
toSend = text.replace(foundAlias, "")
|
|
||||||
setTyping(Boolean(text))
|
|
||||||
textChangedSinceLostFocus = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
writingUserId = Qt.binding(() => chat.userId)
|
|
||||||
toSend = text
|
|
||||||
|
|
||||||
const vals = Object.values(aliases)
|
|
||||||
|
|
||||||
const longestAlias =
|
|
||||||
vals.reduce((a, b) => a.length > b.length ? a: b)
|
|
||||||
|
|
||||||
const textNotStartsWithAnyAlias =
|
|
||||||
! vals.some(a => a.startsWith(text))
|
|
||||||
|
|
||||||
const textContainsCharNotInAnyAlias =
|
|
||||||
vals.every(a => text.split("").some(c => ! a.includes(c)))
|
|
||||||
|
|
||||||
// Only set typing when it's sure that the user will not use
|
|
||||||
// an alias and has written something
|
|
||||||
if (toSend &&
|
|
||||||
(text.length > longestAlias.length ||
|
|
||||||
textNotStartsWithAnyAlias ||
|
|
||||||
textContainsCharNotInAnyAlias))
|
|
||||||
{
|
|
||||||
setTyping(Boolean(text))
|
|
||||||
textChangedSinceLostFocus = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
area.onEditingFinished: { // when lost focus
|
|
||||||
if (text && textChangedSinceLostFocus) {
|
|
||||||
setTyping(false)
|
|
||||||
textChangedSinceLostFocus = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
area.Keys.onEscapePressed.connect(ev => {
|
|
||||||
if (chat.replyToEventId) {
|
|
||||||
chat.replyToEventId = ""
|
|
||||||
chat.replyToUserId = ""
|
|
||||||
chat.replyToDisplayName = ""
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
area.Keys.onReturnPressed.connect(ev => {
|
|
||||||
ev.accepted = true
|
|
||||||
|
|
||||||
if (ev.modifiers & Qt.ShiftModifier ||
|
|
||||||
ev.modifiers & Qt.ControlModifier ||
|
|
||||||
ev.modifiers & Qt.AltModifier)
|
|
||||||
{
|
|
||||||
let indents = 0
|
|
||||||
const parts = lineText.split(indent)
|
|
||||||
|
|
||||||
for (const [i, part] of parts.entries()) {
|
|
||||||
if (i === parts.length - 1 || part) { break }
|
|
||||||
indents += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const add = indent.repeat(indents)
|
|
||||||
textArea.insert(cursorPosition, "\n" + add)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textArea.text === "") { return }
|
|
||||||
|
|
||||||
const args = [chat.roomId, toSend, chat.replyToEventId]
|
|
||||||
py.callClientCoro(writingUserId, "send_text", args)
|
|
||||||
|
|
||||||
area.clear()
|
|
||||||
chat.replyToEventId = ""
|
|
||||||
chat.replyToUserId = ""
|
|
||||||
chat.replyToDisplayName = ""
|
|
||||||
})
|
|
||||||
|
|
||||||
area.Keys.onEnterPressed.connect(area.Keys.onReturnPressed)
|
|
||||||
|
|
||||||
area.Keys.onTabPressed.connect(ev => {
|
|
||||||
ev.accepted = true
|
|
||||||
textArea.insert(cursorPosition, indent)
|
|
||||||
})
|
|
||||||
|
|
||||||
area.Keys.onPressed.connect(ev => {
|
|
||||||
if (ev.matches(StandardKey.Copy) &&
|
|
||||||
! area.selectedText &&
|
|
||||||
eventList &&
|
|
||||||
(eventList.selectedCount ||
|
|
||||||
eventList.currentIndex !== -1)) {
|
|
||||||
|
|
||||||
ev.accepted = true
|
|
||||||
eventList.copySelectedDelegates()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: buggy
|
|
||||||
// if (ev.modifiers === Qt.NoModifier &&
|
|
||||||
// ev.key === Qt.Key_Backspace &&
|
|
||||||
// ! textArea.selectedText)
|
|
||||||
// {
|
|
||||||
// ev.accepted = true
|
|
||||||
// textArea.remove(
|
|
||||||
// cursorPosition - deleteCharsOnBackspace,
|
|
||||||
// cursorPosition
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HButton {
|
|
||||||
enabled: chat.roomInfo.can_send_messages
|
|
||||||
icon.name: "upload-file"
|
|
||||||
backgroundColor: theme.chat.composer.uploadButton.background
|
|
||||||
toolTip.text:
|
|
||||||
chat.userInfo.max_upload_size ?
|
|
||||||
qsTr("Send files (%1 max)").arg(
|
|
||||||
CppUtils.formattedBytes(chat.userInfo.max_upload_size, 0),
|
|
||||||
) :
|
|
||||||
qsTr("Send files")
|
|
||||||
|
|
||||||
onClicked: sendFilePicker.dialog.open()
|
|
||||||
|
|
||||||
Layout.fillHeight: true
|
|
||||||
|
|
||||||
HShortcut {
|
|
||||||
sequences: window.settings.keys.sendFileFromPathInClipboard
|
|
||||||
onActivated: utils.sendFile(
|
|
||||||
chat.userId, chat.roomId, Clipboard.text.trim(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
SendFilePicker {
|
|
||||||
id: sendFilePicker
|
|
||||||
userId: chat.userId
|
|
||||||
roomId: chat.roomId
|
|
||||||
|
|
||||||
HShortcut {
|
|
||||||
sequences: window.settings.keys.sendFile
|
|
||||||
onActivated: sendFilePicker.dialog.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
51
src/gui/Pages/Chat/Composer/Composer.qml
Normal file
51
src/gui/Pages/Chat/Composer/Composer.qml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.12
|
||||||
|
import QtQuick.Controls 2.12
|
||||||
|
import QtQuick.Layouts 1.12
|
||||||
|
import "../../../Base"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property alias eventList: messageArea.eventList
|
||||||
|
|
||||||
|
|
||||||
|
function takeFocus() { messageArea.forceActiveFocus() }
|
||||||
|
|
||||||
|
|
||||||
|
implicitHeight:
|
||||||
|
Math.max(theme.baseElementsHeight, messageArea.implicitHeight)
|
||||||
|
|
||||||
|
color: theme.chat.composer.background
|
||||||
|
|
||||||
|
|
||||||
|
HRowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
HUserAvatar {
|
||||||
|
id: avatar
|
||||||
|
radius: 0
|
||||||
|
userId: messageArea.writingUserId
|
||||||
|
|
||||||
|
mxc:
|
||||||
|
messageArea.writingUserInfo ?
|
||||||
|
messageArea.writingUserInfo.avatar_url :
|
||||||
|
""
|
||||||
|
|
||||||
|
displayName:
|
||||||
|
messageArea.writingUserInfo ?
|
||||||
|
messageArea.writingUserInfo.display_name :
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
MessageArea { id: messageArea}
|
||||||
|
}
|
||||||
|
|
||||||
|
UploadButton {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
205
src/gui/Pages/Chat/Composer/MessageArea.qml
Normal file
205
src/gui/Pages/Chat/Composer/MessageArea.qml
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.12
|
||||||
|
import "../../.."
|
||||||
|
import "../../../Base"
|
||||||
|
|
||||||
|
HTextArea {
|
||||||
|
id: textArea
|
||||||
|
|
||||||
|
|
||||||
|
property HListView eventList
|
||||||
|
|
||||||
|
property string indent: " "
|
||||||
|
|
||||||
|
property string toSend: ""
|
||||||
|
property bool textChangedSinceLostFocus: false
|
||||||
|
property string writingUserId: chat.userId
|
||||||
|
|
||||||
|
readonly property QtObject writingUserInfo:
|
||||||
|
ModelStore.get("accounts").find(writingUserId)
|
||||||
|
|
||||||
|
readonly property int cursorY:
|
||||||
|
textArea.text.substring(0, cursorPosition).split("\n").length - 1
|
||||||
|
|
||||||
|
readonly property int cursorX:
|
||||||
|
cursorPosition - lines.slice(0, cursorY).join("").length - cursorY
|
||||||
|
|
||||||
|
readonly property var lines: textArea.text.split("\n")
|
||||||
|
readonly property string lineText: lines[cursorY] || ""
|
||||||
|
|
||||||
|
readonly property string lineTextUntilCursor:
|
||||||
|
lineText.substring(0, cursorX)
|
||||||
|
|
||||||
|
// readonly property int deleteCharsOnBackspace:
|
||||||
|
// lineTextUntilCursor.match(/^ +$/) ?
|
||||||
|
// lineTextUntilCursor.match(/ {1,4}/g).slice(-1)[0].length :
|
||||||
|
// 1
|
||||||
|
|
||||||
|
readonly property var usableAliases: {
|
||||||
|
const obj = {}
|
||||||
|
|
||||||
|
// Get accounts that are members of this room with permission to talk
|
||||||
|
for (const [id, alia] of Object.entries(window.settings.writeAliases)){
|
||||||
|
const room = ModelStore.get(id, "rooms").find(chat.roomId)
|
||||||
|
if (room &&
|
||||||
|
! room.inviter_id && ! room.left && room.can_send_messages)
|
||||||
|
obj[id] = alia
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function setTyping(typing) {
|
||||||
|
py.callClientCoro(
|
||||||
|
writingUserId, "room_typing", [chat.roomId, typing, 5000],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearReplyTo() {
|
||||||
|
if (! chat.replyToEventId) return
|
||||||
|
|
||||||
|
chat.replyToEventId = ""
|
||||||
|
chat.replyToUserId = ""
|
||||||
|
chat.replyToDisplayName = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNewLine() {
|
||||||
|
let indents = 0
|
||||||
|
const parts = lineText.split(indent)
|
||||||
|
|
||||||
|
for (const [i, part] of parts.entries()) {
|
||||||
|
if (i === parts.length - 1 || part) { break }
|
||||||
|
indents += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const add = indent.repeat(indents)
|
||||||
|
textArea.append("\n" + add)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendText() {
|
||||||
|
if (! toSend) return
|
||||||
|
|
||||||
|
const args = [chat.roomId, toSend, chat.replyToEventId]
|
||||||
|
py.callClientCoro(writingUserId, "send_text", args)
|
||||||
|
|
||||||
|
textArea.clear()
|
||||||
|
clearReplyTo()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
saveName: "composer"
|
||||||
|
saveId: [chat.roomId, writingUserId]
|
||||||
|
|
||||||
|
enabled: chat.roomInfo.can_send_messages
|
||||||
|
disabledText: qsTr("You do not have permission to post in this room")
|
||||||
|
placeholderText: qsTr("Type a message...")
|
||||||
|
|
||||||
|
backgroundColor: "transparent"
|
||||||
|
tabStopDistance: 4 * 4 // 4 spaces
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
// TODO: make this more declarative
|
||||||
|
onTextChanged: {
|
||||||
|
if (utils.isEmptyObject(usableAliases)) {
|
||||||
|
writingUserId = Qt.binding(() => chat.userId)
|
||||||
|
toSend = text
|
||||||
|
setTyping(Boolean(text))
|
||||||
|
textChangedSinceLostFocus = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let foundAlias = null
|
||||||
|
|
||||||
|
for (const [user, writing_alias] of Object.entries(usableAliases)) {
|
||||||
|
if (text.startsWith(writing_alias + " ")) {
|
||||||
|
writingUserId = user
|
||||||
|
foundAlias = new RegExp("^" + writing_alias + " ")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundAlias) {
|
||||||
|
toSend = text.replace(foundAlias, "")
|
||||||
|
setTyping(Boolean(text))
|
||||||
|
textChangedSinceLostFocus = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writingUserId = Qt.binding(() => chat.userId)
|
||||||
|
toSend = text
|
||||||
|
|
||||||
|
const vals = Object.values(usableAliases)
|
||||||
|
|
||||||
|
const longestAlias =
|
||||||
|
vals.reduce((a, b) => a.length > b.length ? a: b)
|
||||||
|
|
||||||
|
const textNotStartsWithAnyAlias =
|
||||||
|
! vals.some(a => a.startsWith(text))
|
||||||
|
|
||||||
|
const textContainsCharNotInAnyAlias =
|
||||||
|
vals.every(a => text.split("").some(c => ! a.includes(c)))
|
||||||
|
|
||||||
|
// Only set typing when it's sure that the user will not use
|
||||||
|
// an alias and has written something
|
||||||
|
if (toSend &&
|
||||||
|
(text.length > longestAlias.length ||
|
||||||
|
textNotStartsWithAnyAlias ||
|
||||||
|
textContainsCharNotInAnyAlias))
|
||||||
|
{
|
||||||
|
setTyping(Boolean(text))
|
||||||
|
textChangedSinceLostFocus = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditingFinished: { // when focus is lost
|
||||||
|
if (text && textChangedSinceLostFocus) {
|
||||||
|
setTyping(false)
|
||||||
|
textChangedSinceLostFocus = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onEscapePressed: clearReplyTo()
|
||||||
|
|
||||||
|
Keys.onReturnPressed: ev => {
|
||||||
|
ev.accepted = true
|
||||||
|
|
||||||
|
ev.modifiers & Qt.ShiftModifier ||
|
||||||
|
ev.modifiers & Qt.ControlModifier ||
|
||||||
|
ev.modifiers & Qt.AltModifier ?
|
||||||
|
addNewLine() :
|
||||||
|
sendText()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onEnterPressed: ev => Keys.returnPressed(ev)
|
||||||
|
|
||||||
|
Keys.onTabPressed: ev => {
|
||||||
|
ev.accepted = true
|
||||||
|
textArea.append(indent)
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: ev => {
|
||||||
|
if (ev.matches(StandardKey.Copy) &&
|
||||||
|
! textArea.selectedText &&
|
||||||
|
eventList &&
|
||||||
|
(eventList.selectedCount || eventList.currentIndex !== -1))
|
||||||
|
{
|
||||||
|
ev.accepted = true
|
||||||
|
eventList.copySelectedDelegates()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: buggy
|
||||||
|
// if (ev.modifiers === Qt.NoModifier &&
|
||||||
|
// ev.key === Qt.Key_Backspace &&
|
||||||
|
// ! textArea.selectedText)
|
||||||
|
// {
|
||||||
|
// ev.accepted = true
|
||||||
|
// textArea.remove(
|
||||||
|
// cursorPosition - deleteCharsOnBackspace,
|
||||||
|
// cursorPosition
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
40
src/gui/Pages/Chat/Composer/UploadButton.qml
Normal file
40
src/gui/Pages/Chat/Composer/UploadButton.qml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.12
|
||||||
|
import Clipboard 0.1
|
||||||
|
import CppUtils 0.1
|
||||||
|
import "../../../Base"
|
||||||
|
import "../../../Dialogs"
|
||||||
|
|
||||||
|
HButton {
|
||||||
|
enabled: chat.roomInfo.can_send_messages
|
||||||
|
icon.name: "upload-file"
|
||||||
|
backgroundColor: theme.chat.composer.uploadButton.background
|
||||||
|
toolTip.text:
|
||||||
|
chat.userInfo.max_upload_size ?
|
||||||
|
qsTr("Send files (%1 max)").arg(
|
||||||
|
CppUtils.formattedBytes(chat.userInfo.max_upload_size, 0),
|
||||||
|
) :
|
||||||
|
qsTr("Send files")
|
||||||
|
|
||||||
|
onClicked: sendFilePicker.dialog.open()
|
||||||
|
|
||||||
|
|
||||||
|
HShortcut {
|
||||||
|
sequences: window.settings.keys.sendFileFromPathInClipboard
|
||||||
|
onActivated: utils.sendFile(
|
||||||
|
chat.userId, chat.roomId, Clipboard.text.trim(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SendFilePicker {
|
||||||
|
id: sendFilePicker
|
||||||
|
userId: chat.userId
|
||||||
|
roomId: chat.roomId
|
||||||
|
|
||||||
|
HShortcut {
|
||||||
|
sequences: window.settings.keys.sendFile
|
||||||
|
onActivated: sendFilePicker.dialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user