ChatPage.qml 20 KB
Newer Older
1
/*
2
 *  Kaidan - A user-friendly XMPP client for every device!
3
 *
Linus Jahn's avatar
Linus Jahn committed
4
 *  Copyright (C) 2016-2020 Kaidan developers and contributors
Linus Jahn's avatar
Linus Jahn committed
5
 *  (see the LICENSE file for a full list of copyright authors)
6 7 8 9 10 11
 *
 *  Kaidan is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
Linus Jahn's avatar
Linus Jahn committed
12 13 14 15 16 17 18 19 20 21
 *  In addition, as a special exception, the author of Kaidan gives
 *  permission to link the code of its release with the OpenSSL
 *  project's "OpenSSL" library (or with modified versions of it that
 *  use the same license as the "OpenSSL" library), and distribute the
 *  linked executables. You must obey the GNU General Public License in
 *  all respects for all of the code used other than "OpenSSL". If you
 *  modify this file, you may extend this exception to your version of
 *  the file, but you are not obligated to do so.  If you do not wish to
 *  do so, delete this exception statement from your version.
 *
22 23 24 25 26 27
 *  Kaidan is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
Linus Jahn's avatar
Linus Jahn committed
28
 *  along with Kaidan.  If not, see <http://www.gnu.org/licenses/>.
29 30
 */

31 32 33 34 35
import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12 as Controls
import QtGraphicalEffects 1.12
import QtMultimedia 5.12 as Multimedia
36
import org.kde.kirigami 2.8 as Kirigami
Filipe Azevedo's avatar
Filipe Azevedo committed
37

38
import im.kaidan.kaidan 1.0
39
import EmojiModel 0.1
Filipe Azevedo's avatar
Filipe Azevedo committed
40 41
import MediaUtils 0.1

42
import "elements"
43

44
ChatPageBase {
Filipe Azevedo's avatar
Filipe Azevedo committed
45 46
	id: root

Melvin Keskin's avatar
Melvin Keskin committed
47 48 49 50 51 52
	property string chatName: {
		var currentChatJid = Kaidan.messageModel.currentChatJid
		var chatDisplayName = Kaidan.rosterModel.itemName(currentChatJid)
		return chatDisplayName ? chatDisplayName : currentChatJid
	}

Xavier's avatar
Xavier committed
53
	property bool isWritingSpoiler
Linus Jahn's avatar
Linus Jahn committed
54
	property string messageToCorrect
55

Filipe Azevedo's avatar
Filipe Azevedo committed
56 57
	readonly property bool cameraAvailable: Multimedia.QtMultimedia.availableCameras.length > 0

58
	title: chatName
59
	keyboardNavigationEnabled: true
60
	contextualActions: [
61
		// Action to toggle the message search bar
Xavier's avatar
Xavier committed
62
		Kirigami.Action {
63 64 65 66 67 68 69 70 71 72
			id: searchAction
			text: qsTr("Search")
			icon.name: "search"

			onTriggered: {
				if (searchBar.active)
					searchBar.close()
				else
					searchBar.open()
			}
73 74 75 76
		},
		Kirigami.Action {
			visible: true
			icon.name: {
77
				Kaidan.notificationsMuted(Kaidan.messageModel.currentChatJid)
78 79 80 81
					? "player-volume"
					: "audio-volume-muted-symbolic"
			}
			text: {
82
				Kaidan.notificationsMuted(Kaidan.messageModel.currentChatJid)
83 84 85 86
					? qsTr("Unmute notifications")
					: qsTr("Mute notifications")
			}
			onTriggered: {
87
				Kaidan.setNotificationsMuted(
88 89
					Kaidan.messageModel.currentChatJid,
					!Kaidan.notificationsMuted(Kaidan.messageModel.currentChatJid)
90 91 92 93
				)
			}

			function handleNotificationsMuted(jid) {
94
				text = Kaidan.notificationsMuted(Kaidan.messageModel.currentChatJid)
95 96
						? qsTr("Unmute notifications")
						: qsTr("Mute notifications")
97
				icon.name = Kaidan.notificationsMuted(Kaidan.messageModel.currentChatJid)
98 99 100 101 102
							? "player-volume"
							: "audio-volume-muted-symbolic"
			}

			Component.onCompleted: {
103
				Kaidan.notificationsMutedChanged.connect(handleNotificationsMuted)
104 105
			}
			Component.onDestruction: {
106
				Kaidan.notificationsMutedChanged.disconnect(handleNotificationsMuted)
107
			}
108 109 110 111 112
		},
		Kirigami.Action {
			visible: true
			icon.name: "user-identity"
			text: qsTr("View profile")
113
			onTriggered: pageStack.push(userProfilePage, {jid: Kaidan.messageModel.currentChatJid, name: chatName})
Filipe Azevedo's avatar
Filipe Azevedo committed
114 115 116 117 118 119 120 121 122 123 124 125
		},
		Kirigami.Action {
			readonly property int type: Enums.MessageType.MessageImage

			text: MediaUtilsInstance.newMediaLabel(type)
			enabled: root.cameraAvailable

			icon {
				name: MediaUtilsInstance.newMediaIconName(type)
			}

			onTriggered: {
126
				sendMediaSheet.sendNewMessageType(Kaidan.messageModel.currentChatJid, type)
Filipe Azevedo's avatar
Filipe Azevedo committed
127 128 129 130 131 132 133 134 135 136 137 138
			}
		},
		Kirigami.Action {
			readonly property int type: Enums.MessageType.MessageAudio

			text: MediaUtilsInstance.newMediaLabel(type)

			icon {
				name: MediaUtilsInstance.newMediaIconName(type)
			}

			onTriggered: {
139
				sendMediaSheet.sendNewMessageType(Kaidan.messageModel.currentChatJid, type)
Filipe Azevedo's avatar
Filipe Azevedo committed
140 141 142 143 144 145 146 147 148 149 150 151 152
			}
		},
		Kirigami.Action {
			readonly property int type: Enums.MessageType.MessageVideo

			text: MediaUtilsInstance.newMediaLabel(type)
			enabled: root.cameraAvailable

			icon {
				name: MediaUtilsInstance.newMediaIconName(type)
			}

			onTriggered: {
153
				sendMediaSheet.sendNewMessageType(Kaidan.messageModel.currentChatJid, type)
Filipe Azevedo's avatar
Filipe Azevedo committed
154 155 156 157 158 159 160 161 162 163 164 165
			}
		},
		Kirigami.Action {
			readonly property int type: Enums.MessageType.MessageGeoLocation

			text: MediaUtilsInstance.newMediaLabel(type)

			icon {
				name: MediaUtilsInstance.newMediaIconName(type)
			}

			onTriggered: {
166
				sendMediaSheet.sendNewMessageType(Kaidan.messageModel.currentChatJid, type)
Filipe Azevedo's avatar
Filipe Azevedo committed
167
			}
Blue's avatar
Blue committed
168 169
		},
		Kirigami.Action {
170 171 172 173
			visible: !isWritingSpoiler
			icon.name: "password-show-off"
			text: qsTr("Send a spoiler message")
			onTriggered: isWritingSpoiler = true
Xavier's avatar
Xavier committed
174 175
		}
	]
176

Blue's avatar
Blue committed
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
	// Message search bar
	header: Item {
		id: searchBar
		height: active ? searchField.height + 2 * Kirigami.Units.largeSpacing : 0
		clip: true
		visible: height != 0
		property bool active: false

		Behavior on height {
			SmoothedAnimation {
				velocity: 200
			}
		}

		// Background of the message search bar
		Rectangle {
			anchors.fill: parent
			color: Kirigami.Theme.backgroundColor
		}

		/**
		 * Searches for a message containing the entered text in the search field.
		 *
		 * If a message is found for the entered text, that message is highlighted.
		 * If the upwards search reaches the top of the message list view, the search is restarted at the bottom to search for messages which were not included in the search yet because they were below the message at the start index.
		 * That behavior is not applied to an upwards search starting from the index of the most recent message (0) to avoid searching twice.
		 * If the downwards search reaches the bottom of the message list view, the search is restarted at the top to search for messages which were not included in the search yet because they were above the message at the start index.
		 *
		 * @param searchUpwards true for searching upwards or false for searching downwards
		 * @param startIndex index index of the first message to search for the entered text
		 */
		function search(searchUpwards, startIndex) {
			let newIndex = -1
			if (searchBar.active && searchField.text.length > 0) {
				if (searchUpwards) {
					if (startIndex === 0) {
213
						newIndex = Kaidan.messageModel.searchForMessageFromNewToOld(searchField.text)
Blue's avatar
Blue committed
214
					} else {
215
						newIndex = Kaidan.messageModel.searchForMessageFromNewToOld(searchField.text, startIndex)
Blue's avatar
Blue committed
216
						if (newIndex === -1)
217
							newIndex = Kaidan.messageModel.searchForMessageFromNewToOld(searchField.text, 0)
Blue's avatar
Blue committed
218 219
					}
				} else {
220
					newIndex = Kaidan.messageModel.searchForMessageFromOldToNew(searchField.text, startIndex)
Blue's avatar
Blue committed
221
					if (newIndex === -1)
222
						newIndex = Kaidan.messageModel.searchForMessageFromOldToNew(searchField.text)
Blue's avatar
Blue committed
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
				}
			}
			messageListView.currentIndex = newIndex
		}

		/**
		 * Hides the search bar and resets the last search result.
		 */
		function close() {
			messageListView.currentIndex = -1
			searchBar.active = false
		}

		/**
		 * Shows the search bar and focuses the search field.
		 */
		function open() {
			searchField.forceActiveFocus()
			searchBar.active = true
		}

		/**
		 * Searches upwards for a message containing the entered text in the search field starting from the current index of the message list view.
		 */
		function searchUpwardsFromBottom() {
			searchBar.search(true, 0)
		}

		/**
		 * Searches upwards for a message containing the entered text in the search field starting from the current index of the message list view.
		 */
		function searchUpwardsFromCurrentIndex() {
			searchBar.search(true, messageListView.currentIndex + 1)
		}

		/**
		 * Searches downwards for a message containing the entered text in the search field starting from the current index of the message list view.
		 */
		function searchDownwardsFromCurrentIndex() {
			searchBar.search(false, messageListView.currentIndex - 1)
		}

		// Search field and ist corresponding buttons
		RowLayout {
			// Anchoring like this binds it to the top of the chat page.
			// It makes it look like the search bar slides down from behind of the upper element.
			anchors.left: parent.left
			anchors.right: parent.right
			anchors.bottom: parent.bottom
			anchors.margins: Kirigami.Units.largeSpacing

			Controls.Button {
				text: qsTr("Close")
				icon.name: "dialog-close"
				onClicked: searchBar.close()
				display: Controls.Button.IconOnly
				flat: true
			}

			Kirigami.SearchField {
				id: searchField
				Layout.fillWidth: true
				focusSequence: ""
				onVisibleChanged: text = ""

				onTextChanged: searchBar.searchUpwardsFromBottom()
				onAccepted: searchBar.searchUpwardsFromCurrentIndex()
				Keys.onUpPressed: searchBar.searchUpwardsFromCurrentIndex()
				Keys.onDownPressed: searchBar.searchDownwardsFromCurrentIndex()
				Keys.onEscapePressed: searchBar.close()
			}

			Controls.Button {
				text: qsTr("Search up")
				icon.name: "go-up"
				display: Controls.Button.IconOnly
				flat: true
				onClicked: {
					searchBar.searchUpwardsFromCurrentIndex()
					searchField.forceActiveFocus()
				}
			}

			Controls.Button {
				text: qsTr("Search down")
				icon.name: "go-down"
				display: Controls.Button.IconOnly
				flat: true
				onClicked: {
					searchBar.searchDownwardsFromCurrentIndex()
					searchField.forceActiveFocus()
				}
			}
		}
	}

319 320 321 322
	SendMediaSheet {
		id: sendMediaSheet
	}

323 324
	Loader {
		id: fileChooserLoader
325 326
	}

Filipe Azevedo's avatar
Filipe Azevedo committed
327
	function openFileDialog(filterName, filter, title) {
328 329
		fileChooserLoader.source = "qrc:/qml/elements/FileChooser.qml"
		fileChooserLoader.item.selectedNameFilter = filterName
330
		fileChooserLoader.item.accepted.connect(function() { sendMediaSheet.sendFile(Kaidan.messageModel.currentChatJid, fileChooserLoader.item.fileUrl) })
Filipe Azevedo's avatar
Filipe Azevedo committed
331
		if (title !== undefined)
332 333
			fileChooserLoader.item.title = title
		fileChooserLoader.item.open()
334
		mediaDrawer.close()
335 336 337 338
	}

	Kirigami.OverlayDrawer {
		id: mediaDrawer
Filipe Azevedo's avatar
Filipe Azevedo committed
339

340
		edge: Qt.BottomEdge
341
		height: Kirigami.Units.gridUnit * 8
Filipe Azevedo's avatar
Filipe Azevedo committed
342 343

		contentItem: ListView {
344
			id: content
Filipe Azevedo's avatar
Filipe Azevedo committed
345 346 347

			orientation: Qt.Horizontal

348
			Layout.fillHeight: true
Filipe Azevedo's avatar
Filipe Azevedo committed
349
			Layout.fillWidth: true
350

Filipe Azevedo's avatar
Filipe Azevedo committed
351 352 353 354 355 356 357
			model: [
				Enums.MessageType.MessageFile,
				Enums.MessageType.MessageImage,
				Enums.MessageType.MessageAudio,
				Enums.MessageType.MessageVideo,
				Enums.MessageType.MessageDocument
			]
358

359
			delegate: Controls.ToolButton {
Filipe Azevedo's avatar
Filipe Azevedo committed
360 361
				height: ListView.view.height
				width: height
362 363 364 365
				text: MediaUtilsInstance.label(model.modelData)
				icon {
					name: MediaUtilsInstance.iconName(model.modelData)
					height: Kirigami.Units.gridUnit * 5
366
					width: height
367 368
				}
				display: Controls.AbstractButton.TextUnderIcon
Filipe Azevedo's avatar
Filipe Azevedo committed
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387

				onClicked: {
					switch (model.modelData) {
					case Enums.MessageType.MessageFile:
					case Enums.MessageType.MessageImage:
					case Enums.MessageType.MessageAudio:
					case Enums.MessageType.MessageVideo:
					case Enums.MessageType.MessageDocument:
						openFileDialog(MediaUtilsInstance.filterName(model.modelData),
									   MediaUtilsInstance.filter(model.modelData),
									   MediaUtilsInstance.label(model.modelData))
						break
					case Enums.MessageType.MessageText:
					case Enums.MessageType.MessageGeoLocation:
					case Enums.MessageType.MessageUnknown:
						break
					}
				}
			}
388 389 390
		}
	}

Blue's avatar
Blue committed
391
	// View containing the messages
392
	ListView {
Blue's avatar
Blue committed
393
		id: messageListView
394
		verticalLayoutDirection: ListView.BottomToTop
395
		spacing: Kirigami.Units.smallSpacing * 1.5
396

Blue's avatar
Blue committed
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
		// Highlighting of the message containing a searched string.
		highlight: Component {
			id: highlightBar
			Rectangle {
				height: messageListView.currentIndex === -1 ? 0 : messageListView.currentItem.height + Kirigami.Units.smallSpacing * 2
				width: messageListView.currentIndex === -1 ? 0 : messageListView.currentItem.width + Kirigami.Units.smallSpacing * 2
				color: Kirigami.Theme.hoverColor

				// This is used to make the highlight bar a little bit bigger than the highlighted message.
				// It works only together with "messageListView.highlightFollowsCurrentItem: false".
				y: messageListView.currentIndex === -1 ? 0 : messageListView.currentItem.y - Kirigami.Units.smallSpacing
				x: messageListView.currentIndex === -1 ? 0 : messageListView.currentItem.x
				Behavior on y {
					SmoothedAnimation {
						velocity: 1000
						duration: 500
					}
				}

				Behavior on height {
					SmoothedAnimation {
						velocity: 1000
						duration: 500
					}
				}
			}
		}
		// This is used to make the highlight bar a little bit bigger than the highlighted message.
		highlightFollowsCurrentItem: false

		// Initially highlighted value
		currentIndex: -1

		// Connect to the database,
431
		model: Kaidan.messageModel
432

433 434
		Controls.Menu {
			id: contextMenu
Jonah Brüchert's avatar
Jonah Brüchert committed
435
			property ChatMessage message: null
436
			Controls.MenuItem {
437
				text: qsTr("Copy message")
Jonah Brüchert's avatar
Jonah Brüchert committed
438
				enabled: contextMenu.message && contextMenu.message.bodyLabel.visible
439
				onTriggered: {
Jonah Brüchert's avatar
Jonah Brüchert committed
440
					if (contextMenu.message && !contextMenu.message.isSpoiler || message && contextMenu.message.isShowingSpoiler)
441
						Utils.copyToClipboard(contextMenu.message && contextMenu.message.messageBody)
442
					else
443
						Utils.copyToClipboard(contextMenu.message && contextMenu.message.spoilerHint)
444 445 446 447
				}
			}

			Controls.MenuItem {
448
				text: qsTr("Edit message")
Jonah Brüchert's avatar
Jonah Brüchert committed
449 450
				enabled: Kaidan.messageModel.canCorrectMessage(contextMenu.message && contextMenu.message.msgId)
				onTriggered: contextMenu.message.messageEditRequested(contextMenu.message.msgId, contextMenu.message.messageBody)
451 452 453 454
			}

			Controls.MenuItem {
				text: qsTr("Copy download URL")
Jonah Brüchert's avatar
Jonah Brüchert committed
455
				enabled: contextMenu.message && contextMenu.message.mediaGetUrl
456 457 458 459
				onTriggered: Utils.copyToClipboard(contextMenu.message.mediaGetUrl)
			}

			Controls.MenuItem {
460
				text: qsTr("Quote message")
461
				onTriggered: {
Jonah Brüchert's avatar
Jonah Brüchert committed
462
					contextMenu.message.quoteRequested(contextMenu.message.messageBody)
463 464 465 466
				}
			}
		}

467
		delegate: ChatMessage {
468
			msgId: model.id
469 470
			senderJid: model.sender
			senderName: chatName
471 472
			sentByMe: model.sentByMe
			messageBody: model.body
473
			dateTime: new Date(model.timestamp)
474
			deliveryState: model.deliveryState
475
			mediaType: model.mediaType
476 477
			mediaGetUrl: model.mediaUrl
			mediaLocation: model.mediaLocation
478
			edited: model.isEdited
Xavier's avatar
Xavier committed
479 480 481
			isSpoiler: model.isSpoiler
			isShowingSpoiler: false
			spoilerHint: model.spoilerHint
482 483 484
			errorText: model.errorText
			deliveryStateName: model.deliveryStateName
			deliveryStateIcon: model.deliveryStateIcon
Linus Jahn's avatar
Linus Jahn committed
485

486 487
			menu: contextMenu

Linus Jahn's avatar
Linus Jahn committed
488 489 490 491 492
			onMessageEditRequested: {
				messageToCorrect = id
				messageField.text = body
				messageField.state = "edit"
			}
493 494 495 496 497 498 499 500 501 502
			onQuoteRequested: {
				var quotedText = ""
				var lines = body.split("\n")

				for (var line in lines) {
					quotedText += "> " + lines[line] + "\n"
				}

				messageField.insert(0, quotedText)
			}
503
		}
Ilya Bizyaev's avatar
Ilya Bizyaev committed
504
	}
505

506
	// area for writing and sending a message
Ilya Bizyaev's avatar
Ilya Bizyaev committed
507 508
	footer: Controls.Pane {
		id: sendingArea
509 510 511 512 513 514 515 516
		padding: 0
		wheelEnabled: true

		background: Rectangle {
			id: sendingAreaBackground
			color: Kirigami.Theme.backgroundColor
		}

Ilya Bizyaev's avatar
Ilya Bizyaev committed
517 518 519
		layer.enabled: sendingArea.enabled
		layer.effect: DropShadow {
			verticalOffset: 1
520
			color: Qt.darker(sendingAreaBackground.color, 1.2)
Ilya Bizyaev's avatar
Ilya Bizyaev committed
521 522
			samples: 20
			spread: 0.3
523
			cached: true // element is static
Ilya Bizyaev's avatar
Ilya Bizyaev committed
524
		}
525

Ilya Bizyaev's avatar
Ilya Bizyaev committed
526
		RowLayout {
527
			anchors.fill: parent
Xavier's avatar
Xavier committed
528
			Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Ilya Bizyaev's avatar
Ilya Bizyaev committed
529 530 531

			Controls.ToolButton {
				id: attachButton
532
				visible: Kaidan.serverFeaturesCache.httpUploadSupported
533 534
				Layout.preferredWidth: Kirigami.Units.gridUnit * 3
				Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Ilya Bizyaev's avatar
Ilya Bizyaev committed
535 536 537 538 539 540
				padding: 0
				Kirigami.Icon {
					source: "document-send-symbolic"
					isMask: true
					smooth: true
					anchors.centerIn: parent
541
					width: Kirigami.Units.gridUnit * 2
Ilya Bizyaev's avatar
Ilya Bizyaev committed
542 543
					height: width
				}
544 545 546 547
				onClicked: {
					if (Kirigami.Settings.isMobile)
						mediaDrawer.open()
					else
Filipe Azevedo's avatar
Filipe Azevedo committed
548
						openFileDialog(qsTr("All files"), "*", MediaUtilsInstance.label(Enums.MessageType.MessageFile))
549
				}
Linus Jahn's avatar
Linus Jahn committed
550
			}
551

Xavier's avatar
Xavier committed
552 553
			ColumnLayout {
				Layout.minimumHeight: messageField.height + Kirigami.Units.smallSpacing * 2
Ilya Bizyaev's avatar
Ilya Bizyaev committed
554
				Layout.fillWidth: true
Xavier's avatar
Xavier committed
555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580
				spacing: 0
				RowLayout {
					visible: isWritingSpoiler
					Controls.TextArea {
						id: spoilerHintField
						Layout.fillWidth: true
						placeholderText: qsTr("Spoiler hint")
						wrapMode: Controls.TextArea.Wrap
						selectByMouse: true
						background: Item {}
					}
					Controls.ToolButton {
						Layout.preferredWidth: Kirigami.Units.gridUnit * 1.5
						Layout.preferredHeight: Kirigami.Units.gridUnit * 1.5
						padding: 0
						Kirigami.Icon {
							source: "tab-close"
							smooth: true
							anchors.centerIn: parent
							width: Kirigami.Units.gridUnit * 1.5
							height: width
						}
						onClicked: {
							isWritingSpoiler = false
							spoilerHintField.text = ""
						}
581
					}
Xavier's avatar
Xavier committed
582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612
				}
				Kirigami.Separator {
					visible: isWritingSpoiler
					Layout.fillWidth: true
				}
				Controls.TextArea {
					id: messageField

					Layout.fillWidth: true
					Layout.alignment: Qt.AlignVCenter
					placeholderText: qsTr("Compose message")
					wrapMode: Controls.TextArea.Wrap
					selectByMouse: true
					background: Item {}
					state: "compose"
					states: [
						State {
							name: "compose"
						},
						State {
							name: "edit"
						}
					]
					Keys.onReturnPressed: {
						if (event.key === Qt.Key_Return) {
							if (event.modifiers & Qt.ControlModifier) {
								messageField.append("")
							} else {
								sendButton.onClicked()
								event.accepted = true
							}
Ilya Bizyaev's avatar
Ilya Bizyaev committed
613
						}
Ilya Bizyaev's avatar
Ilya Bizyaev committed
614
					}
615
				}
Ilya Bizyaev's avatar
Ilya Bizyaev committed
616
			}
617

618 619 620 621 622 623 624 625 626
			EmojiPicker {
				x: -width + parent.width
				y: -height - 16

				width: Kirigami.Units.gridUnit * 20
				height: Kirigami.Units.gridUnit * 15

				id: emojiPicker

627
				model: EmojiProxyModel {
628
					group: Emoji.Group.People
629
					sourceModel: EmojiModel {}
630 631 632 633 634 635 636 637 638 639 640
				}

				textArea: messageField
			}

			Controls.ToolButton {
				id: emojiPickerButton
				Layout.preferredWidth: Kirigami.Units.gridUnit * 3
				Layout.preferredHeight: Kirigami.Units.gridUnit * 3
				padding: 0
				Kirigami.Icon {
641
					source: "face-smile"
642
					enabled: sendButton.enabled
643
					isMask: false
644 645 646 647 648 649 650 651
					smooth: true
					anchors.centerIn: parent
					width: Kirigami.Units.gridUnit * 2
					height: width
				}
				onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.open()
			}

Ilya Bizyaev's avatar
Ilya Bizyaev committed
652 653
			Controls.ToolButton {
				id: sendButton
654 655
				Layout.preferredWidth: Kirigami.Units.gridUnit * 3
				Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Ilya Bizyaev's avatar
Ilya Bizyaev committed
656 657
				padding: 0
				Kirigami.Icon {
658 659 660 661 662 663
					source: {
						if (messageField.state == "compose")
							return "document-send"
						else if (messageField.state == "edit")
							return "edit-symbolic"
					}
Ilya Bizyaev's avatar
Ilya Bizyaev committed
664 665 666 667
					enabled: sendButton.enabled
					isMask: true
					smooth: true
					anchors.centerIn: parent
668
					width: Kirigami.Units.gridUnit * 2
Ilya Bizyaev's avatar
Ilya Bizyaev committed
669 670 671 672 673 674 675
					height: width
				}
				onClicked: {
					// don't send empty messages
					if (!messageField.text.length) {
						return
					}
676

Ilya Bizyaev's avatar
Ilya Bizyaev committed
677 678 679
					// disable the button to prevent sending
					// the same message several times
					sendButton.enabled = false
680

Ilya Bizyaev's avatar
Ilya Bizyaev committed
681
					// send the message
682
					if (messageField.state == "compose") {
683
						Kaidan.sendMessage(
684
							Kaidan.messageModel.currentChatJid,
685 686 687 688
							messageField.text,
							isWritingSpoiler,
							spoilerHintField.text
						)
689
					} else if (messageField.state == "edit") {
690
						Kaidan.correctMessage(
691
							Kaidan.messageModel.currentChatJid,
692 693 694
							messageToCorrect,
							messageField.text
						)
695 696
					}

Xavier's avatar
Xavier committed
697
					// clean up the text fields
Ilya Bizyaev's avatar
Ilya Bizyaev committed
698
					messageField.text = ""
699
					messageField.state = "compose"
Xavier's avatar
Xavier committed
700 701
					spoilerHintField.text = ""
					isWritingSpoiler = false
702
					messageToCorrect = ''
703

Ilya Bizyaev's avatar
Ilya Bizyaev committed
704 705
					// reenable the button
					sendButton.enabled = true
706 707 708 709
				}
			}
		}
	}
710 711 712 713 714 715 716

	Component.onCompleted: {
		// This makes it possible on desktop devices to directly enter a message after opening the chat page.
		// It is not used on mobile devices because the soft keyboard would otherwise always pop up after opening the chat page.
		if (!Kirigami.Settings.isMobile)
			messageField.forceActiveFocus()
	}
717
}