Refactor input stuff
This is the start of a significant refactoring of everything related to sending messages, which is roughly:
- the chatbox
- action handling
- message sending on the c++ side
- autocompletion of users/rooms/emojis/commands/things i forgot
Notable changes so far include:
- ChatBox is now a ColumnLayout. As part of this, i removed the height animations for now. <del>as far as i can tell, they were broken anyway.</del> I'll readd them later
- Actions were refactored to live outside of the message sending function and are now each an object; it's mostly a wrapper around a function that is executed when the action is invoked
- Everything that used to live in ChatBoxHelper is now in NeoChatRoom; that means that the exact input status (text, message being replied to, message being edited, attachment) is now saved between room switching).
- To edit/reply an event, set `NeoChatRoom::chatBox{edit,reply}Id` to the desired event id, `NeoChatRoom::chatBox{reply,edit}{User,Message}` will then be updated automatically
- Attachments behave equivalently with `NeoChatRoom::chatBoxAttachmentPath`
- Error message reporting from ActionsHandler has been fixed (same fix as in !517) and moved to NeoChatRoom
Broken at the moment:
- [x] Any kind of autocompletion
- [x] Mentions
- [x] Fancy effects
- [x] sed-style edits
- [x] last-user-message edits and replies
- [x] Some of the actions, probably
- [x] Replies from notifications
- [x] Lots of keyboard shortcuts
- [x] Custom emojis
- [x] ChatBox height animations
TODO:
- [x] User / room mentions based on QTextCursors instead of the hack we currently use
- [x] Refactor autocompletion stuff
- [x] ???
- [x] Profit
This commit is contained in:
@@ -13,12 +13,12 @@ Dependencies:
|
|||||||
'frameworks/kitemmodels': '@stable'
|
'frameworks/kitemmodels': '@stable'
|
||||||
'frameworks/knotifications': '@stable'
|
'frameworks/knotifications': '@stable'
|
||||||
'libraries/kquickimageeditor': '@stable'
|
'libraries/kquickimageeditor': '@stable'
|
||||||
|
'frameworks/sonnet': '@stable'
|
||||||
- 'on': ['Windows', 'Linux', 'FreeBSD']
|
- 'on': ['Windows', 'Linux', 'FreeBSD']
|
||||||
'require':
|
'require':
|
||||||
'frameworks/qqc2-desktop-style': '@stable'
|
'frameworks/qqc2-desktop-style': '@stable'
|
||||||
'frameworks/kio': '@stable'
|
'frameworks/kio': '@stable'
|
||||||
'frameworks/kwindowsystem': '@stable'
|
'frameworks/kwindowsystem': '@stable'
|
||||||
'frameworks/sonnet': '@stable'
|
|
||||||
'frameworks/kconfigwidgets': '@stable'
|
'frameworks/kconfigwidgets': '@stable'
|
||||||
- 'on': ['Linux', 'FreeBSD']
|
- 'on': ['Linux', 'FreeBSD']
|
||||||
'require':
|
'require':
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES
|
|||||||
TYPE REQUIRED
|
TYPE REQUIRED
|
||||||
PURPOSE "Basic application components"
|
PURPOSE "Basic application components"
|
||||||
)
|
)
|
||||||
find_package(KF5 ${KF5_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons)
|
find_package(KF5 ${KF5_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet)
|
||||||
set_package_properties(KF5 PROPERTIES
|
set_package_properties(KF5 PROPERTIES
|
||||||
TYPE REQUIRED
|
TYPE REQUIRED
|
||||||
PURPOSE "Basic application components"
|
PURPOSE "Basic application components"
|
||||||
@@ -76,7 +76,6 @@ else()
|
|||||||
set_package_properties(KF5QQC2DesktopStyle PROPERTIES
|
set_package_properties(KF5QQC2DesktopStyle PROPERTIES
|
||||||
TYPE RUNTIME
|
TYPE RUNTIME
|
||||||
)
|
)
|
||||||
ecm_find_qmlmodule(org.kde.sonnet 1.0)
|
|
||||||
ecm_find_qmlmodule(org.kde.syntaxhighlighting 1.0)
|
ecm_find_qmlmodule(org.kde.syntaxhighlighting 1.0)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|||||||
@@ -12,16 +12,17 @@ import org.kde.neochat 1.0
|
|||||||
import NeoChat.Page 1.0
|
import NeoChat.Page 1.0
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: root
|
id: attachmentPaneLoader
|
||||||
|
|
||||||
property var attachmentMimetype: FileType.mimeTypeForUrl(chatBoxHelper.attachmentPath)
|
readonly property var attachmentMimetype: FileType.mimeTypeForUrl(attachmentPaneLoader.attachmentPath)
|
||||||
readonly property bool hasImage: attachmentMimetype.valid && FileType.supportedImageFormats.includes(attachmentMimetype.preferredSuffix)
|
readonly property bool hasImage: attachmentMimetype.valid && FileType.supportedImageFormats.includes(attachmentMimetype.preferredSuffix)
|
||||||
|
readonly property string attachmentPath: currentRoom.chatBoxAttachmentPath
|
||||||
|
readonly property string baseFileName: attachmentPath.substring(attachmentPath.lastIndexOf('/') + 1, attachmentPath.length)
|
||||||
|
|
||||||
active: visible
|
active: visible
|
||||||
sourceComponent: Component {
|
sourceComponent: Component {
|
||||||
Pane {
|
Pane {
|
||||||
id: attachmentPane
|
id: attachmentPane
|
||||||
property string baseFileName: chatBoxHelper.attachmentPath.toString().substring(chatBoxHelper.attachmentPath.toString().lastIndexOf('/') + 1, chatBoxHelper.attachmentPath.length)
|
|
||||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
|
|
||||||
contentItem: Item {
|
contentItem: Item {
|
||||||
@@ -45,8 +46,8 @@ Loader {
|
|||||||
width: Math.min(implicitWidth, attachmentPane.availableWidth)
|
width: Math.min(implicitWidth, attachmentPane.availableWidth)
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
cache: false // Cache is not needed. Images will rarely be shown repeatedly.
|
cache: false // Cache is not needed. Images will rarely be shown repeatedly.
|
||||||
smooth: height == preferredHeight && parent.height == parent.implicitHeight // Don't smooth until height animation stops
|
smooth: height === preferredHeight && parent.height === parent.implicitHeight // Don't smooth until height animation stops
|
||||||
source: hasImage ? chatBoxHelper.attachmentPath : ""
|
source: hasImage ? attachmentPaneLoader.attachmentPath : ""
|
||||||
visible: hasImage
|
visible: hasImage
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
|
|
||||||
@@ -152,7 +153,7 @@ Loader {
|
|||||||
Item {
|
Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
Button {
|
ToolButton {
|
||||||
id: editImageButton
|
id: editImageButton
|
||||||
visible: hasImage
|
visible: hasImage
|
||||||
icon.name: "document-edit"
|
icon.name: "document-edit"
|
||||||
@@ -162,25 +163,25 @@ Loader {
|
|||||||
Component {
|
Component {
|
||||||
id: imageEditorPage
|
id: imageEditorPage
|
||||||
ImageEditorPage {
|
ImageEditorPage {
|
||||||
imagePath: chatBoxHelper.attachmentPath
|
imagePath: attachmentPaneLoader.attachmentPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClicked: {
|
onClicked: {
|
||||||
let imageEditor = applicationWindow().pageStack.layers.push(imageEditorPage);
|
let imageEditor = applicationWindow().pageStack.layers.push(imageEditorPage);
|
||||||
imageEditor.newPathChanged.connect(function(newPath) {
|
imageEditor.newPathChanged.connect(function(newPath) {
|
||||||
applicationWindow().pageStack.layers.pop();
|
applicationWindow().pageStack.layers.pop();
|
||||||
chatBoxHelper.attachmentPath = newPath;
|
attachmentPaneLoader.attachmentPath = newPath;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ToolTip.text: text
|
ToolTip.text: text
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
}
|
}
|
||||||
Button {
|
ToolButton {
|
||||||
id: cancelAttachmentButton
|
id: cancelAttachmentButton
|
||||||
icon.name: "dialog-cancel"
|
icon.name: "dialog-close"
|
||||||
text: i18n("Cancel")
|
text: i18n("Cancel sending Image")
|
||||||
display: AbstractButton.IconOnly
|
display: AbstractButton.IconOnly
|
||||||
onClicked: chatBoxHelper.clearAttachment();
|
onClicked: currentRoom.chatBoxAttachmentPath = "";
|
||||||
ToolTip.text: text
|
ToolTip.text: text
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
import QtQuick 2.15
|
import QtQuick 2.15
|
||||||
import QtQuick.Layouts 1.15
|
import QtQuick.Layouts 1.15
|
||||||
import QtQuick.Controls 2.15
|
import QtQuick.Controls 2.15
|
||||||
import QtQuick.Templates 2.15 as T
|
|
||||||
import Qt.labs.platform 1.1 as Platform
|
|
||||||
import QtQuick.Window 2.15
|
import QtQuick.Window 2.15
|
||||||
|
|
||||||
import org.kde.kirigami 2.18 as Kirigami
|
import org.kde.kirigami 2.18 as Kirigami
|
||||||
@@ -14,25 +12,13 @@ import org.kde.neochat 1.0
|
|||||||
|
|
||||||
ToolBar {
|
ToolBar {
|
||||||
id: chatBar
|
id: chatBar
|
||||||
property string replyEventId: ""
|
|
||||||
property string editEventId: ""
|
|
||||||
property alias inputFieldText: inputField.text
|
property alias inputFieldText: inputField.text
|
||||||
property alias textField: inputField
|
property alias textField: inputField
|
||||||
property alias emojiPaneOpened: emojiButton.checked
|
property alias emojiPaneOpened: emojiButton.checked
|
||||||
|
|
||||||
// store each user we autoComplete here, this will be helpful later to generate
|
|
||||||
// the matrix.to links.
|
|
||||||
// This use an hack to define: https://doc.qt.io/qt-5/qml-var.html#property-value-initialization-semantics
|
|
||||||
property var userAutocompleted: ({})
|
|
||||||
|
|
||||||
signal closeAllTriggered()
|
signal closeAllTriggered()
|
||||||
signal inputFieldForceActiveFocusTriggered()
|
signal inputFieldForceActiveFocusTriggered()
|
||||||
signal messageSent()
|
signal messageSent()
|
||||||
signal pasteImageTriggered()
|
|
||||||
signal editLastUserMessage()
|
|
||||||
signal replyPreviousUserMessage()
|
|
||||||
|
|
||||||
property alias isCompleting: completionMenu.visible
|
|
||||||
|
|
||||||
onInputFieldForceActiveFocusTriggered: {
|
onInputFieldForceActiveFocusTriggered: {
|
||||||
inputField.forceActiveFocus();
|
inputField.forceActiveFocus();
|
||||||
@@ -92,12 +78,7 @@ ToolBar {
|
|||||||
topPadding: 0
|
topPadding: 0
|
||||||
bottomPadding: 0
|
bottomPadding: 0
|
||||||
|
|
||||||
property real progress: 0
|
placeholderText: readOnly ? i18n("This room is encrypted. Sending encrypted messages is not yet supported.") : currentRoom.chatBoxEditId.length > 0 ? i18n("Edit Message") : currentRoom.usesEncryption ? i18n("Send an encrypted message…") : i18n("Send a message…")
|
||||||
property bool autoAppeared: false
|
|
||||||
//property int lineHeight: contentHeight / lineCount
|
|
||||||
|
|
||||||
text: inputFieldText
|
|
||||||
placeholderText: readOnly ? i18n("This room is encrypted. Sending encrypted messages is not yet supported.") : editEventId.length > 0 ? i18n("Edit Message") : currentRoom.usesEncryption ? i18n("Send an encrypted message…") : i18n("Send a message…")
|
|
||||||
verticalAlignment: TextEdit.AlignVCenter
|
verticalAlignment: TextEdit.AlignVCenter
|
||||||
horizontalAlignment: TextEdit.AlignLeft
|
horizontalAlignment: TextEdit.AlignLeft
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
@@ -105,7 +86,6 @@ ToolBar {
|
|||||||
|
|
||||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
Kirigami.Theme.inherit: false
|
Kirigami.Theme.inherit: false
|
||||||
Kirigami.SpellChecking.enabled: true
|
|
||||||
|
|
||||||
color: Kirigami.Theme.textColor
|
color: Kirigami.Theme.textColor
|
||||||
selectionColor: Kirigami.Theme.highlightColor
|
selectionColor: Kirigami.Theme.highlightColor
|
||||||
@@ -114,117 +94,54 @@ ToolBar {
|
|||||||
|
|
||||||
selectByMouse: !Kirigami.Settings.tabletMode
|
selectByMouse: !Kirigami.Settings.tabletMode
|
||||||
|
|
||||||
ChatDocumentHandler {
|
Keys.onEnterPressed: {
|
||||||
id: documentHandler
|
if (completionMenu.visible) {
|
||||||
document: inputField.textDocument
|
completionMenu.complete()
|
||||||
cursorPosition: inputField.cursorPosition
|
|
||||||
selectionStart: inputField.selectionStart
|
|
||||||
selectionEnd: inputField.selectionEnd
|
|
||||||
room: currentRoom ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: repeatTimer
|
|
||||||
interval: 5000
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendMessage(event) {
|
|
||||||
if (isCompleting && completionMenu.count > 0) {
|
|
||||||
chatBar.complete();
|
|
||||||
} else if (event.modifiers & Qt.ShiftModifier) {
|
} else if (event.modifiers & Qt.ShiftModifier) {
|
||||||
inputField.insert(cursorPosition, "\n")
|
inputField.insert(cursorPosition, "\n")
|
||||||
} else {
|
} else {
|
||||||
currentRoom.sendTypingNotification(false)
|
chatBar.postMessage();
|
||||||
chatBar.postMessage()
|
}
|
||||||
|
}
|
||||||
|
Keys.onReturnPressed: {
|
||||||
|
if (completionMenu.visible) {
|
||||||
|
completionMenu.complete()
|
||||||
|
} else if (event.modifiers & Qt.ShiftModifier) {
|
||||||
|
inputField.insert(cursorPosition, "\n")
|
||||||
|
} else {
|
||||||
|
chatBar.postMessage();
|
||||||
}
|
}
|
||||||
isCompleting = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onReturnPressed: { sendMessage(event) }
|
Keys.onTabPressed: {
|
||||||
Keys.onEnterPressed: { sendMessage(event) }
|
if (completionMenu.visible) {
|
||||||
|
completionMenu.complete()
|
||||||
Keys.onEscapePressed: {
|
}
|
||||||
closeAllTriggered()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onPressed: {
|
Keys.onPressed: {
|
||||||
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
|
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
|
||||||
chatBar.pasteImage();
|
chatBar.pasteImage();
|
||||||
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
|
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
|
||||||
replyPreviousUserMessage();
|
let replyEvent = messageEventModel.getLatestMessageFromIndex(0)
|
||||||
|
if (replyEvent && replyEvent["event_id"]) {
|
||||||
|
currentRoom.chatBoxReplyId = replyEvent["event_id"]
|
||||||
|
}
|
||||||
} else if (event.key === Qt.Key_Up && inputField.text.length === 0) {
|
} else if (event.key === Qt.Key_Up && inputField.text.length === 0) {
|
||||||
editLastUserMessage();
|
let editEvent = messageEventModel.getLastLocalUserMessageEventId()
|
||||||
}
|
if (editEvent) {
|
||||||
}
|
currentRoom.chatBoxEditId = editEvent["event_id"]
|
||||||
|
|
||||||
Keys.onBacktabPressed: {
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
switchRoomUp();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isCompleting) {
|
|
||||||
nextItemInFocusChain(false).forceActiveFocus(Qt.TabFocusReason)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!autoAppeared) {
|
|
||||||
let decrementedIndex = completionMenu.currentIndex - 1
|
|
||||||
// Wrap around to the last item
|
|
||||||
if (decrementedIndex < 0) {
|
|
||||||
decrementedIndex = Math.max(completionMenu.count - 1, 0) // 0 if count == 0
|
|
||||||
}
|
}
|
||||||
completionMenu.currentIndex = decrementedIndex
|
} else if (event.key === Qt.Key_Up && completionMenu.visible) {
|
||||||
} else {
|
completionMenu.decrementIndex()
|
||||||
autoAppeared = false;
|
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
|
||||||
|
completionMenu.incrementIndex()
|
||||||
}
|
}
|
||||||
|
|
||||||
chatBar.complete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// yes, decrement goes up and increment goes down visually.
|
Timer {
|
||||||
Keys.onUpPressed: (event) => {
|
id: repeatTimer
|
||||||
if (chatBar.isCompleting) {
|
interval: 5000
|
||||||
event.accepted = true
|
|
||||||
completionMenu.listView.decrementCurrentIndex()
|
|
||||||
autoAppeared = true;
|
|
||||||
}
|
|
||||||
event.accepted = false
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onDownPressed: (event) => {
|
|
||||||
if (chatBar.isCompleting) {
|
|
||||||
event.accepted = true
|
|
||||||
completionMenu.listView.incrementCurrentIndex()
|
|
||||||
autoAppeared = true;
|
|
||||||
}
|
|
||||||
event.accepted = false
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onTabPressed: {
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
switchRoomDown();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isCompleting) {
|
|
||||||
nextItemInFocusChain().forceActiveFocus(Qt.TabFocusReason);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO detect moved cursor
|
|
||||||
|
|
||||||
// ignore first time tab was clicked so that user can select
|
|
||||||
// first emoji/user
|
|
||||||
if (!autoAppeared) {
|
|
||||||
let incrementedIndex = completionMenu.currentIndex + 1;
|
|
||||||
// Wrap around to the first item
|
|
||||||
if (incrementedIndex > completionMenu.count - 1) {
|
|
||||||
incrementedIndex = 0
|
|
||||||
}
|
|
||||||
completionMenu.currentIndex = incrementedIndex;
|
|
||||||
} else {
|
|
||||||
autoAppeared = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
chatBar.complete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onTextChanged: {
|
onTextChanged: {
|
||||||
@@ -233,58 +150,20 @@ ToolBar {
|
|||||||
}
|
}
|
||||||
repeatTimer.start()
|
repeatTimer.start()
|
||||||
|
|
||||||
currentRoom.cachedInput = text
|
currentRoom.chatBoxText = text
|
||||||
autoAppeared = false;
|
|
||||||
|
|
||||||
const completionInfo = documentHandler.getAutocompletionInfo(isCompleting);
|
|
||||||
|
|
||||||
if (completionInfo.type === ChatDocumentHandler.Ignore) {
|
|
||||||
if (completionInfo.keyword) {
|
|
||||||
// custom emojis
|
|
||||||
const idx = completionMenu.currentIndex;
|
|
||||||
completionMenu.model = Array.from(chatBar.customEmojiModel.filterModel(completionInfo.keyword)).concat(EmojiModel.filterModel(completionInfo.keyword))
|
|
||||||
completionMenu.currentIndex = idx;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (completionInfo.type === ChatDocumentHandler.None) {
|
|
||||||
isCompleting = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
completionMenu.completionType = completionInfo.type
|
|
||||||
if (completionInfo.type === ChatDocumentHandler.User) {
|
|
||||||
completionMenu.model = currentRoom.getUsers(completionInfo.keyword, 10);
|
|
||||||
} else if (completionInfo.type === ChatDocumentHandler.Command) {
|
|
||||||
completionMenu.model = CommandModel.filterModel(completionInfo.keyword);
|
|
||||||
} else {
|
|
||||||
completionMenu.model = Array.from(chatBar.customEmojiModel.filterModel(completionInfo.keyword)).concat(EmojiModel.filterModel(completionInfo.keyword))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (completionMenu.model.length === 0) {
|
|
||||||
isCompleting = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isCompleting) {
|
|
||||||
isCompleting = true
|
|
||||||
autoAppeared = true;
|
|
||||||
completionMenu.endPosition = cursorPosition
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
visible: !chatBoxHelper.isReplying && (!chatBoxHelper.hasAttachment || uploadingBusySpinner.running)
|
visible: currentRoom.chatBoxReplyId.length === 0 && (currentRoom.chatBoxAttachmentPath.length === 0 || uploadingBusySpinner.running)
|
||||||
implicitWidth: uploadButton.implicitWidth
|
implicitWidth: uploadButton.implicitWidth
|
||||||
implicitHeight: uploadButton.implicitHeight
|
implicitHeight: uploadButton.implicitHeight
|
||||||
ToolButton {
|
ToolButton {
|
||||||
id: uploadButton
|
id: uploadButton
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
// Matrix does not allow sending attachments in replies
|
// Matrix does not allow sending attachments in replies
|
||||||
visible: !chatBoxHelper.isReplying && !chatBoxHelper.hasAttachment && !uploadingBusySpinner.running
|
visible: currentRoom.chatBoxReplyId.length === 0 && currentRoom.chatBoxAttachmentPath.length === 0 && !uploadingBusySpinner.running
|
||||||
icon.name: "mail-attachment"
|
icon.name: "mail-attachment"
|
||||||
text: i18n("Attach an image or file")
|
text: i18n("Attach an image or file")
|
||||||
display: AbstractButton.IconOnly
|
display: AbstractButton.IconOnly
|
||||||
@@ -295,8 +174,10 @@ ToolBar {
|
|||||||
} else {
|
} else {
|
||||||
var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay)
|
var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay)
|
||||||
fileDialog.chosen.connect((path) => {
|
fileDialog.chosen.connect((path) => {
|
||||||
if (!path) { return }
|
if (!path) {
|
||||||
chatBoxHelper.attachmentPath = path;
|
return;
|
||||||
|
}
|
||||||
|
currentRoom.chatBoxAttachmentPath = path;
|
||||||
})
|
})
|
||||||
fileDialog.open()
|
fileDialog.open()
|
||||||
}
|
}
|
||||||
@@ -339,24 +220,12 @@ ToolBar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Action {
|
|
||||||
id: pasteAction
|
|
||||||
shortcut: StandardKey.Paste
|
|
||||||
onTriggered: {
|
|
||||||
if (Clipboard.hasImage) {
|
|
||||||
pasteImageTriggered();
|
|
||||||
}
|
|
||||||
activeFocusItem.paste();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CompletionMenu {
|
CompletionMenu {
|
||||||
id: completionMenu
|
id: completionMenu
|
||||||
width: parent.width
|
|
||||||
//height: 80 //Math.min(implicitHeight, delegate.implicitHeight * 6)
|
|
||||||
height: implicitHeight
|
height: implicitHeight
|
||||||
y: -height - 1
|
y: -height - 5
|
||||||
z: 1
|
z: 1
|
||||||
|
chatDocumentHandler: documentHandler
|
||||||
Behavior on height {
|
Behavior on height {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
property: "height"
|
property: "height"
|
||||||
@@ -364,59 +233,42 @@ ToolBar {
|
|||||||
easing.type: Easing.OutCubic
|
easing.type: Easing.OutCubic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onCompleteTriggered: {
|
}
|
||||||
complete()
|
|
||||||
isCompleting = false;
|
Connections {
|
||||||
|
target: currentRoom
|
||||||
|
function onChatBoxEditIdChanged() {
|
||||||
|
chatBar.inputFieldText = currentRoom.chatBoxEditMessage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property CustomEmojiModel customEmojiModel: CustomEmojiModel {
|
ChatDocumentHandler {
|
||||||
connection: Controller.activeConnection
|
id: documentHandler
|
||||||
|
document: inputField.textDocument
|
||||||
|
cursorPosition: inputField.cursorPosition
|
||||||
|
selectionStart: inputField.selectionStart
|
||||||
|
selectionEnd: inputField.selectionEnd
|
||||||
|
Component.onCompleted: {
|
||||||
|
RoomManager.chatDocumentHandler = documentHandler;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function pasteImage() {
|
function pasteImage() {
|
||||||
let localPath = Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/screenshots/" + (new Date()).getTime() + ".png";
|
let localPath = Clipboard.saveImage();
|
||||||
if (!Clipboard.saveImage(localPath)) {
|
if (localPath.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
chatBoxHelper.attachmentPath = localPath;
|
currentRoom.chatBoxAttachmentPath = localPath
|
||||||
}
|
}
|
||||||
|
|
||||||
function postMessage() {
|
function postMessage() {
|
||||||
checkForFancyEffectsReason();
|
actionsHandler.handleMessage();
|
||||||
|
|
||||||
if (chatBoxHelper.hasAttachment) {
|
|
||||||
// send attachment but don't reset the text
|
|
||||||
actionsHandler.postMessage("", chatBoxHelper.attachmentPath,
|
|
||||||
chatBoxHelper.replyEventId, chatBoxHelper.editEventId, {}, this.customEmojiModel);
|
|
||||||
currentRoom.markAllMessagesAsRead();
|
|
||||||
messageSent();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const re = /^s\/([^\/]*)\/([^\/]*)/;
|
|
||||||
if (Config.allowQuickEdit && re.test(inputField.text)) {
|
|
||||||
// send edited messages
|
|
||||||
actionsHandler.postEdit(inputField.text);
|
|
||||||
} else {
|
|
||||||
// send normal message
|
|
||||||
actionsHandler.postMessage(inputField.text.trim(), chatBoxHelper.attachmentPath,
|
|
||||||
chatBoxHelper.replyEventId, chatBoxHelper.editEventId, userAutocompleted, this.customEmojiModel);
|
|
||||||
}
|
|
||||||
currentRoom.markAllMessagesAsRead();
|
currentRoom.markAllMessagesAsRead();
|
||||||
inputField.clear();
|
inputField.clear();
|
||||||
inputField.text = Qt.binding(function() {
|
currentRoom.chatBoxReplyId = "";
|
||||||
return currentRoom ? currentRoom.cachedInput : "";
|
currentRoom.chatBoxEditId = "";
|
||||||
});
|
|
||||||
messageSent()
|
messageSent()
|
||||||
}
|
}
|
||||||
|
|
||||||
function complete() {
|
|
||||||
documentHandler.replaceAutoComplete(completionMenu.currentDisplayText);
|
|
||||||
if (completionMenu.completionType === ChatDocumentHandler.User
|
|
||||||
&& completionMenu.currentDisplayText.length > 0
|
|
||||||
&& completionMenu.currentItem.userId.length > 0) {
|
|
||||||
userAutocompleted[completionMenu.currentDisplayText] = completionMenu.currentItem.userId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,58 +4,26 @@
|
|||||||
|
|
||||||
import QtQuick 2.15
|
import QtQuick 2.15
|
||||||
import QtQuick.Controls 2.15 as QQC2
|
import QtQuick.Controls 2.15 as QQC2
|
||||||
import Qt.labs.platform 1.1 as Platform
|
import QtQuick.Layouts 1.15
|
||||||
import org.kde.kirigami 2.15 as Kirigami
|
import org.kde.kirigami 2.15 as Kirigami
|
||||||
|
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
import NeoChat.Component.ChatBox 1.0
|
import NeoChat.Component.ChatBox 1.0
|
||||||
import NeoChat.Component.Emoji 1.0
|
import NeoChat.Component.Emoji 1.0
|
||||||
|
|
||||||
Item {
|
ColumnLayout {
|
||||||
id: root
|
id: chatBox
|
||||||
|
|
||||||
property alias inputFieldText: chatBar.inputFieldText
|
property alias inputFieldText: chatBar.inputFieldText
|
||||||
|
|
||||||
signal fancyEffectsReasonFound(string fancyEffect)
|
|
||||||
signal messageSent()
|
signal messageSent()
|
||||||
signal editLastUserMessage()
|
|
||||||
signal replyPreviousUserMessage()
|
|
||||||
|
|
||||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
spacing: 0
|
||||||
|
|
||||||
implicitWidth: {
|
|
||||||
let w = 0
|
|
||||||
for(let i = 0; i < visibleChildren.length; ++i) {
|
|
||||||
w = Math.max(w, Math.ceil(visibleChildren[i].implicitWidth))
|
|
||||||
}
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
implicitHeight: {
|
|
||||||
let h = 0
|
|
||||||
for(let i = 0; i < visibleChildren.length; ++i) {
|
|
||||||
h += Math.ceil(visibleChildren[i].implicitHeight)
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// For some reason, this is needed to make the height animation work even though
|
|
||||||
// it used to work and height should be directly affected by implicitHeight
|
|
||||||
height: implicitHeight
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
property: "height"
|
|
||||||
duration: Kirigami.Units.shortDuration
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Kirigami.Separator {
|
Kirigami.Separator {
|
||||||
id: connectionPaneSeparator
|
id: connectionPaneSeparator
|
||||||
visible: connectionPane.visible
|
visible: connectionPane.visible
|
||||||
width: parent.width
|
Layout.fillWidth: true
|
||||||
height: visible ? implicitHeight : 0
|
|
||||||
anchors.bottom: connectionPane.top
|
|
||||||
z: 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.Pane {
|
QQC2.Pane {
|
||||||
@@ -71,30 +39,25 @@ Item {
|
|||||||
color: Kirigami.Theme.backgroundColor
|
color: Kirigami.Theme.backgroundColor
|
||||||
}
|
}
|
||||||
visible: !Controller.isOnline
|
visible: !Controller.isOnline
|
||||||
width: parent.width
|
Layout.fillWidth: true
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
id: networkLabel
|
id: networkLabel
|
||||||
text: i18n("NeoChat is offline. Please check your network connection.")
|
text: i18n("NeoChat is offline. Please check your network connection.")
|
||||||
}
|
}
|
||||||
anchors.bottom: emojiPickerLoaderSeparator.top
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Kirigami.Separator {
|
Kirigami.Separator {
|
||||||
id: emojiPickerLoaderSeparator
|
id: emojiPickerLoaderSeparator
|
||||||
visible: emojiPickerLoader.visible
|
visible: emojiPickerLoader.visible
|
||||||
width: parent.width
|
Layout.fillWidth: true
|
||||||
height: visible ? implicitHeight : 0
|
height: visible ? implicitHeight : 0
|
||||||
anchors.bottom: emojiPickerLoader.top
|
|
||||||
z: 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: emojiPickerLoader
|
id: emojiPickerLoader
|
||||||
active: visible
|
active: visible
|
||||||
visible: chatBar.emojiPaneOpened
|
visible: chatBar.emojiPaneOpened
|
||||||
width: parent.width
|
Layout.fillWidth: true
|
||||||
height: visible ? implicitHeight : 0
|
|
||||||
anchors.bottom: replySeparator.top
|
|
||||||
sourceComponent: QQC2.Pane {
|
sourceComponent: QQC2.Pane {
|
||||||
topPadding: 0
|
topPadding: 0
|
||||||
bottomPadding: 0
|
bottomPadding: 0
|
||||||
@@ -106,178 +69,79 @@ Item {
|
|||||||
onChosen: addText(emoji)
|
onChosen: addText(emoji)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
property: "height"
|
|
||||||
duration: Kirigami.Units.shortDuration
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Kirigami.Separator {
|
Kirigami.Separator {
|
||||||
id: replySeparator
|
id: replySeparator
|
||||||
visible: replyPane.visible
|
visible: replyPane.visible
|
||||||
width: parent.width
|
Layout.fillWidth: true
|
||||||
height: visible ? implicitHeight : 0
|
|
||||||
anchors.bottom: replyPane.top
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ReplyPane {
|
ReplyPane {
|
||||||
id: replyPane
|
id: replyPane
|
||||||
visible: chatBoxHelper.isReplying || chatBoxHelper.isEditing
|
visible: currentRoom.chatBoxReplyId.length > 0 || currentRoom.chatBoxEditId.length > 0
|
||||||
user: chatBoxHelper.replyUser
|
Layout.fillWidth: true
|
||||||
width: parent.width
|
|
||||||
height: visible ? implicitHeight : 0
|
|
||||||
anchors.bottom: attachmentSeparator.top
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
property: "height"
|
|
||||||
duration: Kirigami.Units.shortDuration
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onReplyCancelled: {
|
onReplyCancelled: {
|
||||||
root.focusInputField()
|
chatBox.focusInputField()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Kirigami.Separator {
|
Kirigami.Separator {
|
||||||
id: attachmentSeparator
|
id: attachmentSeparator
|
||||||
visible: attachmentPane.visible
|
visible: attachmentPane.visible
|
||||||
width: parent.width
|
Layout.fillWidth: true
|
||||||
height: visible ? implicitHeight : 0
|
|
||||||
anchors.bottom: attachmentPane.top
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AttachmentPane {
|
AttachmentPane {
|
||||||
id: attachmentPane
|
id: attachmentPane
|
||||||
visible: chatBoxHelper.hasAttachment
|
visible: currentRoom.chatBoxAttachmentPath.length > 0
|
||||||
width: parent.width
|
Layout.fillWidth: true
|
||||||
height: visible ? implicitHeight : 0
|
|
||||||
anchors.bottom: chatBarSeparator.top
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
property: "height"
|
|
||||||
duration: Kirigami.Units.shortDuration
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Kirigami.Separator {
|
Kirigami.Separator {
|
||||||
id: chatBarSeparator
|
id: chatBarSeparator
|
||||||
visible: chatBar.visible
|
visible: chatBar.visible
|
||||||
width: parent.width
|
|
||||||
height: visible ? implicitHeight : 0
|
Layout.fillWidth: true
|
||||||
anchors.bottom: chatBar.top
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatBar {
|
ChatBar {
|
||||||
id: chatBar
|
id: chatBar
|
||||||
visible: currentRoom.canSendEvent("m.room.message")
|
visible: currentRoom.canSendEvent("m.room.message")
|
||||||
width: parent.width
|
|
||||||
height: visible ? implicitHeight : 0
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
|
|
||||||
Behavior on height {
|
Layout.fillWidth: true
|
||||||
NumberAnimation {
|
|
||||||
property: "height"
|
|
||||||
duration: Kirigami.Units.shortDuration
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCloseAllTriggered: closeAll()
|
onCloseAllTriggered: closeAll()
|
||||||
onMessageSent: {
|
onMessageSent: {
|
||||||
closeAll()
|
closeAll()
|
||||||
checkForFancyEffectsReason()
|
chatBox.messageSent();
|
||||||
root.messageSent();
|
|
||||||
}
|
|
||||||
onEditLastUserMessage: {
|
|
||||||
root.editLastUserMessage();
|
|
||||||
}
|
|
||||||
onReplyPreviousUserMessage: {
|
|
||||||
root.replyPreviousUserMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkForFancyEffectsReason() {
|
|
||||||
if (!Config.showFancyEffects) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let text = root.inputFieldText.trim()
|
Behavior on implicitHeight {
|
||||||
if (text.includes('\u{2744}')) {
|
NumberAnimation {
|
||||||
root.fancyEffectsReasonFound("snowflake")
|
property: "implicitHeight"
|
||||||
}
|
duration: Kirigami.Units.shortDuration
|
||||||
if (text.includes('\u{1F386}')) {
|
easing.type: Easing.OutCubic
|
||||||
root.fancyEffectsReasonFound("fireworks")
|
}
|
||||||
}
|
|
||||||
if (text.includes('\u{1F387}')) {
|
|
||||||
root.fancyEffectsReasonFound("fireworks")
|
|
||||||
}
|
|
||||||
if (text.includes('\u{1F389}')) {
|
|
||||||
root.fancyEffectsReasonFound("confetti")
|
|
||||||
}
|
|
||||||
if (text.includes('\u{1F38A}')) {
|
|
||||||
root.fancyEffectsReasonFound("confetti")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addText(text) {
|
function addText(text) {
|
||||||
root.inputFieldText = inputFieldText + text
|
chatBox.inputFieldText = inputFieldText + text
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertText(str) {
|
function insertText(str) {
|
||||||
root.inputFieldText = inputFieldText.substr(0, inputField.cursorPosition) + str + inputFieldText.substr(inputField.cursorPosition)
|
chatBox.inputFieldText = inputFieldText.substr(0, inputField.cursorPosition) + str + inputFieldText.substr(inputField.cursorPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusInputField() {
|
function focusInputField() {
|
||||||
chatBar.inputFieldForceActiveFocusTriggered()
|
chatBar.inputFieldForceActiveFocusTriggered()
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: RoomManager
|
|
||||||
|
|
||||||
function onCurrentRoomChanged() {
|
|
||||||
chatBar.userAutocompleted = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: chatBoxHelper
|
|
||||||
|
|
||||||
function onShouldClearText() {
|
|
||||||
root.inputFieldText = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEditing(editContent, editFormatedContent) {
|
|
||||||
// Set the input field in edit mode
|
|
||||||
root.inputFieldText = editContent;
|
|
||||||
|
|
||||||
// clean autocompletion list
|
|
||||||
chatBar.userAutocompleted = {};
|
|
||||||
|
|
||||||
// Fill autocompletion list with values extracted from message.
|
|
||||||
// We can't just iterate on every user in the list and try to
|
|
||||||
// find matching display name since some users have display name
|
|
||||||
// matching frequent words and this will marks too many words as
|
|
||||||
// mentions.
|
|
||||||
const regex = /<a href=\"https:\/\/matrix.to\/#\/(@[a-zA-Z09]*:[a-zA-Z09.]*)\">([^<]*)<\/a>/g;
|
|
||||||
|
|
||||||
let match;
|
|
||||||
while ((match = regex.exec(editFormatedContent.toString())) !== null) {
|
|
||||||
chatBar.userAutocompleted[match[2]] = match[1];
|
|
||||||
}
|
|
||||||
chatBox.forceActiveFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeAll() {
|
function closeAll() {
|
||||||
chatBoxHelper.clear();
|
// TODO clear();
|
||||||
chatBar.emojiPaneOpened = false;
|
chatBar.emojiPaneOpened = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,154 +10,65 @@ import Qt.labs.qmlmodels 1.0
|
|||||||
import org.kde.kirigami 2.15 as Kirigami
|
import org.kde.kirigami 2.15 as Kirigami
|
||||||
|
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
import NeoChat.Component 1.0
|
|
||||||
|
|
||||||
Popup {
|
Popup {
|
||||||
id: control
|
id: completionMenu
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
// Expose internal ListView properties.
|
visible: completions.count > 0
|
||||||
property alias model: completionListView.model
|
|
||||||
property alias listView: completionListView
|
|
||||||
property alias currentIndex: completionListView.currentIndex
|
|
||||||
property alias currentItem: completionListView.currentItem
|
|
||||||
property alias count: completionListView.count
|
|
||||||
property alias delegate: completionListView.delegate
|
|
||||||
|
|
||||||
// Autocomplee text
|
RoomListModel {
|
||||||
property string currentDisplayText: currentItem && (currentItem.displayName ?? "")
|
id: roomListModel
|
||||||
|
connection: Controller.activeConnection
|
||||||
|
}
|
||||||
|
|
||||||
property int completionType: ChatDocumentHandler.Emoji
|
required property var chatDocumentHandler
|
||||||
property int beginPosition: 0
|
Component.onCompleted: {
|
||||||
property int endPosition: 0
|
chatDocumentHandler.completionModel.roomListModel = roomListModel;
|
||||||
|
}
|
||||||
|
|
||||||
signal completeTriggered()
|
function incrementIndex() {
|
||||||
|
completions.incrementCurrentIndex()
|
||||||
|
}
|
||||||
|
|
||||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
function decrementIndex() {
|
||||||
|
completions.decrementCurrentIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
function complete() {
|
||||||
|
completionMenu.chatDocumentHandler.complete(completions.currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
bottomPadding: 0
|
|
||||||
leftPadding: 0
|
leftPadding: 0
|
||||||
rightPadding: 0
|
rightPadding: 0
|
||||||
topPadding: 0
|
topPadding: 0
|
||||||
clip: true
|
bottomPadding: 0
|
||||||
|
implicitHeight: Math.min(completions.contentHeight, Kirigami.Units.gridUnit * 10)
|
||||||
|
|
||||||
onVisibleChanged: if (!visible) {
|
contentItem: ListView {
|
||||||
completionListView.currentIndex = 0;
|
id: completions
|
||||||
}
|
|
||||||
|
|
||||||
implicitHeight: Math.min(completionListView.contentHeight, Kirigami.Units.gridUnit * 10)
|
anchors.fill: parent
|
||||||
|
model: completionMenu.chatDocumentHandler.completionModel
|
||||||
contentItem: ScrollView {
|
currentIndex: 0
|
||||||
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
|
keyNavigationWraps: true
|
||||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
highlightMoveDuration: 100
|
||||||
ListView {
|
delegate: Kirigami.BasicListItem {
|
||||||
id: completionListView
|
text: model.text
|
||||||
implicitWidth: contentWidth
|
subtitle: model.subtitle ?? ""
|
||||||
delegate: {
|
leading: RowLayout {
|
||||||
if (completionType === ChatDocumentHandler.Emoji) {
|
Kirigami.Avatar {
|
||||||
emojiDelegate
|
visible: model.icon !== "invalid"
|
||||||
} else if (completionType === ChatDocumentHandler.Command) {
|
Layout.preferredWidth: height
|
||||||
commandDelegate
|
Layout.fillHeight: true
|
||||||
} else if (completionType === ChatDocumentHandler.User) {
|
source: model.icon === "invalid" ? "" : ("image://mxc/" + model.icon)
|
||||||
usernameDelegate
|
name: model.text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onClicked: completionMenu.chatDocumentHandler.complete(model.index)
|
||||||
keyNavigationWraps: true
|
|
||||||
|
|
||||||
//interactive: Window.window ? contentHeight + control.topPadding + control.bottomPadding > Window.window.height : false
|
|
||||||
clip: true
|
|
||||||
currentIndex: control.currentIndex || 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: Kirigami.Theme.backgroundColor
|
color: Kirigami.Theme.backgroundColor
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
|
||||||
id: usernameDelegate
|
|
||||||
Kirigami.BasicListItem {
|
|
||||||
id: usernameItem
|
|
||||||
width: ListView.view.width ?? implicitWidth
|
|
||||||
property string displayName: modelData.displayName
|
|
||||||
property string userId: modelData.id
|
|
||||||
leading: Kirigami.Avatar {
|
|
||||||
implicitHeight: Kirigami.Units.gridUnit
|
|
||||||
implicitWidth: implicitHeight
|
|
||||||
source: modelData.avatarMediaId ? ("image://mxc/" + modelData.avatarMediaId) : ""
|
|
||||||
color: modelData.color ? Qt.darker(modelData.color, 1.1) : null
|
|
||||||
}
|
|
||||||
labelItem.textFormat: Text.PlainText
|
|
||||||
text: modelData.displayName
|
|
||||||
onClicked: completeTriggered();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: emojiDelegate
|
|
||||||
Kirigami.BasicListItem {
|
|
||||||
id: emojiItem
|
|
||||||
width: ListView.view.width ?? implicitWidth
|
|
||||||
property string displayName: modelData.isCustom ? modelData.shortname : modelData.unicode
|
|
||||||
text: modelData.shortname
|
|
||||||
height: Kirigami.Units.gridUnit * 2
|
|
||||||
|
|
||||||
leading: Image {
|
|
||||||
source: modelData.isCustom ? modelData.unicode : ""
|
|
||||||
|
|
||||||
width: height
|
|
||||||
sourceSize.width: width
|
|
||||||
sourceSize.height: height
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: parent.status === Image.Loading
|
|
||||||
radius: height/2
|
|
||||||
gradient: ShimmerGradient { }
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
id: unicodeLabel
|
|
||||||
|
|
||||||
visible: !modelData.isCustom
|
|
||||||
|
|
||||||
font.family: 'emoji'
|
|
||||||
font.pixelSize: height - 2
|
|
||||||
|
|
||||||
text: modelData.unicode
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClicked: completeTriggered();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: commandDelegate
|
|
||||||
Kirigami.BasicListItem {
|
|
||||||
id: commandItem
|
|
||||||
width: ListView.view.width ?? implicitWidth
|
|
||||||
text: "<i>" + modelData.parameter.replace("<", "<").replace(">", ">") + "</i> " + modelData.help
|
|
||||||
property string displayName: modelData.command
|
|
||||||
|
|
||||||
leading: Label {
|
|
||||||
id: commandLabel
|
|
||||||
Layout.preferredHeight: Kirigami.Units.gridUnit
|
|
||||||
Layout.preferredWidth: textMetrics.tightBoundingRect.width
|
|
||||||
text: modelData.command
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
|
||||||
TextMetrics {
|
|
||||||
id: textMetrics
|
|
||||||
text: modelData.command
|
|
||||||
font: commandLabel.font
|
|
||||||
}
|
|
||||||
onClicked: completeTriggered();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,8 @@ import org.kde.kirigami 2.14 as Kirigami
|
|||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: root
|
id: replyPane
|
||||||
readonly property bool isEdit: chatBoxHelper.isEditing
|
property NeoChatUser user: currentRoom.chatBoxReplyUser ?? currentRoom.chatBoxEditUser
|
||||||
property var user: null
|
|
||||||
property string avatarMediaUrl: user ? "image://mxc/" + user.avatarMediaId : ""
|
|
||||||
|
|
||||||
signal replyCancelled()
|
signal replyCancelled()
|
||||||
|
|
||||||
@@ -40,7 +38,7 @@ Loader {
|
|||||||
Layout.alignment: textContentLayout.height > avatar.height ? Qt.AlignHCenter | Qt.AlignTop : Qt.AlignCenter
|
Layout.alignment: textContentLayout.height > avatar.height ? Qt.AlignHCenter | Qt.AlignTop : Qt.AlignCenter
|
||||||
Layout.preferredWidth: Layout.preferredHeight
|
Layout.preferredWidth: Layout.preferredHeight
|
||||||
Layout.preferredHeight: fontMetrics.lineSpacing * 2 - fontMetrics.leading
|
Layout.preferredHeight: fontMetrics.lineSpacing * 2 - fontMetrics.leading
|
||||||
source: root.avatarMediaUrl
|
source: user ? "image://mxc/" + currentRoom.getUser(user.id).avatarMediaId : ""
|
||||||
name: user ? user.displayName : ""
|
name: user ? user.displayName : ""
|
||||||
color: user ? user.color : "transparent"
|
color: user ? user.color : "transparent"
|
||||||
visible: Boolean(user)
|
visible: Boolean(user)
|
||||||
@@ -58,7 +56,7 @@ Loader {
|
|||||||
text: {
|
text: {
|
||||||
let heading = "<b>%1</b>"
|
let heading = "<b>%1</b>"
|
||||||
let userName = user ? "<font color=\""+ user.color +"\">" + currentRoom.htmlSafeMemberName(user.id) + "</font>" : ""
|
let userName = user ? "<font color=\""+ user.color +"\">" + currentRoom.htmlSafeMemberName(user.id) + "</font>" : ""
|
||||||
if (isEdit) {
|
if (currentRoom.chatBoxEditId.length > 0) {
|
||||||
heading = heading.arg(i18n("Editing message:")) + "<br/>"
|
heading = heading.arg(i18n("Editing message:")) + "<br/>"
|
||||||
} else {
|
} else {
|
||||||
heading = heading.arg(i18n("Replying to %1:", userName))
|
heading = heading.arg(i18n("Replying to %1:", userName))
|
||||||
@@ -67,6 +65,7 @@ Loader {
|
|||||||
return heading
|
return heading
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//TODO edit user mentions
|
||||||
ScrollView {
|
ScrollView {
|
||||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
@@ -81,11 +80,7 @@ Loader {
|
|||||||
rightPadding: 0
|
rightPadding: 0
|
||||||
topPadding: 0
|
topPadding: 0
|
||||||
bottomPadding: 0
|
bottomPadding: 0
|
||||||
text: {
|
text: "<style> a{color:" + Kirigami.Theme.linkColor + ";}.user-pill{}</style>" + (currentRoom.chatBoxEditId.length > 0 ? currentRoom.chatBoxEditMessage : currentRoom.chatBoxReplyMessage)
|
||||||
const stylesheet = "<style> a{color:"+Kirigami.Theme.linkColor+";}.user-pill{}</style>";
|
|
||||||
const content = chatBoxHelper.isReplying ? chatBoxHelper.replyEventContent : chatBoxHelper.editContent;
|
|
||||||
return stylesheet + content;
|
|
||||||
}
|
|
||||||
selectByMouse: true
|
selectByMouse: true
|
||||||
selectByKeyboard: true
|
selectByKeyboard: true
|
||||||
readOnly: true
|
readOnly: true
|
||||||
@@ -99,15 +94,16 @@ Loader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
ToolButton {
|
||||||
id: cancelReplyButton
|
|
||||||
Layout.alignment: avatar.Layout.alignment
|
|
||||||
icon.name: "dialog-cancel"
|
|
||||||
text: i18n("Cancel")
|
|
||||||
display: AbstractButton.IconOnly
|
display: AbstractButton.IconOnly
|
||||||
onClicked: {
|
action: Kirigami.Action {
|
||||||
chatBoxHelper.clear();
|
text: i18nc("@action:button", "Cancel reply")
|
||||||
root.replyCancelled();
|
icon.name: "dialog-close"
|
||||||
|
onTriggered: {
|
||||||
|
currentRoom.chatBoxReplyId = "";
|
||||||
|
currentRoom.chatBoxEditId = "";
|
||||||
|
}
|
||||||
|
shortcut: "Escape"
|
||||||
}
|
}
|
||||||
ToolTip.text: text
|
ToolTip.text: text
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import QtQuick.Controls 2.15
|
|||||||
import QtQuick.Layouts 1.15
|
import QtQuick.Layouts 1.15
|
||||||
import org.kde.kirigami 2.15 as Kirigami
|
import org.kde.kirigami 2.15 as Kirigami
|
||||||
|
|
||||||
import org.kde.neochat 1.0 as NeoChat
|
import org.kde.neochat 1.0
|
||||||
import NeoChat.Component 1.0
|
import NeoChat.Component 1.0
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
@@ -14,11 +14,7 @@ ColumnLayout {
|
|||||||
|
|
||||||
property string emojiCategory: "history"
|
property string emojiCategory: "history"
|
||||||
property var textArea
|
property var textArea
|
||||||
readonly property var emojiModel: NeoChat.EmojiModel
|
readonly property var emojiModel: EmojiModel
|
||||||
|
|
||||||
property NeoChat.CustomEmojiModel customModel: NeoChat.CustomEmojiModel {
|
|
||||||
connection: NeoChat.Controller.activeConnection
|
|
||||||
}
|
|
||||||
|
|
||||||
signal chosen(string emoji)
|
signal chosen(string emoji)
|
||||||
|
|
||||||
@@ -102,7 +98,7 @@ ColumnLayout {
|
|||||||
model: {
|
model: {
|
||||||
switch (emojiCategory) {
|
switch (emojiCategory) {
|
||||||
case "custom":
|
case "custom":
|
||||||
return _picker.customModel
|
return CustomEmojiModel
|
||||||
case "history":
|
case "history":
|
||||||
return emojiModel.history
|
return emojiModel.history
|
||||||
case "people":
|
case "people":
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ MessageDelegateContextMenu {
|
|||||||
text: i18n("Reply")
|
text: i18n("Reply")
|
||||||
icon.name: "mail-replied-symbolic"
|
icon.name: "mail-replied-symbolic"
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
chatBoxHelper.replyToMessage(eventId, message, author);
|
currentRoom.chatBoxReplyId = eventId
|
||||||
|
currentRoom.chatBoxEditId = ""
|
||||||
root.closeFullscreen()
|
root.closeFullscreen()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,13 +28,19 @@ Loader {
|
|||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
text: i18n("Edit")
|
text: i18n("Edit")
|
||||||
icon.name: "document-edit"
|
icon.name: "document-edit"
|
||||||
onTriggered: chatBoxHelper.edit(message, formattedBody, eventId);
|
onTriggered: {
|
||||||
|
currentRoom.chatBoxEditId = eventId;
|
||||||
|
currentRoom.chatBoxReplyId = "";
|
||||||
|
}
|
||||||
visible: eventType.length > 0 && author.id === Controller.activeConnection.localUserId && (eventType === "emote" || eventType === "message")
|
visible: eventType.length > 0 && author.id === Controller.activeConnection.localUserId && (eventType === "emote" || eventType === "message")
|
||||||
},
|
},
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
text: i18n("Reply")
|
text: i18n("Reply")
|
||||||
icon.name: "mail-replied-symbolic"
|
icon.name: "mail-replied-symbolic"
|
||||||
onTriggered: chatBoxHelper.replyToMessage(eventId, message, author);
|
onTriggered: {
|
||||||
|
currentRoom.chatBoxReplyId = eventId;
|
||||||
|
currentRoom.chatBoxEditId = "";
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
visible: author.id === currentRoom.localUser.id || currentRoom.canSendState("redact")
|
visible: author.id === currentRoom.localUser.id || currentRoom.canSendState("redact")
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ Kirigami.ScrollablePage {
|
|||||||
|
|
||||||
onCurrentRoomChanged: {
|
onCurrentRoomChanged: {
|
||||||
hasScrolledUpBefore = false;
|
hasScrolledUpBefore = false;
|
||||||
chatBoxHelper.clearEditReply()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -78,11 +77,6 @@ Kirigami.ScrollablePage {
|
|||||||
ActionsHandler {
|
ActionsHandler {
|
||||||
id: actionsHandler
|
id: actionsHandler
|
||||||
room: page.currentRoom
|
room: page.currentRoom
|
||||||
connection: Controller.activeConnection
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatBoxHelper {
|
|
||||||
id: chatBoxHelper
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Shortcut {
|
Shortcut {
|
||||||
@@ -101,10 +95,10 @@ Kirigami.ScrollablePage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: actionsHandler
|
target: currentRoom
|
||||||
function onShowMessage(messageType, message) {
|
function onShowMessage(messageType, message) {
|
||||||
page.header.contentItem.text = message;
|
page.header.contentItem.text = message;
|
||||||
page.header.contentItem.type = messageType === ActionsHandler.Error ? Kirigami.MessageType.Error : Kirigami.MessageType.Information;
|
page.header.contentItem.type = messageType === ActionsHandler.Error ? Kirigami.MessageType.Error : messageType === ActionsHandler.Positive ? Kirigami.MessageType.Positive : Kirigami.MessageType.Information;
|
||||||
page.header.visible = true;
|
page.header.visible = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,15 +171,6 @@ Kirigami.ScrollablePage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: currentRoom
|
|
||||||
function onPositiveMessage(message) {
|
|
||||||
page.header.contentItem.text = message;
|
|
||||||
page.header.contentItem.type = Kirigami.MessageType.Positive;
|
|
||||||
page.header.visible = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// hover actions on a delegate, activated in TimelineContainer.qml
|
// hover actions on a delegate, activated in TimelineContainer.qml
|
||||||
Connections {
|
Connections {
|
||||||
target: page.flickable
|
target: page.flickable
|
||||||
@@ -268,9 +253,10 @@ Kirigami.ScrollablePage {
|
|||||||
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
|
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
|
||||||
|
|
||||||
fileDialog.chosen.connect(function(path) {
|
fileDialog.chosen.connect(function(path) {
|
||||||
if (!path) return
|
if (!path) {
|
||||||
|
return;
|
||||||
chatBoxHelper.attachmentPath = path;
|
}
|
||||||
|
currentRoom.chatBoxAttachmentPath = path;
|
||||||
})
|
})
|
||||||
|
|
||||||
fileDialog.open()
|
fileDialog.open()
|
||||||
@@ -292,7 +278,7 @@ Kirigami.ScrollablePage {
|
|||||||
if (!Clipboard.saveImage(localPath)) {
|
if (!Clipboard.saveImage(localPath)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
chatBoxHelper.attachmentPath = localPath;
|
currentRoom.chatBoxAttachmentPath = localPath;
|
||||||
attachDialog.close();
|
attachDialog.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -367,7 +353,7 @@ Kirigami.ScrollablePage {
|
|||||||
DropArea {
|
DropArea {
|
||||||
id: dropAreaFile
|
id: dropAreaFile
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onDropped: chatBoxHelper.attachmentPath = drop.urls[0]
|
onDropped: currentRoom.chatBoxAttachmentPath = drop.urls[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.Pane {
|
QQC2.Pane {
|
||||||
@@ -510,9 +496,8 @@ Kirigami.ScrollablePage {
|
|||||||
visible: hoverActions.showEdit
|
visible: hoverActions.showEdit
|
||||||
icon.name: "document-edit"
|
icon.name: "document-edit"
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (hoverActions.showEdit) {
|
currentRoom.chatBoxEditId = hoverActions.event.eventId;
|
||||||
chatBoxHelper.edit(hoverActions.event.message, hoverActions.event.formattedBody, hoverActions.event.eventId)
|
currentRoom.chatBoxReplyId = "";
|
||||||
}
|
|
||||||
chatBox.focusInputField();
|
chatBox.focusInputField();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -522,7 +507,8 @@ Kirigami.ScrollablePage {
|
|||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
icon.name: "mail-replied-symbolic"
|
icon.name: "mail-replied-symbolic"
|
||||||
onClicked: {
|
onClicked: {
|
||||||
chatBoxHelper.replyToMessage(hoverActions.event.eventId, hoverActions.event.message, hoverActions.event.author);
|
currentRoom.chatBoxReplyId = hoverActions.event.eventId;
|
||||||
|
currentRoom.chatBoxEditId = "";
|
||||||
chatBox.focusInputField();
|
chatBox.focusInputField();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -534,24 +520,12 @@ Kirigami.ScrollablePage {
|
|||||||
footer: ChatBox {
|
footer: ChatBox {
|
||||||
id: chatBox
|
id: chatBox
|
||||||
visible: !invitation.visible && !(messageListView.count === 0 && !currentRoom.allHistoryLoaded)
|
visible: !invitation.visible && !(messageListView.count === 0 && !currentRoom.allHistoryLoaded)
|
||||||
|
width: parent.width
|
||||||
onMessageSent: {
|
onMessageSent: {
|
||||||
if (!messageListView.atYEnd) {
|
if (!messageListView.atYEnd) {
|
||||||
goToLastMessage();
|
goToLastMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onEditLastUserMessage: {
|
|
||||||
const targetMessage = messageEventModel.getLastLocalUserMessageEventId();
|
|
||||||
if (targetMessage) {
|
|
||||||
chatBoxHelper.edit(targetMessage["message"], targetMessage["formattedBody"], targetMessage["event_id"]);
|
|
||||||
chatBox.focusInputField();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onReplyPreviousUserMessage: {
|
|
||||||
const replyResponse = messageEventModel.getLatestMessageFromIndex(0);
|
|
||||||
if (replyResponse && replyResponse["event_id"]) {
|
|
||||||
chatBoxHelper.replyToMessage(replyResponse["event_id"], replyResponse["message"], replyResponse["sender_id"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
background: FancyEffectsContainer {
|
background: FancyEffectsContainer {
|
||||||
@@ -582,8 +556,8 @@ Kirigami.ScrollablePage {
|
|||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
enabled: Config.showFancyEffects
|
enabled: Config.showFancyEffects
|
||||||
target: chatBox
|
target: actionsHandler
|
||||||
function onFancyEffectsReasonFound(fancyEffect) {
|
function onShowEffect(fancyEffect) {
|
||||||
fancyEffectsContainer.processFancyEffectsReason(fancyEffect)
|
fancyEffectsContainer.processFancyEffectsReason(fancyEffect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,7 @@ Kirigami.ScrollablePage {
|
|||||||
ListView {
|
ListView {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
model: CustomEmojiModel {
|
model: CustomEmojiModel
|
||||||
id: emojiModel
|
|
||||||
connection: Controller.activeConnection
|
|
||||||
}
|
|
||||||
|
|
||||||
Kirigami.PlaceholderMessage {
|
Kirigami.PlaceholderMessage {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -99,7 +96,7 @@ Kirigami.ScrollablePage {
|
|||||||
this.fileDialog = openFileDialog.createObject(QQC2.Overlay.overlay)
|
this.fileDialog = openFileDialog.createObject(QQC2.Overlay.overlay)
|
||||||
|
|
||||||
this.fileDialog.chosen.connect((url) => {
|
this.fileDialog.chosen.connect((url) => {
|
||||||
emojiModel.addEmoji(emojiCreator.name, url)
|
CustomEmojiModel.addEmoji(emojiCreator.name, url)
|
||||||
this.fileDialog = null
|
this.fileDialog = null
|
||||||
})
|
})
|
||||||
this.fileDialog.onRejected.connect(() => {
|
this.fileDialog.onRejected.connect(() => {
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ add_library(neochat STATIC
|
|||||||
filetypesingleton.cpp
|
filetypesingleton.cpp
|
||||||
login.cpp
|
login.cpp
|
||||||
stickerevent.cpp
|
stickerevent.cpp
|
||||||
chatboxhelper.cpp
|
|
||||||
commandmodel.cpp
|
|
||||||
webshortcutmodel.cpp
|
webshortcutmodel.cpp
|
||||||
blurhash.cpp
|
blurhash.cpp
|
||||||
blurhashimageprovider.cpp
|
blurhashimageprovider.cpp
|
||||||
@@ -40,6 +38,9 @@ add_library(neochat STATIC
|
|||||||
urlhelper.cpp
|
urlhelper.cpp
|
||||||
windowcontroller.cpp
|
windowcontroller.cpp
|
||||||
linkpreviewer.cpp
|
linkpreviewer.cpp
|
||||||
|
completionmodel.cpp
|
||||||
|
completionproxymodel.cpp
|
||||||
|
actionsmodel.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(neochat-app
|
add_executable(neochat-app
|
||||||
@@ -84,7 +85,7 @@ else()
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
|
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
|
||||||
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF5::I18n KF5::Kirigami2 KF5::Notifications KF5::ConfigCore KF5::ConfigGui KF5::CoreAddons Quotient cmark::cmark ${QTKEYCHAIN_LIBRARIES})
|
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF5::I18n KF5::Kirigami2 KF5::Notifications KF5::ConfigCore KF5::ConfigGui KF5::CoreAddons KF5::SonnetCore Quotient cmark::cmark ${QTKEYCHAIN_LIBRARIES})
|
||||||
if(TARGET QCoro5::Coro)
|
if(TARGET QCoro5::Coro)
|
||||||
target_link_libraries(neochat PUBLIC QCoro5::Coro)
|
target_link_libraries(neochat PUBLIC QCoro5::Coro)
|
||||||
else()
|
else()
|
||||||
|
|||||||
@@ -3,21 +3,49 @@
|
|||||||
|
|
||||||
#include "actionshandler.h"
|
#include "actionshandler.h"
|
||||||
|
|
||||||
|
#include "controller.h"
|
||||||
|
|
||||||
|
#include <csapi/joining.h>
|
||||||
|
#include <events/roommemberevent.h>
|
||||||
|
|
||||||
|
#include <cmark.h>
|
||||||
|
|
||||||
#include <KLocalizedString>
|
#include <KLocalizedString>
|
||||||
#include <QStringBuilder>
|
#include <QStringBuilder>
|
||||||
|
|
||||||
|
#include "actionsmodel.h"
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "customemojimodel.h"
|
#include "customemojimodel.h"
|
||||||
|
#include "neochatconfig.h"
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
#include "roommanager.h"
|
#include "roommanager.h"
|
||||||
|
#include "neochatuser.h"
|
||||||
|
|
||||||
|
using namespace Quotient;
|
||||||
|
|
||||||
|
QString markdownToHTML(const QString &markdown)
|
||||||
|
{
|
||||||
|
const auto str = markdown.toUtf8();
|
||||||
|
char *tmp_buf = cmark_markdown_to_html(str.constData(), str.size(), CMARK_OPT_HARDBREAKS);
|
||||||
|
|
||||||
|
const std::string html(tmp_buf);
|
||||||
|
|
||||||
|
free(tmp_buf);
|
||||||
|
|
||||||
|
auto result = QString::fromStdString(html).trimmed();
|
||||||
|
|
||||||
|
result.replace("<!-- raw HTML omitted -->", "");
|
||||||
|
result.replace("<p>", "");
|
||||||
|
result.replace("</p>", "");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
ActionsHandler::ActionsHandler(QObject *parent)
|
ActionsHandler::ActionsHandler(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionsHandler::~ActionsHandler(){};
|
|
||||||
|
|
||||||
NeoChatRoom *ActionsHandler::room() const
|
NeoChatRoom *ActionsHandler::room() const
|
||||||
{
|
{
|
||||||
return m_room;
|
return m_room;
|
||||||
@@ -33,298 +61,106 @@ void ActionsHandler::setRoom(NeoChatRoom *room)
|
|||||||
Q_EMIT roomChanged();
|
Q_EMIT roomChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
Connection *ActionsHandler::connection() const
|
void ActionsHandler::handleMessage()
|
||||||
{
|
{
|
||||||
return m_connection;
|
checkEffects();
|
||||||
}
|
if (!m_room->chatBoxAttachmentPath().isEmpty()) {
|
||||||
|
auto path = m_room->chatBoxAttachmentPath();
|
||||||
void ActionsHandler::setConnection(Connection *connection)
|
path = path.mid(path.lastIndexOf('/') + 1);
|
||||||
{
|
m_room->uploadFile(m_room->chatBoxAttachmentPath(), m_room->chatBoxText().isEmpty() ? path : m_room->chatBoxText());
|
||||||
if (m_connection == connection) {
|
m_room->setChatBoxAttachmentPath({});
|
||||||
|
m_room->setChatBoxText({});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (m_connection != nullptr) {
|
QString handledText = m_room->chatBoxText();
|
||||||
disconnect(m_connection, &Connection::directChatAvailable, nullptr, nullptr);
|
|
||||||
}
|
|
||||||
m_connection = connection;
|
|
||||||
if (m_connection != nullptr) {
|
|
||||||
connect(m_connection, &Connection::directChatAvailable, this, [this](Quotient::Room *room) {
|
|
||||||
room->setDisplayed(true);
|
|
||||||
RoomManager::instance().enterRoom(qobject_cast<NeoChatRoom *>(room));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Q_EMIT connectionChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ActionsHandler::postEdit(const QString &text)
|
std::sort(m_room->mentions()->begin(), m_room->mentions()->end(), [](const auto &a, const auto &b) -> bool {
|
||||||
{
|
return a.cursor.anchor() > b.cursor.anchor();
|
||||||
const auto localId = Controller::instance().activeConnection()->userId();
|
});
|
||||||
for (auto it = m_room->messageEvents().crbegin(); it != m_room->messageEvents().crend(); ++it) {
|
|
||||||
const auto &evt = **it;
|
for (const auto &mention : *m_room->mentions()) {
|
||||||
if (const auto event = eventCast<const RoomMessageEvent>(&evt)) {
|
handledText = handledText.replace(mention.cursor.anchor(),
|
||||||
if (event->senderId() == localId && event->hasTextContent()) {
|
mention.cursor.position() - mention.cursor.anchor(),
|
||||||
static QRegularExpression re("^s/([^/]*)/([^/]*)(/g)?");
|
QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text, mention.id));
|
||||||
auto match = re.match(text);
|
}
|
||||||
if (!match.hasMatch()) {
|
|
||||||
// should not happen but still make sure to send the message normally
|
if (NeoChatConfig::allowQuickEdit()) {
|
||||||
// just in case.
|
QRegularExpression sed("^s/([^/]*)/([^/]*)(/g)?$");
|
||||||
postMessage(text, QString(), QString(), QString(), QVariantMap(), nullptr);
|
auto match = sed.match(m_room->chatBoxText());
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
const QString regex = match.captured(1);
|
||||||
|
const QString replacement = match.captured(2);
|
||||||
|
const QString flags = match.captured(3);
|
||||||
|
|
||||||
|
for (auto it = m_room->messageEvents().crbegin(); it != m_room->messageEvents().crend(); it++) {
|
||||||
|
if (const auto event = eventCast<const RoomMessageEvent>(&**it)) {
|
||||||
|
if (event->senderId() == m_room->localUser()->id() && event->hasTextContent()) {
|
||||||
|
QString originalString;
|
||||||
|
if (event->content()) {
|
||||||
|
originalString = static_cast<const Quotient::EventContent::TextContent *>(event->content())->body;
|
||||||
|
} else {
|
||||||
|
originalString = event->plainBody();
|
||||||
|
}
|
||||||
|
if (flags == "/g") {
|
||||||
|
m_room->postHtmlMessage(handledText, originalString.replace(regex, replacement), event->msgtype(), "", event->id());
|
||||||
|
} else {
|
||||||
|
m_room->postHtmlMessage(handledText,
|
||||||
|
originalString.replace(originalString.indexOf(regex), regex.size(), replacement),
|
||||||
|
event->msgtype(),
|
||||||
|
"",
|
||||||
|
event->id());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const QString regex = match.captured(1);
|
|
||||||
const QString replacement = match.captured(2);
|
|
||||||
const QString flags = match.captured(3);
|
|
||||||
QString originalString;
|
|
||||||
if (event->content()) {
|
|
||||||
originalString = static_cast<const Quotient::EventContent::TextContent *>(event->content())->body;
|
|
||||||
} else {
|
|
||||||
originalString = event->plainBody();
|
|
||||||
}
|
|
||||||
if (flags == "/g") {
|
|
||||||
m_room->postHtmlMessage(text, originalString.replace(regex, replacement), event->msgtype(), "", event->id());
|
|
||||||
} else {
|
|
||||||
m_room->postHtmlMessage(text,
|
|
||||||
originalString.replace(originalString.indexOf(regex), regex.size(), replacement),
|
|
||||||
event->msgtype(),
|
|
||||||
"",
|
|
||||||
event->id());
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
auto messageType = RoomMessageEvent::MsgType::Text;
|
||||||
|
|
||||||
void ActionsHandler::postMessage(const QString &text,
|
if (handledText.startsWith(QLatin1Char('/'))) {
|
||||||
const QString &attachmentPath,
|
for (const auto &action : ActionsModel::instance().allActions()) {
|
||||||
const QString &replyEventId,
|
if (handledText.indexOf(action.prefix) == 1
|
||||||
const QString &editEventId,
|
&& (handledText.indexOf(" ") == action.prefix.length() + 1 || handledText.length() == action.prefix.length() + 1)) {
|
||||||
const QVariantMap &usernames,
|
handledText = action.handle(handledText.mid(action.prefix.length() + 1).trimmed(), m_room);
|
||||||
CustomEmojiModel *cem)
|
if (action.messageType.has_value()) {
|
||||||
{
|
messageType = *action.messageType;
|
||||||
QString rawText = text;
|
}
|
||||||
QString cleanedText = text;
|
if (action.messageAction) {
|
||||||
|
break;
|
||||||
auto preprocess = [cem](const QString &it) -> QString {
|
} else {
|
||||||
if (cem == nullptr) {
|
|
||||||
return it;
|
|
||||||
}
|
|
||||||
return cem->preprocessText(it);
|
|
||||||
};
|
|
||||||
|
|
||||||
for (auto it = usernames.constBegin(); it != usernames.constEnd(); it++) {
|
|
||||||
cleanedText = cleanedText.replace(it.key(), "[" + it.key() + "](https://matrix.to/#/" + it.value().toString() + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attachmentPath.length() > 0) {
|
|
||||||
m_room->uploadFile(attachmentPath, cleanedText);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleanedText.length() == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto messageEventType = RoomMessageEvent::MsgType::Text;
|
|
||||||
|
|
||||||
// Message commands
|
|
||||||
static const QString shrugPrefix = QStringLiteral("/shrug");
|
|
||||||
static const QString lennyPrefix = QStringLiteral("/lenny");
|
|
||||||
static const QString tableflipPrefix = QStringLiteral("/tableflip");
|
|
||||||
static const QString unflipPrefix = QStringLiteral("/unflip");
|
|
||||||
// static const QString plainPrefix = QStringLiteral("/plain "); // TODO
|
|
||||||
// static const QString htmlPrefix = QStringLiteral("/html "); // TODO
|
|
||||||
static const QString rainbowPrefix = QStringLiteral("/rainbow ");
|
|
||||||
static const QString rainbowmePrefix = QStringLiteral("/rainbowme ");
|
|
||||||
static const QString spoilerPrefix = QStringLiteral("/spoiler ");
|
|
||||||
static const QString mePrefix = QStringLiteral("/me ");
|
|
||||||
static const QString noticePrefix = QStringLiteral("/notice ");
|
|
||||||
|
|
||||||
// Actions commands
|
|
||||||
// static const QString ddgPrefix = QStringLiteral("/ddg "); // TODO
|
|
||||||
// static const QString nickPrefix = QStringLiteral("/nick "); // TODO
|
|
||||||
// static const QString meroomnickPrefix = QStringLiteral("/myroomnick "); // TODO
|
|
||||||
// static const QString roomavatarPrefix = QStringLiteral("/roomavatar "); // TODO
|
|
||||||
// static const QString myroomavatarPrefix = QStringLiteral("/myroomavatar "); // TODO
|
|
||||||
// static const QString myavatarPrefix = QStringLiteral("/myavatar "); // TODO
|
|
||||||
static const QString invitePrefix = QStringLiteral("/invite ");
|
|
||||||
static const QString joinPrefix = QStringLiteral("/join ");
|
|
||||||
static const QString joinShortPrefix = QStringLiteral("/j ");
|
|
||||||
static const QString partPrefix = QStringLiteral("/part");
|
|
||||||
static const QString leavePrefix = QStringLiteral("/leave");
|
|
||||||
static const QString ignorePrefix = QStringLiteral("/ignore ");
|
|
||||||
static const QString unignorePrefix = QStringLiteral("/unignore ");
|
|
||||||
// static const QString queryPrefix = QStringLiteral("/query "); // TODO
|
|
||||||
// static const QString msgPrefix = QStringLiteral("/msg "); // TODO
|
|
||||||
static const QString reactPrefix = QStringLiteral("/react ");
|
|
||||||
|
|
||||||
// Admin commands
|
|
||||||
|
|
||||||
static QStringList rainbowColors{"#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"};
|
|
||||||
|
|
||||||
if (cleanedText.indexOf(shrugPrefix) == 0) {
|
|
||||||
cleanedText = QStringLiteral("¯\\_(ツ)_/¯") % cleanedText.remove(0, shrugPrefix.length());
|
|
||||||
m_room->postHtmlMessage(cleanedText, cleanedText, messageEventType, replyEventId, editEventId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleanedText.indexOf(lennyPrefix) == 0) {
|
|
||||||
cleanedText = QStringLiteral("( ͡° ͜ʖ ͡°)") % cleanedText.remove(0, lennyPrefix.length());
|
|
||||||
m_room->postHtmlMessage(cleanedText, cleanedText, messageEventType, replyEventId, editEventId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleanedText.indexOf(tableflipPrefix) == 0) {
|
|
||||||
cleanedText = QStringLiteral("(╯°□°)╯︵ ┻━┻") % cleanedText.remove(0, tableflipPrefix.length());
|
|
||||||
m_room->postHtmlMessage(cleanedText, cleanedText, messageEventType, replyEventId, editEventId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleanedText.indexOf(unflipPrefix) == 0) {
|
|
||||||
cleanedText = QStringLiteral("┬──┬ ノ( ゜-゜ノ)") % cleanedText.remove(0, unflipPrefix.length());
|
|
||||||
m_room->postHtmlMessage(cleanedText, cleanedText, messageEventType, replyEventId, editEventId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleanedText.indexOf(rainbowPrefix) == 0) {
|
|
||||||
cleanedText = cleanedText.remove(0, rainbowPrefix.length());
|
|
||||||
QString rainbowText;
|
|
||||||
for (int i = 0; i < cleanedText.length(); i++) {
|
|
||||||
rainbowText = rainbowText % QStringLiteral("<font color='") % rainbowColors.at(i % rainbowColors.length()) % "'>" % cleanedText.at(i) % "</font>";
|
|
||||||
}
|
|
||||||
m_room->postHtmlMessage(cleanedText, preprocess(rainbowText), RoomMessageEvent::MsgType::Notice, replyEventId, editEventId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleanedText.indexOf(spoilerPrefix) == 0) {
|
|
||||||
cleanedText = cleanedText.remove(0, spoilerPrefix.length());
|
|
||||||
const QStringList splittedText = rawText.split(" ");
|
|
||||||
QString spoilerHtml = QStringLiteral("<span data-mx-spoiler>") % preprocess(cleanedText) % QStringLiteral("</span>");
|
|
||||||
m_room->postHtmlMessage(cleanedText, spoilerHtml, RoomMessageEvent::MsgType::Notice, replyEventId, editEventId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleanedText.indexOf(rainbowmePrefix) == 0) {
|
|
||||||
cleanedText = cleanedText.remove(0, rainbowmePrefix.length());
|
|
||||||
QString rainbowText;
|
|
||||||
for (int i = 0; i < cleanedText.length(); i++) {
|
|
||||||
rainbowText = rainbowText % QStringLiteral("<font color='") % rainbowColors.at(i % rainbowColors.length()) % "'>" % cleanedText.at(i) % "</font>";
|
|
||||||
}
|
|
||||||
m_room->postHtmlMessage(cleanedText, preprocess(rainbowText), messageEventType, replyEventId, editEventId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rawText.indexOf(joinPrefix) == 0 || rawText.indexOf(joinShortPrefix) == 0) {
|
|
||||||
if (rawText.indexOf(joinPrefix) == 0) {
|
|
||||||
rawText = rawText.remove(0, joinPrefix.length());
|
|
||||||
} else {
|
|
||||||
rawText = rawText.remove(0, joinShortPrefix.length());
|
|
||||||
}
|
|
||||||
const QStringList splittedText = rawText.split(" ");
|
|
||||||
if (text.count() == 0) {
|
|
||||||
Q_EMIT showMessage(MessageType::Error, i18n("Invalid command"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (splittedText.count() > 1) {
|
|
||||||
Controller::instance().joinRoom(splittedText[0] + ":" + splittedText[1]);
|
|
||||||
return;
|
|
||||||
} else if (splittedText[0].indexOf(":") != -1) {
|
|
||||||
Controller::instance().joinRoom(splittedText[0]);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
Controller::instance().joinRoom(splittedText[0] + ":matrix.org");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rawText.indexOf(invitePrefix) == 0) {
|
|
||||||
rawText = rawText.remove(0, invitePrefix.length());
|
|
||||||
const QStringList splittedText = rawText.split(" ");
|
|
||||||
if (splittedText.count() == 0) {
|
|
||||||
Q_EMIT showMessage(MessageType::Error, i18n("Invalid command"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_room->inviteToRoom(splittedText[0]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rawText.indexOf(partPrefix) == 0 || rawText.indexOf(leavePrefix) == 0) {
|
|
||||||
if (rawText.indexOf(partPrefix) == 0) {
|
|
||||||
rawText = rawText.remove(0, partPrefix.length());
|
|
||||||
} else {
|
|
||||||
rawText = rawText.remove(0, leavePrefix.length());
|
|
||||||
}
|
|
||||||
const QStringList splittedText = rawText.split(" ");
|
|
||||||
if (splittedText.count() == 0 || splittedText[0].isEmpty()) {
|
|
||||||
// leave current room
|
|
||||||
m_connection->leaveRoom(m_room);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_connection->leaveRoom(m_connection->room(splittedText[0]));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rawText.indexOf(ignorePrefix) == 0) {
|
|
||||||
rawText = rawText.remove(0, ignorePrefix.length());
|
|
||||||
const QStringList splittedText = rawText.split(" ");
|
|
||||||
if (splittedText.count() == 0) {
|
|
||||||
Q_EMIT showMessage(MessageType::Error, i18n("Invalid command"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_connection->users().contains(splittedText[0])) {
|
|
||||||
Q_EMIT showMessage(MessageType::Error, i18n("Invalid command"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto *user = m_connection->users()[splittedText[0]];
|
|
||||||
m_connection->addToIgnoredUsers(user);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rawText.indexOf(unignorePrefix) == 0) {
|
|
||||||
rawText = rawText.remove(0, unignorePrefix.length());
|
|
||||||
const QStringList splittedText = rawText.split(" ");
|
|
||||||
if (splittedText.count() == 0) {
|
|
||||||
Q_EMIT showMessage(MessageType::Error, i18n("Invalid command"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_connection->users().contains(splittedText[0])) {
|
|
||||||
Q_EMIT showMessage(MessageType::Error, i18n("Invalid command"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto *user = m_connection->users()[splittedText[0]];
|
|
||||||
m_connection->removeFromIgnoredUsers(user);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rawText.indexOf(reactPrefix) == 0) {
|
|
||||||
rawText = rawText.remove(0, reactPrefix.length());
|
|
||||||
if (replyEventId.isEmpty()) {
|
|
||||||
for (auto it = m_room->messageEvents().crbegin(); it != m_room->messageEvents().crend(); ++it) {
|
|
||||||
const auto &evt = **it;
|
|
||||||
if (const auto event = eventCast<const RoomMessageEvent>(&evt)) {
|
|
||||||
m_room->toggleReaction(event->id(), rawText);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Q_EMIT showMessage(MessageType::Error, i18n("Couldn't find a message to react to"));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
m_room->toggleReaction(replyEventId, rawText);
|
}
|
||||||
|
|
||||||
|
handledText = markdownToHTML(handledText);
|
||||||
|
handledText = CustomEmojiModel::instance().preprocessText(handledText);
|
||||||
|
|
||||||
|
if (handledText.length() == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cleanedText.indexOf(mePrefix) == 0) {
|
m_room->postMessage(m_room->chatBoxText(), handledText, messageType, m_room->chatBoxReplyId(), m_room->chatBoxEditId());
|
||||||
cleanedText = cleanedText.remove(0, mePrefix.length());
|
}
|
||||||
messageEventType = RoomMessageEvent::MsgType::Emote;
|
|
||||||
rawText = rawText.remove(0, mePrefix.length());
|
void ActionsHandler::checkEffects()
|
||||||
} else if (cleanedText.indexOf(noticePrefix) == 0) {
|
{
|
||||||
cleanedText = cleanedText.remove(0, noticePrefix.length());
|
std::optional<QString> effect = std::nullopt;
|
||||||
messageEventType = RoomMessageEvent::MsgType::Notice;
|
const auto &text = m_room->chatBoxText();
|
||||||
}
|
if (text.contains("\u2744")) {
|
||||||
m_room->postMessage(rawText, preprocess(m_room->preprocessText(cleanedText)), messageEventType, replyEventId, editEventId);
|
effect = QLatin1String("snowflake");
|
||||||
|
} else if (text.contains("\u1F386")) {
|
||||||
|
effect = QLatin1String("fireworks");
|
||||||
|
} else if (text.contains("\u2F387")) {
|
||||||
|
effect = QLatin1String("fireworks");
|
||||||
|
} else if (text.contains("\u1F389")) {
|
||||||
|
effect = QLatin1String("confetti");
|
||||||
|
} else if (text.contains("\u1F38A")) {
|
||||||
|
effect = QLatin1String("confetti");
|
||||||
|
}
|
||||||
|
if (effect.has_value()) {
|
||||||
|
Q_EMIT showEffect(*effect);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,11 @@
|
|||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
|
||||||
namespace Quotient
|
#include <events/roommessageevent.h>
|
||||||
{
|
|
||||||
class Connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
class NeoChatRoom;
|
class NeoChatRoom;
|
||||||
class CustomEmojiModel;
|
class CustomEmojiModel;
|
||||||
|
class NeoChatRoom;
|
||||||
|
|
||||||
/// \brief Handles user interactions with NeoChat (joining room, creating room,
|
/// \brief Handles user interactions with NeoChat (joining room, creating room,
|
||||||
/// sending message). Account management is handled by Controller.
|
/// sending message). Account management is handled by Controller.
|
||||||
@@ -19,56 +17,29 @@ class ActionsHandler : public QObject
|
|||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
/// \brief The connection that will handle sending the message.
|
/// \brief The room that messages will be sent to.
|
||||||
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
|
||||||
|
|
||||||
/// \brief The connection that will handle sending the message.
|
|
||||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum MessageType {
|
|
||||||
Info,
|
|
||||||
Error,
|
|
||||||
};
|
|
||||||
Q_ENUM(MessageType);
|
|
||||||
|
|
||||||
explicit ActionsHandler(QObject *parent = nullptr);
|
explicit ActionsHandler(QObject *parent = nullptr);
|
||||||
~ActionsHandler();
|
|
||||||
|
|
||||||
[[nodiscard]] Quotient::Connection *connection() const;
|
|
||||||
void setConnection(Quotient::Connection *connection);
|
|
||||||
|
|
||||||
[[nodiscard]] NeoChatRoom *room() const;
|
[[nodiscard]] NeoChatRoom *room() const;
|
||||||
void setRoom(NeoChatRoom *room);
|
void setRoom(NeoChatRoom *room);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
/// \brief Show error or information message.
|
|
||||||
///
|
|
||||||
/// These messages will be displayed in the room view header.
|
|
||||||
void showMessage(ActionsHandler::MessageType messageType, QString message);
|
|
||||||
|
|
||||||
void roomChanged();
|
void roomChanged();
|
||||||
void connectionChanged();
|
void showEffect(QString effect);
|
||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
|
|
||||||
/// \brief Post a message.
|
/// \brief Post a message.
|
||||||
///
|
///
|
||||||
/// This also interprets commands if any.
|
/// This also interprets commands if any.
|
||||||
void postMessage(const QString &text,
|
void handleMessage();
|
||||||
const QString &attachementPath,
|
|
||||||
const QString &replyEventId,
|
|
||||||
const QString &editEventId,
|
|
||||||
const QVariantMap &usernames,
|
|
||||||
CustomEmojiModel *cem);
|
|
||||||
|
|
||||||
/// \brief Send edit instructions (.e.g s/hallo/hello/)
|
|
||||||
///
|
|
||||||
/// This will automatically edit the last message posted and send the sed
|
|
||||||
/// instruction to IRC.
|
|
||||||
void postEdit(const QString &text);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Quotient::Connection *m_connection = nullptr;
|
|
||||||
NeoChatRoom *m_room = nullptr;
|
NeoChatRoom *m_room = nullptr;
|
||||||
|
void checkEffects();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
QString markdownToHTML(const QString &markdown);
|
||||||
|
|||||||
401
src/actionsmodel.cpp
Normal file
401
src/actionsmodel.cpp
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "actionsmodel.h"
|
||||||
|
|
||||||
|
#include "controller.h"
|
||||||
|
#include "neochatroom.h"
|
||||||
|
#include "neochatuser.h"
|
||||||
|
#include <events/roommemberevent.h>
|
||||||
|
|
||||||
|
#include <KLocalizedString>
|
||||||
|
|
||||||
|
using Action = ActionsModel::Action;
|
||||||
|
using namespace Quotient;
|
||||||
|
|
||||||
|
QStringList rainbowColors{"#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"};
|
||||||
|
|
||||||
|
QVector<ActionsModel::Action> actions{
|
||||||
|
Action{
|
||||||
|
QStringLiteral("shrug"),
|
||||||
|
[](const QString &message, NeoChatRoom *) {
|
||||||
|
return QStringLiteral("¯\\\\_(ツ)_/¯ %1").arg(message);
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
std::nullopt,
|
||||||
|
kli18n("<message>"),
|
||||||
|
kli18n("Prepends ¯\\_(ツ)_/¯ to a plain-text message"),
|
||||||
|
},
|
||||||
|
Action{
|
||||||
|
QStringLiteral("lenny"),
|
||||||
|
[](const QString &message, NeoChatRoom *) {
|
||||||
|
return QStringLiteral("( ͡° ͜ʖ ͡°) %1").arg(message);
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
std::nullopt,
|
||||||
|
kli18n("<message>"),
|
||||||
|
kli18n("Prepends ( ͡° ͜ʖ ͡°) to a plain-text message"),
|
||||||
|
},
|
||||||
|
Action{
|
||||||
|
QStringLiteral("tableflip"),
|
||||||
|
[](const QString &message, NeoChatRoom *) {
|
||||||
|
return QStringLiteral("(╯°□°)╯︵ ┻━┻ %1").arg(message);
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
std::nullopt,
|
||||||
|
kli18n("<message>"),
|
||||||
|
kli18n("Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message"),
|
||||||
|
},
|
||||||
|
Action{
|
||||||
|
QStringLiteral("unflip"),
|
||||||
|
[](const QString &message, NeoChatRoom *) {
|
||||||
|
return QStringLiteral("┬──┬ ノ( ゜-゜ノ) %1").arg(message);
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
std::nullopt,
|
||||||
|
kli18n("<message>"),
|
||||||
|
kli18n("Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message"),
|
||||||
|
},
|
||||||
|
Action{
|
||||||
|
QStringLiteral("rainbow"),
|
||||||
|
[](const QString &text, NeoChatRoom *room) {
|
||||||
|
QString rainbowText;
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
rainbowText += QStringLiteral("<font color='%2'>%3</font>").arg(rainbowColors[i % rainbowColors.length()], text.at(i));
|
||||||
|
}
|
||||||
|
// Ideally, we would just return rainbowText and let that do the rest, but the colors don't survive markdownToHTML.
|
||||||
|
room->postMessage(QStringLiteral("/rainbow %1").arg(text),
|
||||||
|
rainbowText,
|
||||||
|
RoomMessageEvent::MsgType::Text,
|
||||||
|
room->chatBoxReplyId(),
|
||||||
|
room->chatBoxEditId());
|
||||||
|
return QString();
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
std::nullopt,
|
||||||
|
kli18n("<message>"),
|
||||||
|
kli18n("Sends the given message colored as a rainbow"),
|
||||||
|
},
|
||||||
|
Action{
|
||||||
|
QStringLiteral("rainbowme"),
|
||||||
|
[](const QString &text, NeoChatRoom *room) {
|
||||||
|
QString rainbowText;
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
rainbowText += QStringLiteral("<font color='%2'>%3</font>").arg(rainbowColors[i % rainbowColors.length()], text.at(i));
|
||||||
|
}
|
||||||
|
// Ideally, we would just return rainbowText and let that do the rest, but the colors don't survive markdownToHTML.
|
||||||
|
room->postMessage(QStringLiteral("/rainbow %1").arg(text),
|
||||||
|
rainbowText,
|
||||||
|
RoomMessageEvent::MsgType::Emote,
|
||||||
|
room->chatBoxReplyId(),
|
||||||
|
room->chatBoxEditId());
|
||||||
|
return QString();
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
std::nullopt,
|
||||||
|
kli18n("<message>"),
|
||||||
|
kli18n("Sends the given emote colored as a rainbow"),
|
||||||
|
},
|
||||||
|
Action{
|
||||||
|
QStringLiteral("spoiler"),
|
||||||
|
[](const QString &text, NeoChatRoom *room) {
|
||||||
|
// Ideally, we would just return rainbowText and let that do the rest, but the colors don't survive markdownToHTML.
|
||||||
|
room->postMessage(QStringLiteral("/rainbow %1").arg(text),
|
||||||
|
QStringLiteral("<span data-mx-spoiler>%1</span>").arg(text),
|
||||||
|
RoomMessageEvent::MsgType::Text,
|
||||||
|
room->chatBoxReplyId(),
|
||||||
|
room->chatBoxEditId());
|
||||||
|
return QString();
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
std::nullopt,
|
||||||
|
kli18n("<message>"),
|
||||||
|
kli18n("Sends the given message as a spoiler"),
|
||||||
|
},
|
||||||
|
Action{
|
||||||
|
QStringLiteral("me"),
|
||||||
|
[](const QString &text, NeoChatRoom *) {
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
RoomMessageEvent::MsgType::Emote,
|
||||||
|
kli18n("<message>"),
|
||||||
|
kli18n("Sends the given emote"),
|
||||||
|
},
|
||||||
|
Action{
|
||||||
|
QStringLiteral("notice"),
|
||||||
|
[](const QString &text, NeoChatRoom *) {
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
RoomMessageEvent::MsgType::Notice,
|
||||||
|
kli18n("<message>"),
|
||||||
|
kli18n("Sends the given message as a notice"),
|
||||||
|
},
|
||||||
|
Action{
|
||||||
|
QStringLiteral("invite"),
|
||||||
|
[](const QString &text, NeoChatRoom *room) {
|
||||||
|
static const QRegularExpression mxidRegex(
|
||||||
|
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
|
||||||
|
auto regexMatch = mxidRegex.match(text);
|
||||||
|
if (!regexMatch.hasMatch()) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
if (room->currentState().get<RoomMemberEvent>(text)->membership() == Membership::Invite) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is already invited to this room.", "%1 is already invited to this room.", text));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
if (room->currentState().get<RoomMemberEvent>(text)->membership() == Membership::Ban) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is banned from this room.", "%1 is banned from this room.", text));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (room->localUser()->id() == text) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18n("You are already in this room."));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
if (room->users().contains(room->user(text))) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is already in this room.", "%1 is already in this room.", text));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
room->inviteToRoom(text);
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> was invited into this room", "%1 was invited into this room", text));
|
||||||
|
return QString();
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
std::nullopt,
|
||||||
|
kli18n("<user id>"),
|
||||||
|
kli18n("Invites the user to this room"),
|
||||||
|
},
|
||||||
|
Action{
|
||||||
|
QStringLiteral("join"),
|
||||||
|
[](const QString &text, NeoChatRoom *room) {
|
||||||
|
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
|
||||||
|
auto regexMatch = roomRegex.match(text);
|
||||||
|
if (!regexMatch.hasMatch()) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Error,
|
||||||
|
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
if (Controller::instance().activeConnection()->room(text) || Controller::instance().activeConnection()->roomByAlias(text)) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("You are already in room <roomname>.", "You are already in room %1.", text));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
|
||||||
|
Controller::instance().joinRoom(text);
|
||||||
|
return QString();
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
std::nullopt,
|
||||||
|
kli18n("<room alias or id>"),
|
||||||
|
kli18n("Joins the given room"),
|
||||||
|
},
|
||||||
|
Action{
|
||||||
|
QStringLiteral("j"),
|
||||||
|
[](const QString &text, NeoChatRoom *room) {
|
||||||
|
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
|
||||||
|
auto regexMatch = roomRegex.match(text);
|
||||||
|
if (!regexMatch.hasMatch()) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Error,
|
||||||
|
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
if (Controller::instance().activeConnection()->room(text) || Controller::instance().activeConnection()->roomByAlias(text)) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("You are already in room <roomname>.", "You are already in room %1.", text));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
|
||||||
|
Controller::instance().joinRoom(text);
|
||||||
|
return QString();
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
std::nullopt,
|
||||||
|
kli18n("<room alias or id>"),
|
||||||
|
kli18n("Joins the given room"),
|
||||||
|
},
|
||||||
|
Action{
|
||||||
|
QStringLiteral("part"),
|
||||||
|
[](const QString &text, NeoChatRoom *room) {
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Info, i18n("Leaving this room."));
|
||||||
|
room->connection()->leaveRoom(room);
|
||||||
|
} else {
|
||||||
|
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
|
||||||
|
auto regexMatch = roomRegex.match(text);
|
||||||
|
if (!regexMatch.hasMatch()) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Error,
|
||||||
|
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
auto leaving = room->connection()->room(text);
|
||||||
|
if (!leaving) {
|
||||||
|
leaving = room->connection()->roomByAlias(text);
|
||||||
|
}
|
||||||
|
if (leaving) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
|
||||||
|
room->connection()->leaveRoom(leaving);
|
||||||
|
} else {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Room <roomname> not found", "Room %1 not found.", text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
std::nullopt,
|
||||||
|
kli18n("[<room alias or id>]"),
|
||||||
|
kli18n("Leaves the given room or this room, if there is none given"),
|
||||||
|
},
|
||||||
|
Action{
|
||||||
|
QStringLiteral("leave"),
|
||||||
|
[](const QString &text, NeoChatRoom *room) {
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Info, i18n("Leaving this room."));
|
||||||
|
room->connection()->leaveRoom(room);
|
||||||
|
} else {
|
||||||
|
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
|
||||||
|
auto regexMatch = roomRegex.match(text);
|
||||||
|
if (!regexMatch.hasMatch()) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Error,
|
||||||
|
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
auto leaving = room->connection()->room(text);
|
||||||
|
if (!leaving) {
|
||||||
|
leaving = room->connection()->roomByAlias(text);
|
||||||
|
}
|
||||||
|
if (leaving) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
|
||||||
|
room->connection()->leaveRoom(leaving);
|
||||||
|
} else {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Room <roomname> not found", "Room %1 not found.", text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
std::nullopt,
|
||||||
|
kli18n("[<room alias or id>]"),
|
||||||
|
kli18n("Leaves the given room or this room, if there is none given"),
|
||||||
|
},
|
||||||
|
Action{
|
||||||
|
QStringLiteral("ignore"),
|
||||||
|
[](const QString &text, NeoChatRoom *room) {
|
||||||
|
static const QRegularExpression mxidRegex(
|
||||||
|
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
|
||||||
|
auto regexMatch = mxidRegex.match(text);
|
||||||
|
if (!regexMatch.hasMatch()) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
auto user = room->connection()->users()[text];
|
||||||
|
if (room->connection()->ignoredUsers().contains(user->id())) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<username> is already ignored.", "%1 is already ignored.", text));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
if (user) {
|
||||||
|
room->connection()->addToIgnoredUsers(user);
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> is now ignored", "%1 is now ignored.", text));
|
||||||
|
} else {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("<username> is not a known user", "%1 is not a known user.", text));
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
std::nullopt,
|
||||||
|
kli18n("<user id>"),
|
||||||
|
kli18n("Ignores the given user"),
|
||||||
|
},
|
||||||
|
Action{
|
||||||
|
QStringLiteral("unignore"),
|
||||||
|
[](const QString &text, NeoChatRoom *room) {
|
||||||
|
static const QRegularExpression mxidRegex(
|
||||||
|
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
|
||||||
|
auto regexMatch = mxidRegex.match(text);
|
||||||
|
if (!regexMatch.hasMatch()) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
auto user = room->connection()->users()[text];
|
||||||
|
if (user) {
|
||||||
|
if (!room->connection()->ignoredUsers().contains(user->id())) {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<username> is not ignored.", "%1 is not ignored.", text));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
room->connection()->removeFromIgnoredUsers(user);
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> is no longer ignored.", "%1 is no longer ignored.", text));
|
||||||
|
} else {
|
||||||
|
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("<username> is not a known user", "%1 is not a known user.", text));
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
std::nullopt,
|
||||||
|
kli18n("<user id>"),
|
||||||
|
kli18n("Unignores the given user"),
|
||||||
|
},
|
||||||
|
Action{
|
||||||
|
QStringLiteral("react"),
|
||||||
|
[](const QString &text, NeoChatRoom *room) {
|
||||||
|
QString replyEventId = room->chatBoxReplyId();
|
||||||
|
if (replyEventId.isEmpty()) {
|
||||||
|
for (auto it = room->messageEvents().crbegin(); it != room->messageEvents().crend(); it++) {
|
||||||
|
const auto &evt = **it;
|
||||||
|
if (const auto event = eventCast<const RoomMessageEvent>(&evt)) {
|
||||||
|
room->toggleReaction(event->id(), text);
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
room->toggleReaction(replyEventId, text);
|
||||||
|
return QString();
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
std::nullopt,
|
||||||
|
kli18n("<reaction text>"),
|
||||||
|
kli18n("React to the message with the given text"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
int ActionsModel::rowCount(const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(parent);
|
||||||
|
return actions.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant ActionsModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
if (index.row() < 0 || index.row() >= actions.size()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (role == Prefix) {
|
||||||
|
return actions[index.row()].prefix;
|
||||||
|
}
|
||||||
|
if (role == Description) {
|
||||||
|
return actions[index.row()].description.toString();
|
||||||
|
}
|
||||||
|
if (role == CompletionType) {
|
||||||
|
return QStringLiteral("action");
|
||||||
|
}
|
||||||
|
if (role == Parameters) {
|
||||||
|
return actions[index.row()].parameters.toString();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> ActionsModel::roleNames() const
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
{Prefix, "prefix"},
|
||||||
|
{Description, "description"},
|
||||||
|
{CompletionType, "completionType"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<Action> &ActionsModel::allActions() const
|
||||||
|
{
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
49
src/actionsmodel.h
Normal file
49
src/actionsmodel.h
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <KLazyLocalizedString>
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <events/roommessageevent.h>
|
||||||
|
|
||||||
|
class NeoChatRoom;
|
||||||
|
|
||||||
|
class ActionsModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct Action {
|
||||||
|
// The prefix, without '/' and space after the word
|
||||||
|
QString prefix;
|
||||||
|
std::function<QString(const QString &, NeoChatRoom *)> handle;
|
||||||
|
// If this is true, this action transforms a message to a different message and it will be sent.
|
||||||
|
// If this is false, this message does some action on the client and should not be sent as a message.
|
||||||
|
bool messageAction;
|
||||||
|
// If this action changes the message type, this is the new message type. Otherwise it's nullopt
|
||||||
|
std::optional<Quotient::RoomMessageEvent::MsgType> messageType = std::nullopt;
|
||||||
|
KLazyLocalizedString parameters;
|
||||||
|
KLazyLocalizedString description;
|
||||||
|
};
|
||||||
|
static ActionsModel &instance()
|
||||||
|
{
|
||||||
|
static ActionsModel _instance;
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Roles {
|
||||||
|
Prefix = Qt::DisplayRole,
|
||||||
|
Description,
|
||||||
|
CompletionType,
|
||||||
|
Parameters,
|
||||||
|
};
|
||||||
|
Q_ENUM(Roles);
|
||||||
|
|
||||||
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
|
QVector<Action> &allActions() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ActionsModel() = default;
|
||||||
|
};
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QVariant>
|
|
||||||
|
|
||||||
/// Helper singleton for keeping the chatbar state in sync in the application.
|
|
||||||
class ChatBoxHelper : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
/// True, iff the user is currently editing one of their previous message.
|
|
||||||
Q_PROPERTY(bool isEditing READ isEditing NOTIFY isEditingChanged)
|
|
||||||
Q_PROPERTY(QString editEventId READ editEventId WRITE setEditEventId NOTIFY editEventIdChanged)
|
|
||||||
Q_PROPERTY(QString editContent READ editContent WRITE setEditContent NOTIFY editContentChanged)
|
|
||||||
|
|
||||||
Q_PROPERTY(bool isReplying READ isReplying NOTIFY isReplyingChanged)
|
|
||||||
Q_PROPERTY(QString replyEventId READ replyEventId WRITE setReplyEventId NOTIFY replyEventIdChanged)
|
|
||||||
Q_PROPERTY(QString replyEventContent READ replyEventContent WRITE setReplyEventContent NOTIFY replyEventContentChanged)
|
|
||||||
Q_PROPERTY(QVariant replyUser READ replyUser WRITE setReplyUser NOTIFY replyUserChanged)
|
|
||||||
|
|
||||||
Q_PROPERTY(QString attachmentPath READ attachmentPath WRITE setAttachmentPath NOTIFY attachmentPathChanged)
|
|
||||||
Q_PROPERTY(bool hasAttachment READ hasAttachment NOTIFY hasAttachmentChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
ChatBoxHelper(QObject *parent = nullptr);
|
|
||||||
~ChatBoxHelper() = default;
|
|
||||||
|
|
||||||
bool isEditing() const;
|
|
||||||
QString editEventId() const;
|
|
||||||
QString editContent() const;
|
|
||||||
|
|
||||||
QString replyEventId() const;
|
|
||||||
QString replyEventContent() const;
|
|
||||||
QVariant replyUser() const;
|
|
||||||
bool isReplying() const;
|
|
||||||
|
|
||||||
QString attachmentPath() const;
|
|
||||||
bool hasAttachment() const;
|
|
||||||
|
|
||||||
void setEditEventId(const QString &editEventId);
|
|
||||||
void setEditContent(const QString &editContent);
|
|
||||||
void setReplyEventId(const QString &replyEventId);
|
|
||||||
void setReplyEventContent(const QString &replyEventContent);
|
|
||||||
void setAttachmentPath(const QString &attachmentPath);
|
|
||||||
void setReplyUser(const QVariant &replyUser);
|
|
||||||
|
|
||||||
Q_INVOKABLE void replyToMessage(const QString &replyEventid, const QString &replyEvent, const QVariant &replyUser);
|
|
||||||
Q_INVOKABLE void edit(const QString &message, const QString &formattedBody, const QString &eventId);
|
|
||||||
Q_INVOKABLE void clear();
|
|
||||||
Q_INVOKABLE void clearEditReply();
|
|
||||||
Q_INVOKABLE void clearAttachment();
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void isEditingChanged(bool isEditing);
|
|
||||||
void editEventIdChanged(const QString &editEventId);
|
|
||||||
void editContentChanged();
|
|
||||||
void replyEventIdChanged(const QString &replyEventId);
|
|
||||||
void replyEventContentChanged(const QString &replyEventContent);
|
|
||||||
void replyUserChanged();
|
|
||||||
void isReplyingChanged(bool isReplying);
|
|
||||||
void attachmentPathChanged(const QString &attachmentPath);
|
|
||||||
void hasAttachmentChanged(bool hasAttachment);
|
|
||||||
void editing(const QString &message, const QString &formattedBody);
|
|
||||||
void shouldClearText();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QString m_editEventId;
|
|
||||||
QString m_editContent;
|
|
||||||
QString m_replyEventId;
|
|
||||||
QString m_replyEventContent;
|
|
||||||
QVariant m_replyUser;
|
|
||||||
QString m_attachmentPath;
|
|
||||||
};
|
|
||||||
@@ -7,18 +7,138 @@
|
|||||||
#include <QQmlFileSelector>
|
#include <QQmlFileSelector>
|
||||||
#include <QQuickTextDocument>
|
#include <QQuickTextDocument>
|
||||||
#include <QStringBuilder>
|
#include <QStringBuilder>
|
||||||
|
#include <QSyntaxHighlighter>
|
||||||
#include <QTextBlock>
|
#include <QTextBlock>
|
||||||
#include <QTextDocument>
|
#include <QTextDocument>
|
||||||
|
|
||||||
|
#include <Sonnet/BackgroundChecker>
|
||||||
|
#include <Sonnet/Settings>
|
||||||
|
|
||||||
|
#include "actionsmodel.h"
|
||||||
|
#include "completionmodel.h"
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
|
#include "roomlistmodel.h"
|
||||||
|
|
||||||
|
class SyntaxHighlighter : public QSyntaxHighlighter
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
QTextCharFormat mentionFormat;
|
||||||
|
QTextCharFormat errorFormat;
|
||||||
|
Sonnet::BackgroundChecker *checker = new Sonnet::BackgroundChecker;
|
||||||
|
Sonnet::Settings settings;
|
||||||
|
QList<QPair<int, QString>> errors;
|
||||||
|
QString previousText;
|
||||||
|
SyntaxHighlighter(QObject *parent)
|
||||||
|
: QSyntaxHighlighter(parent)
|
||||||
|
{
|
||||||
|
mentionFormat.setFontWeight(QFont::Bold);
|
||||||
|
mentionFormat.setForeground(Qt::blue);
|
||||||
|
|
||||||
|
errorFormat.setForeground(Qt::red);
|
||||||
|
errorFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
|
||||||
|
|
||||||
|
connect(checker, &Sonnet::BackgroundChecker::misspelling, this, [this](const QString &word, int start) {
|
||||||
|
errors += {start, word};
|
||||||
|
rehighlight();
|
||||||
|
checker->continueChecking();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
void highlightBlock(const QString &text) override
|
||||||
|
{
|
||||||
|
if (settings.checkerEnabledByDefault()) {
|
||||||
|
if (text != previousText) {
|
||||||
|
previousText = text;
|
||||||
|
checker->stop();
|
||||||
|
errors.clear();
|
||||||
|
checker->setText(text);
|
||||||
|
}
|
||||||
|
for (const auto &error : errors) {
|
||||||
|
setFormat(error.first, error.second.size(), errorFormat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto room = dynamic_cast<ChatDocumentHandler *>(parent())->room();
|
||||||
|
if (!room) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto mentions = room->mentions();
|
||||||
|
mentions->erase(std::remove_if(mentions->begin(),
|
||||||
|
mentions->end(),
|
||||||
|
[this](auto &mention) {
|
||||||
|
if (document()->toPlainText().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mention.cursor.position() == 0 && mention.cursor.anchor() == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mention.cursor.position() - mention.cursor.anchor() != mention.text.size()) {
|
||||||
|
mention.cursor.setPosition(mention.start);
|
||||||
|
mention.cursor.setPosition(mention.cursor.anchor() + mention.text.size(), QTextCursor::KeepAnchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mention.cursor.selectedText() != mention.text) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (currentBlock() == mention.cursor.block()) {
|
||||||
|
mention.start = mention.cursor.anchor();
|
||||||
|
mention.position = mention.cursor.position();
|
||||||
|
setFormat(mention.cursor.selectionStart(), mention.cursor.selectedText().size(), mentionFormat);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
mentions->end());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
|
ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_document(nullptr)
|
, m_document(nullptr)
|
||||||
, m_cursorPosition(-1)
|
, m_cursorPosition(-1)
|
||||||
, m_selectionStart(-1)
|
, m_highlighter(new SyntaxHighlighter(this))
|
||||||
, m_selectionEnd(-1)
|
, m_completionModel(new CompletionModel())
|
||||||
{
|
{
|
||||||
|
connect(this, &ChatDocumentHandler::roomChanged, this, [this]() {
|
||||||
|
m_completionModel->setRoom(m_room);
|
||||||
|
static NeoChatRoom *previousRoom = nullptr;
|
||||||
|
if (previousRoom) {
|
||||||
|
disconnect(nullptr, &NeoChatRoom::chatBoxTextChanged, this, nullptr);
|
||||||
|
}
|
||||||
|
previousRoom = m_room;
|
||||||
|
connect(m_room, &NeoChatRoom::chatBoxTextChanged, this, [this]() {
|
||||||
|
int start = completionStartIndex();
|
||||||
|
m_completionModel->setText(m_room->chatBoxText().mid(start, cursorPosition() - start), m_room->chatBoxText().mid(start));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
connect(this, &ChatDocumentHandler::documentChanged, this, [this]() {
|
||||||
|
m_highlighter->setDocument(m_document->textDocument());
|
||||||
|
});
|
||||||
|
connect(this, &ChatDocumentHandler::cursorPositionChanged, this, [this]() {
|
||||||
|
if (!m_room) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int start = completionStartIndex();
|
||||||
|
m_completionModel->setText(m_room->chatBoxText().mid(start, cursorPosition() - start), m_room->chatBoxText().mid(start));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatDocumentHandler::completionStartIndex() const
|
||||||
|
{
|
||||||
|
if (!m_room) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &cursor = cursorPosition();
|
||||||
|
const auto &text = m_room->chatBoxText();
|
||||||
|
auto start = std::min(cursor, text.size()) - 1;
|
||||||
|
while (start > -1) {
|
||||||
|
if (text.at(start) == QLatin1Char(' ')) {
|
||||||
|
start++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
start--;
|
||||||
|
}
|
||||||
|
return start;
|
||||||
}
|
}
|
||||||
|
|
||||||
QQuickTextDocument *ChatDocumentHandler::document() const
|
QQuickTextDocument *ChatDocumentHandler::document() const
|
||||||
@@ -49,67 +169,12 @@ void ChatDocumentHandler::setCursorPosition(int position)
|
|||||||
if (position == m_cursorPosition) {
|
if (position == m_cursorPosition) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (m_room) {
|
||||||
m_cursorPosition = position;
|
m_cursorPosition = position;
|
||||||
|
}
|
||||||
Q_EMIT cursorPositionChanged();
|
Q_EMIT cursorPositionChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChatDocumentHandler::selectionStart() const
|
|
||||||
{
|
|
||||||
return m_selectionStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::setSelectionStart(int position)
|
|
||||||
{
|
|
||||||
if (position == m_selectionStart) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_selectionStart = position;
|
|
||||||
Q_EMIT selectionStartChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChatDocumentHandler::selectionEnd() const
|
|
||||||
{
|
|
||||||
return m_selectionEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatDocumentHandler::setSelectionEnd(int position)
|
|
||||||
{
|
|
||||||
if (position == m_selectionEnd) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_selectionEnd = position;
|
|
||||||
Q_EMIT selectionEndChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QTextCursor ChatDocumentHandler::textCursor() const
|
|
||||||
{
|
|
||||||
QTextDocument *doc = textDocument();
|
|
||||||
if (!doc) {
|
|
||||||
return QTextCursor();
|
|
||||||
}
|
|
||||||
|
|
||||||
QTextCursor cursor = QTextCursor(doc);
|
|
||||||
if (m_selectionStart != m_selectionEnd) {
|
|
||||||
cursor.setPosition(m_selectionStart);
|
|
||||||
cursor.setPosition(m_selectionEnd, QTextCursor::KeepAnchor);
|
|
||||||
} else {
|
|
||||||
cursor.setPosition(m_cursorPosition);
|
|
||||||
}
|
|
||||||
return cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTextDocument *ChatDocumentHandler::textDocument() const
|
|
||||||
{
|
|
||||||
if (!m_document) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return m_document->textDocument();
|
|
||||||
}
|
|
||||||
|
|
||||||
NeoChatRoom *ChatDocumentHandler::room() const
|
NeoChatRoom *ChatDocumentHandler::room() const
|
||||||
{
|
{
|
||||||
return m_room;
|
return m_room;
|
||||||
@@ -125,91 +190,55 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
|
|||||||
Q_EMIT roomChanged();
|
Q_EMIT roomChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantMap ChatDocumentHandler::getAutocompletionInfo(bool isAutocompleting)
|
void ChatDocumentHandler::complete(int index)
|
||||||
{
|
{
|
||||||
QTextCursor cursor = textCursor();
|
if (m_completionModel->autoCompletionType() == ChatDocumentHandler::User) {
|
||||||
|
auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Text).toString();
|
||||||
if (cursor.block().text() == m_lastState) {
|
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString();
|
||||||
// ignore change, it was caused by autocompletion
|
auto text = m_room->chatBoxText();
|
||||||
return QVariantMap{
|
auto at = text.lastIndexOf(QLatin1Char('@'), cursorPosition() - 1);
|
||||||
{"type", AutoCompletionType::Ignore},
|
QTextCursor cursor(document()->textDocument());
|
||||||
};
|
cursor.setPosition(at);
|
||||||
|
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
||||||
|
cursor.insertText(name % " ");
|
||||||
|
cursor.setPosition(at);
|
||||||
|
cursor.setPosition(cursor.position() + name.size(), QTextCursor::KeepAnchor);
|
||||||
|
cursor.setKeepPositionOnInsert(true);
|
||||||
|
m_room->mentions()->push_back({cursor, name, 0, 0, id});
|
||||||
|
m_highlighter->rehighlight();
|
||||||
|
} else if (m_completionModel->autoCompletionType() == ChatDocumentHandler::Command) {
|
||||||
|
auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString();
|
||||||
|
auto text = m_room->chatBoxText();
|
||||||
|
auto at = text.lastIndexOf(QLatin1Char('/'));
|
||||||
|
QTextCursor cursor(document()->textDocument());
|
||||||
|
cursor.setPosition(at);
|
||||||
|
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
||||||
|
cursor.insertText(QStringLiteral("/%1 ").arg(command));
|
||||||
|
} else if (m_completionModel->autoCompletionType() == ChatDocumentHandler::Room) {
|
||||||
|
auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString();
|
||||||
|
auto text = m_room->chatBoxText();
|
||||||
|
auto at = text.lastIndexOf(QLatin1Char('#'), cursorPosition() - 1);
|
||||||
|
QTextCursor cursor(document()->textDocument());
|
||||||
|
cursor.setPosition(at);
|
||||||
|
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
||||||
|
cursor.insertText(alias % " ");
|
||||||
|
cursor.setPosition(at);
|
||||||
|
cursor.setPosition(cursor.position() + alias.size(), QTextCursor::KeepAnchor);
|
||||||
|
cursor.setKeepPositionOnInsert(true);
|
||||||
|
m_room->mentions()->push_back({cursor, alias, 0, 0, alias});
|
||||||
|
m_highlighter->rehighlight();
|
||||||
|
} else if (m_completionModel->autoCompletionType() == ChatDocumentHandler::Emoji) {
|
||||||
|
auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Text).toString();
|
||||||
|
auto text = m_room->chatBoxText();
|
||||||
|
auto at = text.lastIndexOf(QLatin1Char(':'));
|
||||||
|
QTextCursor cursor(document()->textDocument());
|
||||||
|
cursor.setPosition(at);
|
||||||
|
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
||||||
|
cursor.insertText(shortcode);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString text = cursor.block().text();
|
|
||||||
QString textBeforeCursor = text;
|
|
||||||
textBeforeCursor.truncate(m_cursorPosition);
|
|
||||||
|
|
||||||
QString autoCompletePrefix = textBeforeCursor.section(" ", -1);
|
|
||||||
|
|
||||||
if (autoCompletePrefix.isEmpty()) {
|
|
||||||
return QVariantMap{
|
|
||||||
{"type", AutoCompletionType::None},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoCompletePrefix.startsWith("@") || autoCompletePrefix.startsWith(":") || autoCompletePrefix.startsWith("/")) {
|
|
||||||
m_autoCompleteBeginPosition = textBeforeCursor.lastIndexOf(" ") + 1; // 1 == space
|
|
||||||
|
|
||||||
if (autoCompletePrefix.startsWith("@")) {
|
|
||||||
autoCompletePrefix.remove(0, 1);
|
|
||||||
return QVariantMap{
|
|
||||||
{"keyword", autoCompletePrefix},
|
|
||||||
{"type", AutoCompletionType::User},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoCompletePrefix.startsWith("/") && text.trimmed().length() <= 1) {
|
|
||||||
return QVariantMap{
|
|
||||||
{"keyword", autoCompletePrefix},
|
|
||||||
{"type", AutoCompletionType::Command},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAutocompleting) {
|
|
||||||
return QVariantMap{
|
|
||||||
{"keyword", autoCompletePrefix},
|
|
||||||
{"type", AutoCompletionType::Emoji},
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return QVariantMap{
|
|
||||||
{"type", AutoCompletionType::Ignore},
|
|
||||||
{"keyword", autoCompletePrefix},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return QVariantMap{
|
|
||||||
{"type", AutoCompletionType::None},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatDocumentHandler::replaceAutoComplete(const QString &word)
|
CompletionModel *ChatDocumentHandler::completionModel() const
|
||||||
{
|
{
|
||||||
QTextCursor cursor = textCursor();
|
return m_completionModel;
|
||||||
if (cursor.block().text() == m_lastState) {
|
|
||||||
m_document->textDocument()->undo();
|
|
||||||
}
|
|
||||||
cursor.beginEditBlock();
|
|
||||||
cursor.select(QTextCursor::WordUnderCursor);
|
|
||||||
cursor.removeSelectedText();
|
|
||||||
cursor.deletePreviousChar();
|
|
||||||
while (!cursor.atBlockStart()) {
|
|
||||||
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
|
|
||||||
|
|
||||||
if (cursor.selectedText() == " ") {
|
|
||||||
cursor.movePosition(QTextCursor::NextCharacter);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor.insertHtml(word);
|
|
||||||
|
|
||||||
// Add space after autocomplete if not already there
|
|
||||||
if (!cursor.block().text().endsWith(QStringLiteral(" "))) {
|
|
||||||
cursor.insertText(QStringLiteral(" "));
|
|
||||||
}
|
|
||||||
|
|
||||||
m_lastState = cursor.block().text();
|
|
||||||
cursor.endEditBlock();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,30 +4,33 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
|
||||||
#include <QTextCursor>
|
#include <QTextCursor>
|
||||||
|
|
||||||
|
#include "userlistmodel.h"
|
||||||
|
|
||||||
class QTextDocument;
|
class QTextDocument;
|
||||||
class QQuickTextDocument;
|
class QQuickTextDocument;
|
||||||
class NeoChatRoom;
|
class NeoChatRoom;
|
||||||
class Controller;
|
class SyntaxHighlighter;
|
||||||
|
class CompletionModel;
|
||||||
|
|
||||||
class ChatDocumentHandler : public QObject
|
class ChatDocumentHandler : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged)
|
Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged)
|
||||||
Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged)
|
Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged)
|
||||||
Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged)
|
Q_PROPERTY(CompletionModel *completionModel READ completionModel NOTIFY completionModelChanged)
|
||||||
Q_PROPERTY(int selectionEnd READ selectionEnd WRITE setSelectionEnd NOTIFY selectionEndChanged)
|
|
||||||
|
|
||||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
Q_PROPERTY(NeoChatRoom *room READ room NOTIFY roomChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum AutoCompletionType {
|
enum AutoCompletionType {
|
||||||
User,
|
User,
|
||||||
|
Room,
|
||||||
Emoji,
|
Emoji,
|
||||||
Command,
|
Command,
|
||||||
None,
|
None,
|
||||||
Ignore,
|
|
||||||
};
|
};
|
||||||
Q_ENUM(AutoCompletionType)
|
Q_ENUM(AutoCompletionType)
|
||||||
|
|
||||||
@@ -39,44 +42,34 @@ public:
|
|||||||
[[nodiscard]] int cursorPosition() const;
|
[[nodiscard]] int cursorPosition() const;
|
||||||
void setCursorPosition(int position);
|
void setCursorPosition(int position);
|
||||||
|
|
||||||
[[nodiscard]] int selectionStart() const;
|
|
||||||
void setSelectionStart(int position);
|
|
||||||
|
|
||||||
[[nodiscard]] int selectionEnd() const;
|
|
||||||
void setSelectionEnd(int position);
|
|
||||||
|
|
||||||
[[nodiscard]] NeoChatRoom *room() const;
|
[[nodiscard]] NeoChatRoom *room() const;
|
||||||
void setRoom(NeoChatRoom *room);
|
void setRoom(NeoChatRoom *room);
|
||||||
|
|
||||||
/// This function will look at the current QTextCursor and determine if there
|
Q_INVOKABLE void complete(int index);
|
||||||
/// is the possibility to autocomplete it.
|
|
||||||
Q_INVOKABLE QVariantMap getAutocompletionInfo(bool isAutocompleting);
|
|
||||||
Q_INVOKABLE void replaceAutoComplete(const QString &word);
|
|
||||||
|
|
||||||
|
void updateCompletions();
|
||||||
|
CompletionModel *completionModel() const;
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void documentChanged();
|
void documentChanged();
|
||||||
void cursorPositionChanged();
|
void cursorPositionChanged();
|
||||||
void selectionStartChanged();
|
|
||||||
void selectionEndChanged();
|
|
||||||
void roomChanged();
|
void roomChanged();
|
||||||
void joinRoom(QString roomName);
|
void completionModelChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
[[nodiscard]] QTextCursor textCursor() const;
|
int completionStartIndex() const;
|
||||||
[[nodiscard]] QTextDocument *textDocument() const;
|
|
||||||
|
|
||||||
QQuickTextDocument *m_document;
|
QQuickTextDocument *m_document;
|
||||||
|
|
||||||
NeoChatRoom *m_room;
|
NeoChatRoom *m_room = nullptr;
|
||||||
|
bool completionVisible = false;
|
||||||
|
|
||||||
int m_cursorPosition;
|
int m_cursorPosition;
|
||||||
int m_selectionStart;
|
|
||||||
int m_selectionEnd;
|
|
||||||
|
|
||||||
int m_autoCompleteBeginPosition = -1;
|
SyntaxHighlighter *m_highlighter = nullptr;
|
||||||
int m_autoCompleteEndPosition = -1;
|
|
||||||
|
|
||||||
QString m_lastState;
|
AutoCompletionType m_completionType = None;
|
||||||
|
|
||||||
|
CompletionModel *m_completionModel = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(ChatDocumentHandler::AutoCompletionType);
|
Q_DECLARE_METATYPE(ChatDocumentHandler::AutoCompletionType);
|
||||||
|
|||||||
@@ -4,12 +4,14 @@
|
|||||||
#include "clipboard.h"
|
#include "clipboard.h"
|
||||||
|
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
|
#include <QDateTime>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <QMimeData>
|
#include <QMimeData>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
|
#include <QStandardPaths>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
Clipboard::Clipboard(QObject *parent)
|
Clipboard::Clipboard(QObject *parent)
|
||||||
@@ -29,27 +31,31 @@ QImage Clipboard::image() const
|
|||||||
return m_clipboard->image();
|
return m_clipboard->image();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Clipboard::saveImage(const QUrl &localPath) const
|
QString Clipboard::saveImage(QString localPath) const
|
||||||
{
|
{
|
||||||
if (!localPath.isLocalFile()) {
|
if (localPath.isEmpty()) {
|
||||||
return false;
|
localPath = QStringLiteral("file://%1/screenshots/%2.png")
|
||||||
|
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation),
|
||||||
|
QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd-hh-mm-ss")));
|
||||||
|
}
|
||||||
|
QUrl url(localPath);
|
||||||
|
if (!url.isLocalFile()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
auto image = this->image();
|
||||||
|
|
||||||
|
if (image.isNull()) {
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto i = image();
|
|
||||||
|
|
||||||
if (i.isNull()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString path = QFileInfo(localPath.toLocalFile()).absolutePath();
|
|
||||||
QDir dir;
|
QDir dir;
|
||||||
if (!dir.exists(path)) {
|
if (!dir.exists(localPath)) {
|
||||||
dir.mkpath(path);
|
dir.mkpath(localPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
i.save(localPath.toLocalFile());
|
image.save(url.toLocalFile());
|
||||||
|
|
||||||
return true;
|
return localPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Clipboard::saveText(QString message)
|
void Clipboard::saveText(QString message)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public:
|
|||||||
[[nodiscard]] bool hasImage() const;
|
[[nodiscard]] bool hasImage() const;
|
||||||
[[nodiscard]] QImage image() const;
|
[[nodiscard]] QImage image() const;
|
||||||
|
|
||||||
Q_INVOKABLE bool saveImage(const QUrl &localPath) const;
|
Q_INVOKABLE QString saveImage(QString localPath = {}) const;
|
||||||
|
|
||||||
Q_INVOKABLE void saveText(QString message);
|
Q_INVOKABLE void saveText(QString message);
|
||||||
|
|
||||||
|
|||||||
187
src/completionmodel.cpp
Normal file
187
src/completionmodel.cpp
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "completionmodel.h"
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
#include "actionsmodel.h"
|
||||||
|
#include "chatdocumenthandler.h"
|
||||||
|
#include "completionproxymodel.h"
|
||||||
|
#include "customemojimodel.h"
|
||||||
|
#include "neochatroom.h"
|
||||||
|
#include "roomlistmodel.h"
|
||||||
|
#include "userlistmodel.h"
|
||||||
|
|
||||||
|
CompletionModel::CompletionModel(QObject *parent)
|
||||||
|
: QAbstractListModel(parent)
|
||||||
|
, m_filterModel(new CompletionProxyModel())
|
||||||
|
, m_userListModel(new UserListModel(this))
|
||||||
|
{
|
||||||
|
connect(this, &CompletionModel::textChanged, this, &CompletionModel::updateCompletion);
|
||||||
|
connect(this, &CompletionModel::roomChanged, this, [this]() {
|
||||||
|
m_userListModel->setRoom(m_room);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QString CompletionModel::text() const
|
||||||
|
{
|
||||||
|
return m_text;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompletionModel::setText(const QString &text, const QString &fullText)
|
||||||
|
{
|
||||||
|
m_text = text;
|
||||||
|
m_fullText = fullText;
|
||||||
|
Q_EMIT textChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
int CompletionModel::rowCount(const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(parent);
|
||||||
|
if (m_autoCompletionType == ChatDocumentHandler::None) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return m_filterModel->rowCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant CompletionModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
if (index.row() < 0 || index.row() >= m_filterModel->rowCount()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
auto filterIndex = m_filterModel->index(index.row(), 0);
|
||||||
|
if (m_autoCompletionType == ChatDocumentHandler::User) {
|
||||||
|
if (role == Text) {
|
||||||
|
return m_filterModel->data(filterIndex, UserListModel::NameRole);
|
||||||
|
}
|
||||||
|
if (role == Subtitle) {
|
||||||
|
return m_filterModel->data(filterIndex, UserListModel::UserIDRole);
|
||||||
|
}
|
||||||
|
if (role == Icon) {
|
||||||
|
return m_filterModel->data(filterIndex, UserListModel::AvatarRole);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_autoCompletionType == ChatDocumentHandler::Command) {
|
||||||
|
if (role == Text) {
|
||||||
|
return m_filterModel->data(filterIndex, ActionsModel::Prefix).toString() + QStringLiteral(" ")
|
||||||
|
+ m_filterModel->data(filterIndex, ActionsModel::Parameters).toString();
|
||||||
|
}
|
||||||
|
if (role == Subtitle) {
|
||||||
|
return m_filterModel->data(filterIndex, ActionsModel::Description);
|
||||||
|
}
|
||||||
|
if (role == Icon) {
|
||||||
|
return QStringLiteral("invalid");
|
||||||
|
}
|
||||||
|
if (role == ReplacedText) {
|
||||||
|
return m_filterModel->data(filterIndex, ActionsModel::Prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_autoCompletionType == ChatDocumentHandler::Room) {
|
||||||
|
if (role == Text) {
|
||||||
|
return m_filterModel->data(filterIndex, RoomListModel::DisplayNameRole);
|
||||||
|
}
|
||||||
|
if (role == Subtitle) {
|
||||||
|
return m_filterModel->data(filterIndex, RoomListModel::CanonicalAliasRole);
|
||||||
|
}
|
||||||
|
if (role == Icon) {
|
||||||
|
return m_filterModel->data(filterIndex, RoomListModel::AvatarRole);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_autoCompletionType == ChatDocumentHandler::Emoji) {
|
||||||
|
if (role == Text) {
|
||||||
|
return m_filterModel->data(filterIndex, CustomEmojiModel::Name);
|
||||||
|
}
|
||||||
|
if (role == Icon) {
|
||||||
|
return m_filterModel->data(filterIndex, CustomEmojiModel::MxcUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> CompletionModel::roleNames() const
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
{Text, "text"},
|
||||||
|
{Subtitle, "subtitle"},
|
||||||
|
{Icon, "icon"},
|
||||||
|
{ReplacedText, "replacedText"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompletionModel::updateCompletion()
|
||||||
|
{
|
||||||
|
if (text().startsWith(QLatin1Char('@'))) {
|
||||||
|
m_filterModel->setSourceModel(m_userListModel);
|
||||||
|
m_filterModel->setFilterRole(UserListModel::UserIDRole);
|
||||||
|
m_filterModel->setSecondaryFilterRole(UserListModel::NameRole);
|
||||||
|
m_filterModel->setFullText(m_fullText);
|
||||||
|
m_filterModel->setFilterText(m_text);
|
||||||
|
m_autoCompletionType = ChatDocumentHandler::User;
|
||||||
|
m_filterModel->invalidate();
|
||||||
|
} else if (text().startsWith(QLatin1Char('/'))) {
|
||||||
|
m_filterModel->setSourceModel(&ActionsModel::instance());
|
||||||
|
m_filterModel->setFilterRole(ActionsModel::Prefix);
|
||||||
|
m_filterModel->setSecondaryFilterRole(-1);
|
||||||
|
m_filterModel->setFullText(m_fullText);
|
||||||
|
m_filterModel->setFilterText(m_text.mid(1));
|
||||||
|
m_autoCompletionType = ChatDocumentHandler::Command;
|
||||||
|
m_filterModel->invalidate();
|
||||||
|
} else if (text().startsWith(QLatin1Char('#'))) {
|
||||||
|
m_autoCompletionType = ChatDocumentHandler::Room;
|
||||||
|
m_filterModel->setSourceModel(m_roomListModel);
|
||||||
|
m_filterModel->setFilterRole(RoomListModel::CanonicalAliasRole);
|
||||||
|
m_filterModel->setSecondaryFilterRole(RoomListModel::DisplayNameRole);
|
||||||
|
m_filterModel->setFullText(m_fullText);
|
||||||
|
m_filterModel->setFilterText(m_text);
|
||||||
|
m_filterModel->invalidate();
|
||||||
|
} else if (text().startsWith(QLatin1Char(':'))
|
||||||
|
&& (m_fullText.indexOf(QLatin1Char(':'), 1) == -1
|
||||||
|
|| (m_fullText.indexOf(QLatin1Char(' ')) != -1 && m_fullText.indexOf(QLatin1Char(':'), 1) > m_fullText.indexOf(QLatin1Char(' '), 1)))) {
|
||||||
|
m_autoCompletionType = ChatDocumentHandler::Emoji;
|
||||||
|
m_filterModel->setSourceModel(&CustomEmojiModel::instance());
|
||||||
|
m_filterModel->setFilterRole(CustomEmojiModel::Name);
|
||||||
|
m_filterModel->setSecondaryFilterRole(-1);
|
||||||
|
m_filterModel->setFullText(m_fullText);
|
||||||
|
m_filterModel->setFilterText(m_text);
|
||||||
|
m_filterModel->invalidate();
|
||||||
|
} else {
|
||||||
|
m_autoCompletionType = ChatDocumentHandler::None;
|
||||||
|
}
|
||||||
|
beginResetModel();
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
NeoChatRoom *CompletionModel::room() const
|
||||||
|
{
|
||||||
|
return m_room;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompletionModel::setRoom(NeoChatRoom *room)
|
||||||
|
{
|
||||||
|
m_room = room;
|
||||||
|
Q_EMIT roomChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatDocumentHandler::AutoCompletionType CompletionModel::autoCompletionType() const
|
||||||
|
{
|
||||||
|
return m_autoCompletionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompletionModel::setAutoCompletionType(ChatDocumentHandler::AutoCompletionType autoCompletionType)
|
||||||
|
{
|
||||||
|
m_autoCompletionType = autoCompletionType;
|
||||||
|
Q_EMIT autoCompletionTypeChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomListModel *CompletionModel::roomListModel() const
|
||||||
|
{
|
||||||
|
return m_roomListModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompletionModel::setRoomListModel(RoomListModel *roomListModel)
|
||||||
|
{
|
||||||
|
m_roomListModel = roomListModel;
|
||||||
|
Q_EMIT roomListModelChanged();
|
||||||
|
}
|
||||||
67
src/completionmodel.h
Normal file
67
src/completionmodel.h
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
|
||||||
|
#include "chatdocumenthandler.h"
|
||||||
|
|
||||||
|
class CompletionProxyModel;
|
||||||
|
class UserListModel;
|
||||||
|
class NeoChatRoom;
|
||||||
|
class RoomListModel;
|
||||||
|
|
||||||
|
class CompletionModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QString text READ text NOTIFY textChanged)
|
||||||
|
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||||
|
Q_PROPERTY(ChatDocumentHandler::AutoCompletionType autoCompletionType READ autoCompletionType NOTIFY autoCompletionTypeChanged);
|
||||||
|
Q_PROPERTY(RoomListModel *roomListModel READ roomListModel WRITE setRoomListModel NOTIFY roomListModelChanged);
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum Roles {
|
||||||
|
Text = Qt::DisplayRole,
|
||||||
|
Subtitle,
|
||||||
|
Icon,
|
||||||
|
ReplacedText,
|
||||||
|
};
|
||||||
|
Q_ENUM(Roles);
|
||||||
|
|
||||||
|
CompletionModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
|
QString text() const;
|
||||||
|
void setText(const QString &text, const QString &fullText);
|
||||||
|
void updateCompletion();
|
||||||
|
|
||||||
|
NeoChatRoom *room() const;
|
||||||
|
void setRoom(NeoChatRoom *room);
|
||||||
|
|
||||||
|
RoomListModel *roomListModel() const;
|
||||||
|
void setRoomListModel(RoomListModel *roomListModel);
|
||||||
|
|
||||||
|
ChatDocumentHandler::AutoCompletionType autoCompletionType() const;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void textChanged();
|
||||||
|
void roomChanged();
|
||||||
|
void autoCompletionTypeChanged();
|
||||||
|
void roomListModelChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_text;
|
||||||
|
QString m_fullText;
|
||||||
|
CompletionProxyModel *m_filterModel;
|
||||||
|
NeoChatRoom *m_room = nullptr;
|
||||||
|
ChatDocumentHandler::AutoCompletionType m_autoCompletionType = ChatDocumentHandler::None;
|
||||||
|
|
||||||
|
void setAutoCompletionType(ChatDocumentHandler::AutoCompletionType autoCompletionType);
|
||||||
|
|
||||||
|
UserListModel *m_userListModel;
|
||||||
|
RoomListModel *m_roomListModel;
|
||||||
|
};
|
||||||
46
src/completionproxymodel.cpp
Normal file
46
src/completionproxymodel.cpp
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "completionproxymodel.h"
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
#include "neochatroom.h"
|
||||||
|
|
||||||
|
bool CompletionProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(sourceParent);
|
||||||
|
if (m_filterText.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (sourceModel()->data(sourceModel()->index(sourceRow, 0), filterRole()).toString().startsWith(m_filterText)
|
||||||
|
&& !m_fullText.startsWith(sourceModel()->data(sourceModel()->index(sourceRow, 0), filterRole()).toString()))
|
||||||
|
|| (m_secondaryFilterRole != -1
|
||||||
|
&& sourceModel()->data(sourceModel()->index(sourceRow, 0), secondaryFilterRole()).toString().startsWith(m_filterText.midRef(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
int CompletionProxyModel::secondaryFilterRole() const
|
||||||
|
{
|
||||||
|
return m_secondaryFilterRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompletionProxyModel::setSecondaryFilterRole(int role)
|
||||||
|
{
|
||||||
|
m_secondaryFilterRole = role;
|
||||||
|
Q_EMIT secondaryFilterRoleChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString CompletionProxyModel::filterText() const
|
||||||
|
{
|
||||||
|
return m_filterText;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompletionProxyModel::setFilterText(const QString &filterText)
|
||||||
|
{
|
||||||
|
m_filterText = filterText;
|
||||||
|
Q_EMIT filterTextChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompletionProxyModel::setFullText(const QString &fullText)
|
||||||
|
{
|
||||||
|
m_fullText = fullText;
|
||||||
|
}
|
||||||
33
src/completionproxymodel.h
Normal file
33
src/completionproxymodel.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
|
||||||
|
class CompletionProxyModel : public QSortFilterProxyModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(int secondaryFilterRole READ secondaryFilterRole WRITE setSecondaryFilterRole NOTIFY secondaryFilterRoleChanged)
|
||||||
|
Q_PROPERTY(QString filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
||||||
|
|
||||||
|
int secondaryFilterRole() const;
|
||||||
|
void setSecondaryFilterRole(int role);
|
||||||
|
|
||||||
|
QString filterText() const;
|
||||||
|
void setFilterText(const QString &filterText);
|
||||||
|
|
||||||
|
void setFullText(const QString &fullText);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void secondaryFilterRoleChanged();
|
||||||
|
void filterTextChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
int m_secondaryFilterRole = -1;
|
||||||
|
QString m_filterText;
|
||||||
|
QString m_fullText;
|
||||||
|
};
|
||||||
@@ -4,7 +4,9 @@
|
|||||||
#include <csapi/account-data.h>
|
#include <csapi/account-data.h>
|
||||||
#include <csapi/content-repo.h>
|
#include <csapi/content-repo.h>
|
||||||
|
|
||||||
#include "customemojimodel_p.h"
|
#include "controller.h"
|
||||||
|
#include "customemojimodel.h"
|
||||||
|
#include <connection.h>
|
||||||
|
|
||||||
#ifdef QUOTIENT_07
|
#ifdef QUOTIENT_07
|
||||||
#define running isJobPending
|
#define running isJobPending
|
||||||
@@ -12,35 +14,35 @@
|
|||||||
#define running isJobRunning
|
#define running isJobRunning
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void CustomEmojiModel::fetchEmojies()
|
void CustomEmojiModel::fetchEmojis()
|
||||||
{
|
{
|
||||||
if (d->conn == nullptr) {
|
if (!Controller::instance().activeConnection()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto &data = d->conn->accountData("im.ponies.user_emotes");
|
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes");
|
||||||
if (data == nullptr) {
|
if (data == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
QJsonObject emojies = data->contentJson()["images"].toObject();
|
QJsonObject emojis = data->contentJson()["images"].toObject();
|
||||||
|
|
||||||
// TODO: Remove with stable migration
|
// TODO: Remove with stable migration
|
||||||
const auto legacyEmojies = data->contentJson()["emoticons"].toObject();
|
const auto legacyEmojis = data->contentJson()["emoticons"].toObject();
|
||||||
for (const auto &emoji : legacyEmojies.keys()) {
|
for (const auto &emoji : legacyEmojis.keys()) {
|
||||||
if (!emojies.contains(emoji)) {
|
if (!emojis.contains(emoji)) {
|
||||||
emojies[emoji] = legacyEmojies[emoji];
|
emojis[emoji] = legacyEmojis[emoji];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
d->emojies.clear();
|
m_emojis.clear();
|
||||||
|
|
||||||
for (const auto &emoji : emojies.keys()) {
|
for (const auto &emoji : emojis.keys()) {
|
||||||
const auto &data = emojies[emoji];
|
const auto &data = emojis[emoji];
|
||||||
|
|
||||||
const auto e = emoji.startsWith(":") ? emoji : (QStringLiteral(":") + emoji + QStringLiteral(":"));
|
const auto e = emoji.startsWith(":") ? emoji : (QStringLiteral(":") + emoji + QStringLiteral(":"));
|
||||||
|
|
||||||
d->emojies << CustomEmoji{e, data.toObject()["url"].toString(), QRegularExpression(QStringLiteral(R"((^|[^\\]))") + e)};
|
m_emojis << CustomEmoji{e, data.toObject()["url"].toString(), QRegularExpression(QStringLiteral(R"((^|[^\\]))") + e)};
|
||||||
}
|
}
|
||||||
|
|
||||||
endResetModel();
|
endResetModel();
|
||||||
@@ -50,11 +52,11 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
|
|||||||
{
|
{
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
|
|
||||||
auto job = d->conn->uploadFile(location.toLocalFile());
|
auto job = Controller::instance().activeConnection()->uploadFile(location.toLocalFile());
|
||||||
|
|
||||||
if (running(job)) {
|
if (running(job)) {
|
||||||
connect(job, &BaseJob::success, this, [this, name, job] {
|
connect(job, &BaseJob::success, this, [this, name, job] {
|
||||||
const auto &data = d->conn->accountData("im.ponies.user_emotes");
|
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes");
|
||||||
auto json = data != nullptr ? data->contentJson() : QJsonObject();
|
auto json = data != nullptr ? data->contentJson() : QJsonObject();
|
||||||
auto emojiData = json["images"].toObject();
|
auto emojiData = json["images"].toObject();
|
||||||
emojiData[QStringLiteral("%1").arg(name)] = QJsonObject({
|
emojiData[QStringLiteral("%1").arg(name)] = QJsonObject({
|
||||||
@@ -65,7 +67,7 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
|
|||||||
#endif
|
#endif
|
||||||
});
|
});
|
||||||
json["images"] = emojiData;
|
json["images"] = emojiData;
|
||||||
d->conn->setAccountData("im.ponies.user_emotes", json);
|
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes", json);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,8 +76,8 @@ void CustomEmojiModel::removeEmoji(const QString &name)
|
|||||||
{
|
{
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
|
|
||||||
const auto &data = d->conn->accountData("im.ponies.user_emotes");
|
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes");
|
||||||
Q_ASSERT(data != nullptr); // something's screwed if we get here with a nullptr
|
Q_ASSERT(data);
|
||||||
auto json = data->contentJson();
|
auto json = data->contentJson();
|
||||||
const QString _name = name.mid(1).chopped(1);
|
const QString _name = name.mid(1).chopped(1);
|
||||||
auto emojiData = json["images"].toObject();
|
auto emojiData = json["images"].toObject();
|
||||||
@@ -97,5 +99,5 @@ void CustomEmojiModel::removeEmoji(const QString &name)
|
|||||||
emojiData.remove(_name);
|
emojiData.remove(_name);
|
||||||
json["emoticons"] = emojiData;
|
json["emoticons"] = emojiData;
|
||||||
}
|
}
|
||||||
d->conn->setAccountData("im.ponies.user_emotes", json);
|
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes", json);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,39 @@
|
|||||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include "customemojimodel_p.h"
|
#include "customemojimodel.h"
|
||||||
|
#include "controller.h"
|
||||||
#include "emojimodel.h"
|
#include "emojimodel.h"
|
||||||
|
|
||||||
#include <connection.h>
|
#include <connection.h>
|
||||||
|
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
|
|
||||||
enum Roles {
|
|
||||||
Name,
|
|
||||||
ImageURL,
|
|
||||||
ModelData, // for emulating the regular emoji model's usage, otherwise the UI code would get too complicated
|
|
||||||
};
|
|
||||||
|
|
||||||
CustomEmojiModel::CustomEmojiModel(QObject *parent)
|
CustomEmojiModel::CustomEmojiModel(QObject *parent)
|
||||||
: QAbstractListModel(parent)
|
: QAbstractListModel(parent)
|
||||||
, d(new Private)
|
|
||||||
{
|
{
|
||||||
connect(this, &CustomEmojiModel::connectionChanged, this, &CustomEmojiModel::fetchEmojies);
|
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this]() {
|
||||||
connect(this, &CustomEmojiModel::connectionChanged, this, [this]() {
|
if (!Controller::instance().activeConnection()) {
|
||||||
if (!d->conn)
|
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
connect(d->conn, &Connection::accountDataChanged, this, [this](const QString &id) {
|
CustomEmojiModel::fetchEmojis();
|
||||||
|
disconnect(nullptr, &Connection::accountDataChanged, this, nullptr);
|
||||||
|
connect(Controller::instance().activeConnection(), &Connection::accountDataChanged, this, [this](const QString &id) {
|
||||||
if (id != QStringLiteral("im.ponies.user_emotes")) {
|
if (id != QStringLiteral("im.ponies.user_emotes")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetchEmojies();
|
fetchEmojis();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomEmojiModel::~CustomEmojiModel()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
|
QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
|
||||||
{
|
{
|
||||||
const auto row = idx.row();
|
const auto row = idx.row();
|
||||||
if (row >= d->emojies.length()) {
|
if (row >= m_emojis.length()) {
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
const auto &data = d->emojies[row];
|
const auto &data = m_emojis[row];
|
||||||
|
|
||||||
switch (Roles(role)) {
|
switch (Roles(role)) {
|
||||||
case Roles::ModelData:
|
case Roles::ModelData:
|
||||||
@@ -51,6 +42,8 @@ QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
|
|||||||
return data.name;
|
return data.name;
|
||||||
case Roles::ImageURL:
|
case Roles::ImageURL:
|
||||||
return QUrl(QStringLiteral("image://mxc/") + data.url.mid(6));
|
return QUrl(QStringLiteral("image://mxc/") + data.url.mid(6));
|
||||||
|
case Roles::MxcUrl:
|
||||||
|
return data.url.mid(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
return QVariant();
|
return QVariant();
|
||||||
@@ -60,7 +53,7 @@ int CustomEmojiModel::rowCount(const QModelIndex &parent) const
|
|||||||
{
|
{
|
||||||
Q_UNUSED(parent)
|
Q_UNUSED(parent)
|
||||||
|
|
||||||
return d->emojies.length();
|
return m_emojis.length();
|
||||||
}
|
}
|
||||||
|
|
||||||
QHash<int, QByteArray> CustomEmojiModel::roleNames() const
|
QHash<int, QByteArray> CustomEmojiModel::roleNames() const
|
||||||
@@ -69,41 +62,25 @@ QHash<int, QByteArray> CustomEmojiModel::roleNames() const
|
|||||||
{Name, "name"},
|
{Name, "name"},
|
||||||
{ImageURL, "imageURL"},
|
{ImageURL, "imageURL"},
|
||||||
{ModelData, "modelData"},
|
{ModelData, "modelData"},
|
||||||
|
{MxcUrl, "mxcUrl"},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Connection *CustomEmojiModel::connection() const
|
QString CustomEmojiModel::preprocessText(const QString &text)
|
||||||
{
|
{
|
||||||
return d->conn;
|
auto handledText = text;
|
||||||
}
|
for (const auto &emoji : std::as_const(m_emojis)) {
|
||||||
|
handledText.replace(
|
||||||
void CustomEmojiModel::setConnection(Connection *it)
|
|
||||||
{
|
|
||||||
if (d->conn == it) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (d->conn != nullptr) {
|
|
||||||
disconnect(d->conn, nullptr, this, nullptr);
|
|
||||||
}
|
|
||||||
d->conn = it;
|
|
||||||
Q_EMIT connectionChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString CustomEmojiModel::preprocessText(const QString &it)
|
|
||||||
{
|
|
||||||
auto cp = it;
|
|
||||||
for (const auto &emoji : std::as_const(d->emojies)) {
|
|
||||||
cp.replace(
|
|
||||||
emoji.regexp,
|
emoji.regexp,
|
||||||
QStringLiteral(R"(<img data-mx-emoticon="" src="%1" alt="%2" title="%2" height="32" vertical-align="middle" />)").arg(emoji.url, emoji.name));
|
QStringLiteral(R"(<img data-mx-emoticon="" src="%1" alt="%2" title="%2" height="32" vertical-align="middle" />)").arg(emoji.url, emoji.name));
|
||||||
}
|
}
|
||||||
return cp;
|
return handledText;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantList CustomEmojiModel::filterModel(const QString &filter)
|
QVariantList CustomEmojiModel::filterModel(const QString &filter)
|
||||||
{
|
{
|
||||||
QVariantList results;
|
QVariantList results;
|
||||||
for (const auto &emoji : std::as_const(d->emojies)) {
|
for (const auto &emoji : std::as_const(m_emojis)) {
|
||||||
if (results.length() >= 10)
|
if (results.length() >= 10)
|
||||||
break;
|
break;
|
||||||
if (!emoji.name.contains(filter, Qt::CaseInsensitive))
|
if (!emoji.name.contains(filter, Qt::CaseInsensitive))
|
||||||
|
|||||||
@@ -5,47 +5,46 @@
|
|||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
namespace Quotient
|
struct CustomEmoji {
|
||||||
{
|
QString name; // with :semicolons:
|
||||||
class Connection;
|
QString url; // mxc://
|
||||||
}
|
QRegularExpression regexp;
|
||||||
|
};
|
||||||
|
|
||||||
class CustomEmojiModel : public QAbstractListModel
|
class CustomEmojiModel : public QAbstractListModel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// constructors
|
enum Roles {
|
||||||
|
Name,
|
||||||
|
ImageURL,
|
||||||
|
ModelData, // for emulating the regular emoji model's usage, otherwise the UI code would get too complicated
|
||||||
|
MxcUrl,
|
||||||
|
};
|
||||||
|
Q_ENUM(Roles);
|
||||||
|
|
||||||
explicit CustomEmojiModel(QObject *parent = nullptr);
|
static CustomEmojiModel &instance()
|
||||||
~CustomEmojiModel();
|
{
|
||||||
|
static CustomEmojiModel _instance;
|
||||||
// model
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
// property setters
|
|
||||||
|
|
||||||
Quotient::Connection *connection() const;
|
|
||||||
void setConnection(Quotient::Connection *it);
|
|
||||||
Q_SIGNAL void connectionChanged();
|
|
||||||
|
|
||||||
// QML functions
|
|
||||||
|
|
||||||
Q_INVOKABLE QString preprocessText(const QString &it);
|
Q_INVOKABLE QString preprocessText(const QString &it);
|
||||||
Q_INVOKABLE QVariantList filterModel(const QString &filter);
|
Q_INVOKABLE QVariantList filterModel(const QString &filter);
|
||||||
Q_INVOKABLE void addEmoji(const QString &name, const QUrl &location);
|
Q_INVOKABLE void addEmoji(const QString &name, const QUrl &location);
|
||||||
Q_INVOKABLE void removeEmoji(const QString &name);
|
Q_INVOKABLE void removeEmoji(const QString &name);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Private;
|
explicit CustomEmojiModel(QObject *parent = nullptr);
|
||||||
std::unique_ptr<Private> d;
|
QList<CustomEmoji> m_emojis;
|
||||||
|
|
||||||
void fetchEmojies();
|
void fetchEmojis();
|
||||||
};
|
};
|
||||||
|
|||||||
11
src/main.cpp
11
src/main.cpp
@@ -39,11 +39,9 @@
|
|||||||
|
|
||||||
#include "actionshandler.h"
|
#include "actionshandler.h"
|
||||||
#include "blurhashimageprovider.h"
|
#include "blurhashimageprovider.h"
|
||||||
#include "chatboxhelper.h"
|
|
||||||
#include "chatdocumenthandler.h"
|
#include "chatdocumenthandler.h"
|
||||||
#include "clipboard.h"
|
#include "clipboard.h"
|
||||||
#include "collapsestateproxymodel.h"
|
#include "collapsestateproxymodel.h"
|
||||||
#include "commandmodel.h"
|
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "customemojimodel.h"
|
#include "customemojimodel.h"
|
||||||
#include "devicesmodel.h"
|
#include "devicesmodel.h"
|
||||||
@@ -76,6 +74,8 @@
|
|||||||
#ifdef HAVE_COLORSCHEME
|
#ifdef HAVE_COLORSCHEME
|
||||||
#include "colorschemer.h"
|
#include "colorschemer.h"
|
||||||
#endif
|
#endif
|
||||||
|
#include "completionmodel.h"
|
||||||
|
#include "neochatuser.h"
|
||||||
|
|
||||||
#ifdef HAVE_RUNNER
|
#ifdef HAVE_RUNNER
|
||||||
#include "runner.h"
|
#include "runner.h"
|
||||||
@@ -167,7 +167,6 @@ int main(int argc, char *argv[])
|
|||||||
FileTypeSingleton fileTypeSingleton;
|
FileTypeSingleton fileTypeSingleton;
|
||||||
|
|
||||||
Login *login = new Login();
|
Login *login = new Login();
|
||||||
ChatBoxHelper chatBoxHelper;
|
|
||||||
UrlHelper urlHelper;
|
UrlHelper urlHelper;
|
||||||
|
|
||||||
#ifdef HAVE_COLORSCHEME
|
#ifdef HAVE_COLORSCHEME
|
||||||
@@ -187,20 +186,18 @@ int main(int argc, char *argv[])
|
|||||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "LoginHelper", login);
|
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "LoginHelper", login);
|
||||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "UrlHelper", &urlHelper);
|
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "UrlHelper", &urlHelper);
|
||||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "EmojiModel", new EmojiModel(&app));
|
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "EmojiModel", new EmojiModel(&app));
|
||||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "CommandModel", new CommandModel(&app));
|
|
||||||
#ifdef QUOTIENT_07
|
#ifdef QUOTIENT_07
|
||||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Quotient::Accounts);
|
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Quotient::Accounts);
|
||||||
#else
|
#else
|
||||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Quotient::AccountRegistry::instance());
|
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Quotient::AccountRegistry::instance());
|
||||||
#endif
|
#endif
|
||||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "SpaceHierarchyCache", &SpaceHierarchyCache::instance());
|
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "SpaceHierarchyCache", &SpaceHierarchyCache::instance());
|
||||||
|
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "CustomEmojiModel", &CustomEmojiModel::instance());
|
||||||
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
|
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
|
||||||
qmlRegisterType<ChatBoxHelper>("org.kde.neochat", 1, 0, "ChatBoxHelper");
|
|
||||||
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
|
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
|
||||||
qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel");
|
qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel");
|
||||||
qmlRegisterType<KWebShortcutModel>("org.kde.neochat", 1, 0, "WebShortcutModel");
|
qmlRegisterType<KWebShortcutModel>("org.kde.neochat", 1, 0, "WebShortcutModel");
|
||||||
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
|
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
|
||||||
qmlRegisterType<CustomEmojiModel>("org.kde.neochat", 1, 0, "CustomEmojiModel");
|
|
||||||
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
|
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
|
||||||
qmlRegisterType<CollapseStateProxyModel>("org.kde.neochat", 1, 0, "CollapseStateProxyModel");
|
qmlRegisterType<CollapseStateProxyModel>("org.kde.neochat", 1, 0, "CollapseStateProxyModel");
|
||||||
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
|
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
|
||||||
@@ -210,10 +207,12 @@ int main(int argc, char *argv[])
|
|||||||
qmlRegisterType<SortFilterSpaceListModel>("org.kde.neochat", 1, 0, "SortFilterSpaceListModel");
|
qmlRegisterType<SortFilterSpaceListModel>("org.kde.neochat", 1, 0, "SortFilterSpaceListModel");
|
||||||
qmlRegisterType<DevicesModel>("org.kde.neochat", 1, 0, "DevicesModel");
|
qmlRegisterType<DevicesModel>("org.kde.neochat", 1, 0, "DevicesModel");
|
||||||
qmlRegisterType<LinkPreviewer>("org.kde.neochat", 1, 0, "LinkPreviewer");
|
qmlRegisterType<LinkPreviewer>("org.kde.neochat", 1, 0, "LinkPreviewer");
|
||||||
|
qmlRegisterType<CompletionModel>("org.kde.neochat", 1, 0, "CompletionModel");
|
||||||
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM");
|
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM");
|
||||||
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM");
|
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM");
|
||||||
qmlRegisterUncreatableType<NeoChatRoomType>("org.kde.neochat", 1, 0, "NeoChatRoomType", "ENUM");
|
qmlRegisterUncreatableType<NeoChatRoomType>("org.kde.neochat", 1, 0, "NeoChatRoomType", "ENUM");
|
||||||
qmlRegisterUncreatableType<UserType>("org.kde.neochat", 1, 0, "UserType", "ENUM");
|
qmlRegisterUncreatableType<UserType>("org.kde.neochat", 1, 0, "UserType", "ENUM");
|
||||||
|
qmlRegisterUncreatableType<NeoChatUser>("org.kde.neochat", 1, 0, "NeoChatUser", {});
|
||||||
|
|
||||||
qRegisterMetaType<User *>("User*");
|
qRegisterMetaType<User *>("User*");
|
||||||
qRegisterMetaType<User *>("const User*");
|
qRegisterMetaType<User *>("const User*");
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
|
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
|
|
||||||
#include <cmark.h>
|
|
||||||
|
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QMetaObject>
|
#include <QMetaObject>
|
||||||
#include <QMimeDatabase>
|
#include <QMimeDatabase>
|
||||||
@@ -642,24 +640,6 @@ void NeoChatRoom::removeLocalAlias(const QString &alias)
|
|||||||
setLocalAliases(a);
|
setLocalAliases(a);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString NeoChatRoom::markdownToHTML(const QString &markdown)
|
|
||||||
{
|
|
||||||
const auto str = markdown.toUtf8();
|
|
||||||
char *tmp_buf = cmark_markdown_to_html(str.constData(), str.size(), CMARK_OPT_HARDBREAKS);
|
|
||||||
|
|
||||||
const std::string html(tmp_buf);
|
|
||||||
|
|
||||||
free(tmp_buf);
|
|
||||||
|
|
||||||
auto result = QString::fromStdString(html).trimmed();
|
|
||||||
|
|
||||||
result.replace("<!-- raw HTML omitted -->", "");
|
|
||||||
result.replace("<p>", "");
|
|
||||||
result.replace("</p>", "");
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString msgTypeToString(MessageEventType msgType)
|
QString msgTypeToString(MessageEventType msgType)
|
||||||
{
|
{
|
||||||
switch (msgType) {
|
switch (msgType) {
|
||||||
@@ -684,11 +664,6 @@ QString msgTypeToString(MessageEventType msgType)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString NeoChatRoom::preprocessText(const QString &text)
|
|
||||||
{
|
|
||||||
return markdownToHTML(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
void NeoChatRoom::postMessage(const QString &rawText, const QString &text, MessageEventType type, const QString &replyEventId, const QString &relateToEventId)
|
void NeoChatRoom::postMessage(const QString &rawText, const QString &text, MessageEventType type, const QString &replyEventId, const QString &relateToEventId)
|
||||||
{
|
{
|
||||||
postHtmlMessage(rawText, text, type, replyEventId, relateToEventId);
|
postHtmlMessage(rawText, text, type, replyEventId, relateToEventId);
|
||||||
@@ -1094,7 +1069,99 @@ void NeoChatRoom::reportEvent(const QString &eventId, const QString &reason)
|
|||||||
auto job = connection()->callApi<ReportContentJob>(id(), eventId, -50, reason);
|
auto job = connection()->callApi<ReportContentJob>(id(), eventId, -50, reason);
|
||||||
connect(job, &BaseJob::finished, this, [this, job]() {
|
connect(job, &BaseJob::finished, this, [this, job]() {
|
||||||
if (job->error() == BaseJob::Success) {
|
if (job->error() == BaseJob::Success) {
|
||||||
Q_EMIT positiveMessage(i18n("Report sent successfully."));
|
Q_EMIT showMessage(Positive, i18n("Report sent successfully."));
|
||||||
|
Q_EMIT showMessage(MessageType::Positive, i18n("Report sent successfully."));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString NeoChatRoom::chatBoxText() const
|
||||||
|
{
|
||||||
|
return m_chatBoxText;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeoChatRoom::setChatBoxText(const QString &text)
|
||||||
|
{
|
||||||
|
m_chatBoxText = text;
|
||||||
|
Q_EMIT chatBoxTextChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString NeoChatRoom::chatBoxReplyId() const
|
||||||
|
{
|
||||||
|
return m_chatBoxReplyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeoChatRoom::setChatBoxReplyId(const QString &replyId)
|
||||||
|
{
|
||||||
|
m_chatBoxReplyId = replyId;
|
||||||
|
Q_EMIT chatBoxReplyIdChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString NeoChatRoom::chatBoxEditId() const
|
||||||
|
{
|
||||||
|
return m_chatBoxEditId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeoChatRoom::setChatBoxEditId(const QString &editId)
|
||||||
|
{
|
||||||
|
m_chatBoxEditId = editId;
|
||||||
|
Q_EMIT chatBoxEditIdChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
NeoChatUser *NeoChatRoom::chatBoxReplyUser() const
|
||||||
|
{
|
||||||
|
if (m_chatBoxReplyId.isEmpty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return static_cast<NeoChatUser *>(user((*findInTimeline(m_chatBoxReplyId))->senderId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString NeoChatRoom::chatBoxReplyMessage() const
|
||||||
|
{
|
||||||
|
if (m_chatBoxReplyId.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return eventToString(*static_cast<const RoomMessageEvent *>(&**findInTimeline(m_chatBoxReplyId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
NeoChatUser *NeoChatRoom::chatBoxEditUser() const
|
||||||
|
{
|
||||||
|
if (m_chatBoxEditId.isEmpty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return static_cast<NeoChatUser *>(user((*findInTimeline(m_chatBoxEditId))->senderId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString NeoChatRoom::chatBoxEditMessage() const
|
||||||
|
{
|
||||||
|
if (m_chatBoxEditId.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return eventToString(*static_cast<const RoomMessageEvent *>(&**findInTimeline(m_chatBoxEditId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString NeoChatRoom::chatBoxAttachmentPath() const
|
||||||
|
{
|
||||||
|
return m_chatBoxAttachmentPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeoChatRoom::setChatBoxAttachmentPath(const QString &attachmentPath)
|
||||||
|
{
|
||||||
|
m_chatBoxAttachmentPath = attachmentPath;
|
||||||
|
Q_EMIT chatBoxAttachmentPathChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<Mention> *NeoChatRoom::mentions()
|
||||||
|
{
|
||||||
|
return &m_mentions;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString NeoChatRoom::savedText() const
|
||||||
|
{
|
||||||
|
return m_savedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeoChatRoom::setSavedText(const QString &savedText)
|
||||||
|
{
|
||||||
|
m_savedText = savedText;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,9 +6,12 @@
|
|||||||
#include <room.h>
|
#include <room.h>
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <events/roomevent.h>
|
#include <QTextCursor>
|
||||||
|
|
||||||
#include <qcoro/task.h>
|
#include <qcoro/task.h>
|
||||||
|
|
||||||
|
class NeoChatUser;
|
||||||
|
|
||||||
class PushNotificationState : public QObject
|
class PushNotificationState : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -24,11 +27,20 @@ public:
|
|||||||
Q_ENUM(State);
|
Q_ENUM(State);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct Mention {
|
||||||
|
QTextCursor cursor;
|
||||||
|
QString text;
|
||||||
|
int start = 0;
|
||||||
|
int position = 0;
|
||||||
|
QString id;
|
||||||
|
};
|
||||||
|
|
||||||
class NeoChatRoom : public Quotient::Room
|
class NeoChatRoom : public Quotient::Room
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(QVariantList usersTyping READ getUsersTyping NOTIFY typingChanged)
|
Q_PROPERTY(QVariantList usersTyping READ getUsersTyping NOTIFY typingChanged)
|
||||||
Q_PROPERTY(QString cachedInput MEMBER m_cachedInput NOTIFY cachedInputChanged)
|
|
||||||
Q_PROPERTY(bool hasFileUploading READ hasFileUploading WRITE setHasFileUploading NOTIFY hasFileUploadingChanged)
|
Q_PROPERTY(bool hasFileUploading READ hasFileUploading WRITE setHasFileUploading NOTIFY hasFileUploadingChanged)
|
||||||
Q_PROPERTY(int fileUploadingProgress READ fileUploadingProgress NOTIFY fileUploadingProgressChanged)
|
Q_PROPERTY(int fileUploadingProgress READ fileUploadingProgress NOTIFY fileUploadingProgressChanged)
|
||||||
Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
|
Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
|
||||||
@@ -40,7 +52,24 @@ class NeoChatRoom : public Quotient::Room
|
|||||||
Q_PROPERTY(PushNotificationState::State pushNotificationState MEMBER m_currentPushNotificationState WRITE setPushNotificationState NOTIFY
|
Q_PROPERTY(PushNotificationState::State pushNotificationState MEMBER m_currentPushNotificationState WRITE setPushNotificationState NOTIFY
|
||||||
pushNotificationStateChanged)
|
pushNotificationStateChanged)
|
||||||
|
|
||||||
|
// Due to problems with QTextDocument, unlike the other properties here, chatBoxText is *not* used to store the text when switching rooms
|
||||||
|
Q_PROPERTY(QString chatBoxText READ chatBoxText WRITE setChatBoxText NOTIFY chatBoxTextChanged)
|
||||||
|
Q_PROPERTY(QString chatBoxReplyId READ chatBoxReplyId WRITE setChatBoxReplyId NOTIFY chatBoxReplyIdChanged)
|
||||||
|
Q_PROPERTY(QString chatBoxEditId READ chatBoxEditId WRITE setChatBoxEditId NOTIFY chatBoxEditIdChanged)
|
||||||
|
Q_PROPERTY(NeoChatUser *chatBoxReplyUser READ chatBoxReplyUser NOTIFY chatBoxReplyIdChanged)
|
||||||
|
Q_PROPERTY(QString chatBoxReplyMessage READ chatBoxReplyMessage NOTIFY chatBoxReplyIdChanged)
|
||||||
|
Q_PROPERTY(NeoChatUser *chatBoxEditUser READ chatBoxEditUser NOTIFY chatBoxEditIdChanged)
|
||||||
|
Q_PROPERTY(QString chatBoxEditMessage READ chatBoxEditMessage NOTIFY chatBoxEditIdChanged)
|
||||||
|
Q_PROPERTY(QString chatBoxAttachmentPath READ chatBoxAttachmentPath WRITE setChatBoxAttachmentPath NOTIFY chatBoxAttachmentPathChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
enum MessageType {
|
||||||
|
Positive,
|
||||||
|
Info,
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
Q_ENUM(MessageType);
|
||||||
|
|
||||||
explicit NeoChatRoom(Quotient::Connection *connection, QString roomId, Quotient::JoinState joinState = {});
|
explicit NeoChatRoom(Quotient::Connection *connection, QString roomId, Quotient::JoinState joinState = {});
|
||||||
|
|
||||||
[[nodiscard]] QVariantList getUsersTyping() const;
|
[[nodiscard]] QVariantList getUsersTyping() const;
|
||||||
@@ -137,6 +166,29 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void setPushNotificationState(PushNotificationState::State state);
|
Q_INVOKABLE void setPushNotificationState(PushNotificationState::State state);
|
||||||
|
|
||||||
|
QString chatBoxText() const;
|
||||||
|
void setChatBoxText(const QString &text);
|
||||||
|
|
||||||
|
QString chatBoxReplyId() const;
|
||||||
|
void setChatBoxReplyId(const QString &replyId);
|
||||||
|
|
||||||
|
NeoChatUser *chatBoxReplyUser() const;
|
||||||
|
QString chatBoxReplyMessage() const;
|
||||||
|
|
||||||
|
QString chatBoxEditId() const;
|
||||||
|
void setChatBoxEditId(const QString &editId);
|
||||||
|
|
||||||
|
NeoChatUser *chatBoxEditUser() const;
|
||||||
|
QString chatBoxEditMessage() const;
|
||||||
|
|
||||||
|
QString chatBoxAttachmentPath() const;
|
||||||
|
void setChatBoxAttachmentPath(const QString &attachmentPath);
|
||||||
|
|
||||||
|
QVector<Mention> *mentions();
|
||||||
|
|
||||||
|
QString savedText() const;
|
||||||
|
void setSavedText(const QString &savedText);
|
||||||
|
|
||||||
#ifndef QUOTIENT_07
|
#ifndef QUOTIENT_07
|
||||||
Q_INVOKABLE QString htmlSafeMemberName(const QString &userId) const
|
Q_INVOKABLE QString htmlSafeMemberName(const QString &userId) const
|
||||||
{
|
{
|
||||||
@@ -145,7 +197,6 @@ public:
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString m_cachedInput;
|
|
||||||
QSet<const Quotient::RoomEvent *> highlights;
|
QSet<const Quotient::RoomEvent *> highlights;
|
||||||
|
|
||||||
bool m_hasFileUploading = false;
|
bool m_hasFileUploading = false;
|
||||||
@@ -160,10 +211,16 @@ private:
|
|||||||
void onAddHistoricalTimelineEvents(rev_iter_t from) override;
|
void onAddHistoricalTimelineEvents(rev_iter_t from) override;
|
||||||
void onRedaction(const Quotient::RoomEvent &prevEvent, const Quotient::RoomEvent &after) override;
|
void onRedaction(const Quotient::RoomEvent &prevEvent, const Quotient::RoomEvent &after) override;
|
||||||
|
|
||||||
static QString markdownToHTML(const QString &markdown);
|
|
||||||
QCoro::Task<void> doDeleteMessagesByUser(const QString &user);
|
QCoro::Task<void> doDeleteMessagesByUser(const QString &user);
|
||||||
QCoro::Task<void> doUploadFile(QUrl url, QString body = QString());
|
QCoro::Task<void> doUploadFile(QUrl url, QString body = QString());
|
||||||
|
|
||||||
|
QString m_chatBoxText;
|
||||||
|
QString m_chatBoxReplyId;
|
||||||
|
QString m_chatBoxEditId;
|
||||||
|
QString m_chatBoxAttachmentPath;
|
||||||
|
QVector<Mention> m_mentions;
|
||||||
|
QString m_savedText;
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void countChanged();
|
void countChanged();
|
||||||
void updatePushNotificationState(QString type);
|
void updatePushNotificationState(QString type);
|
||||||
@@ -179,14 +236,17 @@ Q_SIGNALS:
|
|||||||
void isInviteChanged();
|
void isInviteChanged();
|
||||||
void displayNameChanged();
|
void displayNameChanged();
|
||||||
void pushNotificationStateChanged(PushNotificationState::State state);
|
void pushNotificationStateChanged(PushNotificationState::State state);
|
||||||
void positiveMessage(const QString &message);
|
void showMessage(MessageType messageType, const QString &message);
|
||||||
|
void chatBoxTextChanged();
|
||||||
|
void chatBoxReplyIdChanged();
|
||||||
|
void chatBoxEditIdChanged();
|
||||||
|
void chatBoxAttachmentPathChanged();
|
||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
void uploadFile(const QUrl &url, const QString &body = QString());
|
void uploadFile(const QUrl &url, const QString &body = QString());
|
||||||
void acceptInvitation();
|
void acceptInvitation();
|
||||||
void forget();
|
void forget();
|
||||||
void sendTypingNotification(bool isTyping);
|
void sendTypingNotification(bool isTyping);
|
||||||
QString preprocessText(const QString &text);
|
|
||||||
|
|
||||||
/// @param rawText The text as it was typed.
|
/// @param rawText The text as it was typed.
|
||||||
/// @param cleanedText The text with link to the users.
|
/// @param cleanedText The text with link to the users.
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include <jobs/basejob.h>
|
#include <jobs/basejob.h>
|
||||||
#include <user.h>
|
#include <user.h>
|
||||||
|
|
||||||
|
#include "actionshandler.h"
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "neochatconfig.h"
|
#include "neochatconfig.h"
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
@@ -73,7 +74,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
|
|||||||
std::unique_ptr<KNotificationReplyAction> replyAction(new KNotificationReplyAction(i18n("Reply")));
|
std::unique_ptr<KNotificationReplyAction> replyAction(new KNotificationReplyAction(i18n("Reply")));
|
||||||
replyAction->setPlaceholderText(i18n("Reply..."));
|
replyAction->setPlaceholderText(i18n("Reply..."));
|
||||||
connect(replyAction.get(), &KNotificationReplyAction::replied, this, [room, replyEventId](const QString &text) {
|
connect(replyAction.get(), &KNotificationReplyAction::replied, this, [room, replyEventId](const QString &text) {
|
||||||
room->postMessage(text, room->preprocessText(text), RoomMessageEvent::MsgType::Text, replyEventId, QString());
|
room->postMessage(text, markdownToHTML(text), RoomMessageEvent::MsgType::Text, replyEventId, QString());
|
||||||
});
|
});
|
||||||
notification->setReplyAction(std::move(replyAction));
|
notification->setReplyAction(std::move(replyAction));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -337,6 +337,9 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
|||||||
if (role == AvatarRole) {
|
if (role == AvatarRole) {
|
||||||
return room->avatarMediaId();
|
return room->avatarMediaId();
|
||||||
}
|
}
|
||||||
|
if (role == CanonicalAliasRole) {
|
||||||
|
return room->canonicalAlias();
|
||||||
|
}
|
||||||
if (role == TopicRole) {
|
if (role == TopicRole) {
|
||||||
return room->topic();
|
return room->topic();
|
||||||
}
|
}
|
||||||
@@ -429,6 +432,7 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
|
|||||||
roles[NameRole] = "name";
|
roles[NameRole] = "name";
|
||||||
roles[DisplayNameRole] = "displayName";
|
roles[DisplayNameRole] = "displayName";
|
||||||
roles[AvatarRole] = "avatar";
|
roles[AvatarRole] = "avatar";
|
||||||
|
roles[CanonicalAliasRole] = "canonicalAlias";
|
||||||
roles[TopicRole] = "topic";
|
roles[TopicRole] = "topic";
|
||||||
roles[CategoryRole] = "category";
|
roles[CategoryRole] = "category";
|
||||||
roles[UnreadCountRole] = "unreadCount";
|
roles[UnreadCountRole] = "unreadCount";
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ public:
|
|||||||
NameRole = Qt::UserRole + 1,
|
NameRole = Qt::UserRole + 1,
|
||||||
DisplayNameRole,
|
DisplayNameRole,
|
||||||
AvatarRole,
|
AvatarRole,
|
||||||
|
CanonicalAliasRole,
|
||||||
TopicRole,
|
TopicRole,
|
||||||
CategoryRole,
|
CategoryRole,
|
||||||
UnreadCountRole,
|
UnreadCountRole,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <qt_connection_util.h>
|
#include <qt_connection_util.h>
|
||||||
|
#include <QQuickTextDocument>
|
||||||
#include <user.h>
|
#include <user.h>
|
||||||
|
|
||||||
#ifndef Q_OS_ANDROID
|
#ifndef Q_OS_ANDROID
|
||||||
@@ -123,6 +124,12 @@ void RoomManager::openRoomForActiveConnection()
|
|||||||
|
|
||||||
void RoomManager::enterRoom(NeoChatRoom *room)
|
void RoomManager::enterRoom(NeoChatRoom *room)
|
||||||
{
|
{
|
||||||
|
if (m_chatDocumentHandler) {
|
||||||
|
// We're doing these things here because it is critical that they are switched at the same time
|
||||||
|
m_currentRoom->setSavedText(m_chatDocumentHandler->document()->textDocument()->toPlainText());
|
||||||
|
m_chatDocumentHandler->setRoom(room);
|
||||||
|
m_chatDocumentHandler->document()->textDocument()->setPlainText(room->savedText());
|
||||||
|
}
|
||||||
m_lastCurrentRoom = std::exchange(m_currentRoom, room);
|
m_lastCurrentRoom = std::exchange(m_currentRoom, room);
|
||||||
Q_EMIT currentRoomChanged();
|
Q_EMIT currentRoomChanged();
|
||||||
|
|
||||||
@@ -232,3 +239,15 @@ void RoomManager::leaveRoom(NeoChatRoom *room)
|
|||||||
|
|
||||||
room->forget();
|
room->forget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChatDocumentHandler *RoomManager::chatDocumentHandler() const
|
||||||
|
{
|
||||||
|
return m_chatDocumentHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoomManager::setChatDocumentHandler(ChatDocumentHandler *handler)
|
||||||
|
{
|
||||||
|
m_chatDocumentHandler = handler;
|
||||||
|
m_chatDocumentHandler->setRoom(m_currentRoom);
|
||||||
|
Q_EMIT chatDocumentHandlerChanged();
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <uriresolver.h>
|
#include <uriresolver.h>
|
||||||
|
|
||||||
|
#include "chatdocumenthandler.h"
|
||||||
|
|
||||||
class NeoChatRoom;
|
class NeoChatRoom;
|
||||||
|
|
||||||
namespace Quotient
|
namespace Quotient
|
||||||
@@ -29,6 +31,7 @@ class RoomManager : public QObject, public UriResolverBase
|
|||||||
/// This property holds whether a room is currently open in NeoChat.
|
/// This property holds whether a room is currently open in NeoChat.
|
||||||
/// \sa room
|
/// \sa room
|
||||||
Q_PROPERTY(bool hasOpenRoom READ hasOpenRoom NOTIFY currentRoomChanged)
|
Q_PROPERTY(bool hasOpenRoom READ hasOpenRoom NOTIFY currentRoomChanged)
|
||||||
|
Q_PROPERTY(ChatDocumentHandler *chatDocumentHandler READ chatDocumentHandler WRITE setChatDocumentHandler NOTIFY chatDocumentHandlerChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit RoomManager(QObject *parent = nullptr);
|
explicit RoomManager(QObject *parent = nullptr);
|
||||||
@@ -64,6 +67,9 @@ public:
|
|||||||
/// Call this when the current used connection is dropped.
|
/// Call this when the current used connection is dropped.
|
||||||
Q_INVOKABLE void reset();
|
Q_INVOKABLE void reset();
|
||||||
|
|
||||||
|
ChatDocumentHandler *chatDocumentHandler() const;
|
||||||
|
void setChatDocumentHandler(ChatDocumentHandler *handler);
|
||||||
|
|
||||||
void setUrlArgument(const QString &arg);
|
void setUrlArgument(const QString &arg);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
@@ -98,6 +104,8 @@ Q_SIGNALS:
|
|||||||
/// Displays warning to the user.
|
/// Displays warning to the user.
|
||||||
void warning(const QString &title, const QString &message);
|
void warning(const QString &title, const QString &message);
|
||||||
|
|
||||||
|
void chatDocumentHandlerChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void openRoomForActiveConnection();
|
void openRoomForActiveConnection();
|
||||||
|
|
||||||
@@ -106,4 +114,5 @@ private:
|
|||||||
QString m_arg;
|
QString m_arg;
|
||||||
KConfig m_config;
|
KConfig m_config;
|
||||||
KConfigGroup m_lastRoomConfig;
|
KConfigGroup m_lastRoomConfig;
|
||||||
|
ChatDocumentHandler *m_chatDocumentHandler;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class UserListModel : public QAbstractListModel
|
|||||||
Q_PROPERTY(Quotient::Room *room READ room WRITE setRoom NOTIFY roomChanged)
|
Q_PROPERTY(Quotient::Room *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||||
public:
|
public:
|
||||||
enum EventRoles {
|
enum EventRoles {
|
||||||
NameRole = Qt::UserRole + 1,
|
NameRole = Qt::DisplayRole,
|
||||||
UserIDRole,
|
UserIDRole,
|
||||||
AvatarRole,
|
AvatarRole,
|
||||||
ObjectRole,
|
ObjectRole,
|
||||||
|
|||||||
Reference in New Issue
Block a user