<NME> NewMessageForm.vue
<BEF> <!--
  - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
  -
  - @author Marco Ambrosini <marcoambrosini@icloud.com>
  -
  - @license GNU AGPL version 3 or any later version
  -
  - This program is free software: you can redistribute it and/or modify
  - it under the terms of the GNU Affero General Public License as
  - published by the Free Software Foundation, either version 3 of the
  - License, or (at your option) any later version.
  -
  - This program is distributed in the hope that it will be useful,
  - but WITHOUT ANY WARRANTY; without even the implied warranty of
  - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  - GNU Affero General Public License for more details.
  -
  - You should have received a copy of the GNU Affero General Public License
  - along with this program. If not, see <http://www.gnu.org/licenses/>.
-->

<template>
	<div class="wrapper"
		:class="{'wrapper--chatScrolledToBottom': isChatScrolledToBottom}">
		<!--native file picker, hidden -->
		<input id="file-upload"
			ref="fileUploadInput"
			multiple
			type="file"
			tabindex="-1"
			aria-hidden="true"
			class="hidden-visually"
			@change="handleFileInput">
		<div class="new-message">
			<form class="new-message-form"
				@submit.prevent>
				<!-- Attachments menu -->
				<div v-if="canUploadFiles || canShareFiles"
					class="new-message-form__upload-menu">
					<NcActions ref="attachmentsMenu"
						:container="container"
						:boundaries-element="containerElement"
						:disabled="disabled"
						:aria-label="t('spreed', 'Share files to the conversation')"
						:aria-haspopup="true">
						<template #icon>
							<Paperclip :size="16" />
						</template>

						<NcActionButton v-if="canUploadFiles"
							:close-after-click="true"
							@click.prevent="clickImportInput">
							<template #icon>
								<Upload :size="20" />
							</template>
							{{ t('spreed', 'Upload from device') }}
						</NcActionButton>
						<NcActionButton v-if="canShareFiles"
							:close-after-click="true"
							@click.prevent="handleFileShare">
							<template #icon>
								<Folder :size="20" />
							</template>
							{{ shareFromNextcloudLabel }}
						</NcActionButton>
						<template v-if="canShareFiles">
							<NcActionButton v-for="(provider, i) in fileTemplateOptions"
								:key="i"
								:close-after-click="true"
								:icon="provider.iconClass"
								@click.prevent="showTextFileDialog = i">
								{{ provider.label }}
							</NcActionButton>
						</template>
						<NcActionButton v-if="canCreatePoll"
							:close-after-click="true"
							@click.prevent="toggleSimplePollsEditor(true)">
							<template #icon>
								<Poll :size="20" />
							</template>
							{{ t('spreed', 'Create new poll') }}
						</NcActionButton>
					</NcActions>
				</div>
				<!-- Input area -->
				<div class="new-message-form__input">
					<div class="new-message-form__emoji-picker">
						<NcEmojiPicker v-if="!disabled"
							:container="container"
		createTemporaryMessage() {
			const message = Object.assign({}, {
				id: this.createTemporaryMessageId(),
				actorDisplayName: getCurrentUser().displayName,
				actorId: getCurrentUser().uid,
				actorType: 'users',
				message: this.text,
				token: this.token,
				timestamp: 0,
				systemMessage: '',
			})
			/**
			 * If the current message is a quote-reply messag, add the parent key to the
							:disabled="true">
							<template #icon>
								<EmoticonOutline :size="16" />
							</template>
						</NcButton>
					</div>
					<div v-if="messageToBeReplied" class="new-message-form__quote">
						<Quote :is-new-message-form-quote="true"
							:parent-id="messageToBeReplied.id"
							v-bind="messageToBeReplied" />
					</div>
					<AdvancedInput ref="advancedInput"
						v-model="text"
						:token="token"
						:active-input="!disabled"
						:placeholder-text="placeholderText"
						:aria-label="placeholderText"
						@update:contentEditable="contentEditableToParsed"
						@submit="handleSubmit({ silent: false })"
						@files-pasted="handlePastedFiles" />
				</div>

				<AudioRecorder v-if="!hasText && canUploadFiles"
					:disabled="disabled"
					@recording="handleRecording"
					@audio-file="handleAudioFile" />
				<!-- Send buttons -->
				<template v-else>
					<NcActions :force-menu="true">
						<!-- Silent send -->
						<NcActionButton :close-after-click="true"
							icon="icon-upload"
							:title="t('spreed', 'Send without notification')"
							@click.prevent="handleSubmit({ silent: true })">
							{{ silentSendInfo }}
							<BellOff slot="icon"
								:size="16" />
						</NcActionButton>
					</NcActions>
					<!-- Send -->
					<NcButton :disabled="disabled"
						type="tertiary"
						native-type="submit"
						:title="t('spreed', 'Send message')"
						:aria-label="t('spreed', 'Send message')"
						@click.prevent="handleSubmit({ silent: false })">
						<template #icon>
							<Send :size="16" />
						</template>
					</NcButton>
				</template>
			</form>
		</div>

		<SimplePollsEditor v-if="showSimplePollsEditor"
			:token="token"
			@close="toggleSimplePollsEditor(false)" />

		<!-- Text file creation dialog -->
		<NcModal v-if="showTextFileDialog !== false"
			size="normal"
			:container="$store.getters.getMainContainerSelector()"
			class="templates-picker"
			@close="dismissTextFileCreation">
			<div class="new-text-file">
				<h2>
					{{ t('spreed', 'Create and share a new file') }}
				</h2>
				<form class="new-text-file__form templates-picker__form"
					:style="style"
					@submit.prevent="handleCreateTextFile">
					<NcTextField id="new-file-form-name"
						ref="textFileTitleInput"
						:error="!!newFileError"
						:helper-text="newFileError"
						:label="t('spreed', 'Name of the new file')"
						:placeholder="textFileTitle"
						:value.sync="textFileTitle" />

					<template v-if="fileTemplate.templates.length">
						<ul class="templates-picker__list">
							<TemplatePreview v-bind="emptyTemplate"
								:checked="checked === emptyTemplate.fileid"
								@check="onCheck" />

							<TemplatePreview v-for="template in fileTemplate.templates"
								:key="template.fileid"
								v-bind="template"
								:checked="checked === template.fileid"
								:ratio="fileTemplate.ratio"
								@check="onCheck" />
						</ul>
					</template>

					<div class="new-text-file__buttons">
						<NcButton type="tertiary"
							@click="dismissTextFileCreation">
							{{ t('spreed', 'Close') }}
						</NcButton>
						<NcButton type="primary"
							@click="handleCreateTextFile">
							{{ t('spreed', 'Create file') }}
						</NcButton>
					</div>
				</form>
			</div>
		</NcModal>
	</div>
</template>

<script>
import AdvancedInput from './AdvancedInput/AdvancedInput.vue'
import { getFilePickerBuilder, showError } from '@nextcloud/dialogs'
import { getCapabilities } from '@nextcloud/capabilities'
import Quote from '../Quote.vue'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcEmojiPicker from '@nextcloud/vue/dist/Components/NcEmojiPicker.js'
import { EventBus } from '../../services/EventBus.js'
import { shareFile, createTextFile } from '../../services/filesSharingServices.js'
import { CONVERSATION, PARTICIPANT } from '../../constants.js'
import Paperclip from 'vue-material-design-icons/Paperclip.vue'
import EmoticonOutline from 'vue-material-design-icons/EmoticonOutline.vue'
import Send from 'vue-material-design-icons/Send.vue'
import BellOff from 'vue-material-design-icons/BellOff.vue'
import AudioRecorder from './AudioRecorder/AudioRecorder.vue'
import SimplePollsEditor from './SimplePollsEditor/SimplePollsEditor.vue'
import Poll from 'vue-material-design-icons/Poll.vue'
import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
import Folder from 'vue-material-design-icons/Folder.vue'
import Upload from 'vue-material-design-icons/Upload.vue'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import TemplatePreview from './TemplatePreview.vue'

const picker = getFilePickerBuilder(t('spreed', 'File to share'))
	.setMultiSelect(false)
	.setModal(true)
	.setType(1)
	.allowDirectories()
	.build()

const border = 2
const margin = 8
const width = margin * 20

export default {

	name: 'NewMessageForm',

	components: {
		AdvancedInput,
		Quote,
		NcActions,
		NcActionButton,
		NcButton,
		Paperclip,
		NcEmojiPicker,
		EmoticonOutline,
		Send,
		AudioRecorder,
		BellOff,
		SimplePollsEditor,
		Poll,
		NcModal,
		Folder,
		Upload,
		TemplatePreview,
		NcTextField,
	},

	props: {
		isChatScrolledToBottom: {
			type: Boolean,
			required: true,
		},
	},

	data() {
		return {
			text: '',
			parsedText: '',
			conversationIsFirstInList: false,
			// True when the audiorecorder component is recording
			isRecordingAudio: false,
			showSimplePollsEditor: false,
			showTextFileDialog: false,
			textFileTitle: t('spreed', 'New file'),
			newFileError: '',

			// Check empty template by default
			checked: -1,
		}
	},

	computed: {
		/**
		 * The current conversation token
		 *
		 * @return {string}
		 */
		token() {
			return this.$store.getters.getToken()
		},

		conversation() {
			return this.$store.getters.conversation(this.token) || {
				readOnly: CONVERSATION.STATE.READ_WRITE,
			}
		},

		isReadOnly() {
			return this.conversation.readOnly === CONVERSATION.STATE.READ_ONLY
		},

		noChatPermission() {
			return (this.conversation.permissions & PARTICIPANT.PERMISSIONS.CHAT) === 0
		},

		disabled() {
			return this.isReadOnly || this.noChatPermission || this.isReadOnly || !this.currentConversationIsJoined || this.isRecordingAudio
		},

		placeholderText() {
			if (this.isReadOnly) {
				return t('spreed', 'This conversation has been locked')
			} else if (this.noChatPermission) {
				return t('spreed', 'No permission to post messages in this conversation')
			} else if (!this.currentConversationIsJoined) {
				return t('spreed', 'Joining conversation …')
			} else {
				return t('spreed', 'Write message, @ to mention someone …')
			}
		},

		messageToBeReplied() {
			return this.$store.getters.getMessageToBeReplied(this.token)
		},

		currentUserIsGuest() {
			return this.$store.getters.getUserId() === null
		},

		canShareFiles() {
			return !this.currentUserIsGuest
		},

		canUploadFiles() {
			const allowed = getCapabilities()?.spreed?.config?.attachments?.allowed
			return allowed
				&& this.attachmentFolderFreeSpace !== 0
				&& this.canShareFiles
		},

		canCreatePoll() {
			return !this.isOneToOne && !this.noChatPermission
		},

		attachmentFolderFreeSpace() {
			return this.$store.getters.getAttachmentFolderFreeSpace()
		},

		currentConversationIsJoined() {
			return this.$store.getters.currentConversationIsJoined
		},

		hasText() {
			return this.parsedText !== ''
		},

		container() {
			return this.$store.getters.getMainContainerSelector()
		},

		containerElement() {
			return document.querySelector(this.container)
		},

		isOneToOne() {
			return this.conversation.type === CONVERSATION.TYPE.ONE_TO_ONE
		},

		silentSendInfo() {
			if (this.isOneToOne) {
				return t('spreed', 'The participant will not be notified about this message')
			} else {
				return t('spreed', 'The participants will not be notified about this message')
			}
		},

		shareFromNextcloudLabel() {
			return t('spreed', 'Share from {nextcloud}', { nextcloud: window.oc_defaults.productName })
		},

		fileTemplateOptions() {
			return this.$store.getters.getFileTemplates()
		},

		fileTemplate() {
			return this.fileTemplateOptions[this.showTextFileDialog]
		},

		emptyTemplate() {
			return {
				basename: t('files', 'Blank'),
				fileid: -1,
				filename: t('files', 'Blank'),
				hasPreview: false,
				mime: this.fileTemplate?.mimetypes[0] || this.fileTemplate?.mimetypes,
			}
		},

		selectedTemplate() {
			return this.fileTemplate.templates.find(template => template.fileid === this.checked)
		},

		style() {
			return {
				'--margin': margin + 'px',
				'--width': width + 'px',
				'--border': border + 'px',
				'--fullwidth': width + 2 * margin + 2 * border + 'px',
				'--height': this.fileTemplate.ratio ? Math.round(width / this.fileTemplate.ratio) + 'px' : null,
			}
		},
	},

	watch: {
		currentConversationIsJoined(newValue) {
			this.$refs.advancedInput.focusInput()
		},

		text(newValue) {
			this.$store.dispatch('setCurrentMessageInput', { token: this.token, text: newValue })
		},

		token(token) {
			if (token) {
				this.text = this.$store.getters.currentMessageInput(token) || ''
			} else {
				this.text = ''
			}
		},

		showTextFileDialog(newValue) {
			if (newValue !== false) {
				const fileTemplate = this.fileTemplateOptions[newValue]
				this.textFileTitle = fileTemplate.label + fileTemplate.extension
				this.$nextTick(() => {
					this.focusTextDialogInput()
				})
			}
		},
	},

	mounted() {
		EventBus.$on('upload-start', this.handleUploadStart)
		EventBus.$on('retry-message', this.handleRetryMessage)
		this.text = this.$store.getters.currentMessageInput(this.token) || ''
		// this.startRecording()

		if (!this.$store.getters.areFileTemplatesInitialised) {
			this.$store.dispatch('getFileTemplates')
		}
	},

	beforeDestroy() {
		EventBus.$off('upload-start', this.handleUploadStart)
		EventBus.$off('retry-message', this.handleRetryMessage)
	},

	methods: {
		handleUploadStart() {
			// refocus on upload start as the user might want to type again
			// while the upload is running
			this.$refs.advancedInput.focusInput()
		},

		contentEditableToParsed(contentEditable) {
			const mentions = contentEditable.querySelectorAll('span[data-at-embedded]')
			mentions.forEach(mention => {
				// FIXME Adding a space after the mention should be improved to
				// do it or not based on the next element instead of always
				// adding it.
				mention.replaceWith(' @' + mention.firstElementChild.attributes['data-mention-id'].value + ' ')
			})

			this.parsedText = this.rawToParsed(contentEditable.innerHTML)
		},
		/**
		 * Returns a parsed version of the given raw text of the content
		 * editable div.
		 *
		 * The given raw text contains a plain text representation of HTML
		 * content (like "first&nbsp;line<br>second&nbsp;line"). The returned
		 * parsed text replaces the (known) HTML content with the format
		 * expected by the server (like "first line\nsecond line").
		 *
		 * The parsed text is also trimmed.
		 *
		 * @param {string} text the raw text
		 * @return {string} the parsed text
		 */
		rawToParsed(text) {
			text = text.replace(/<br>/g, '\n')
			text = text.replace(/<div>/g, '\n')
			text = text.replace(/<\/div>/g, '')
			text = text.replace(/&nbsp;/g, ' ')

			// Since we used innerHTML to get the content of the div.contenteditable
			// it is escaped. With this little trick from https://stackoverflow.com/a/7394787
			// We unescape the code again, so if you write `<strong>` we can display
			// it again instead of `&lt;strong&gt;`
			const temp = document.createElement('textarea')
			temp.innerHTML = text
			text = temp.value

			// Although the text is fully trimmed, at the very least the last
			// "\n" occurrence should be always removed, as browsers add a
			// "<br>" element as soon as some rich text is written in a content
			// editable div (for example, if a new line is added the div content
			// will be "<br><br>").
			return text.trim()
		},

		/**
		 * Sends the new message
		 *
		 * @param {object} options the submit options
		 */
		async handleSubmit(options) {
			if (OC.debug && this.parsedText.startsWith('/spam ')) {
				const pattern = /^\/spam (\d+) messages$/i
				const match = pattern.exec(this.parsedText)
				// Escape HTML
				if (match) {
					await this.handleSubmitSpam(match[1])
					return
				}
			}

			if (this.parsedText !== '') {
				const temporaryMessage = await this.$store.dispatch('createTemporaryMessage', { text: this.parsedText, token: this.token })
				// FIXME: move "addTemporaryMessage" into "postNewMessage" as it's a pre-requisite anyway ?
				this.$store.dispatch('addTemporaryMessage', temporaryMessage)
				this.text = ''
				this.parsedText = ''
				// Scrolls the message list to the last added message
				EventBus.$emit('smooth-scroll-chat-to-bottom')
				// Also remove the message to be replied for this conversation
				this.$store.dispatch('removeMessageToBeReplied', this.token)
				await this.$store.dispatch('postNewMessage', { temporaryMessage, options })
			}
		},

		async handleSubmitSpam(numberOfMessages) {
			console.debug('Sending ' + numberOfMessages + ' lorem ipsum messages')
			for (let i = 0; i < numberOfMessages; i++) {
				const randomNumber = parseInt(Math.random() * 500, 10)
				console.debug('[' + i + '/' + numberOfMessages + '] Sleeping ' + randomNumber + 'ms')
				await this.sleep(randomNumber)

				const loremIpsum = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.\n\nDuis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.'
				this.parsedText = loremIpsum.slice(0, 25 + randomNumber)
				await this.handleSubmit()
			}
		},

		sleep(ms) {
			return new Promise(resolve => setTimeout(resolve, ms))
		},

		handleRetryMessage(temporaryMessageId) {
			if (this.parsedText === '') {
				const temporaryMessage = this.$store.getters.message(this.token, temporaryMessageId)
				if (temporaryMessage) {
					this.text = temporaryMessage.message || this.text
					this.parsedText = temporaryMessage.message || this.parsedText

					// Restore the parent/quote message
					if (temporaryMessage.parent) {
						const temporaryParent = this.$store.getters.message(this.token, temporaryMessage.parent)

						this.$store.dispatch('addMessageToBeReplied', {
							id: temporaryParent.id,
							actorId: temporaryParent.actorId,
							actorType: temporaryParent.actorType,
							actorDisplayName: temporaryParent.actorDisplayName,
							timestamp: temporaryParent.timestamp,
							systemMessage: temporaryParent.systemMessage,
							messageType: temporaryParent.messageType,
							message: temporaryParent.message,
							messageParameters: temporaryParent.messageParameters,
							token: temporaryParent.token,
							previousMessageId: temporaryParent.previousMessageId,
						})
					}

					this.$store.dispatch('removeTemporaryMessageFromStore', temporaryMessage)
				}
			}
		},

		async handleFileShare() {
			picker.pick()
				.then(async (path) => {
					console.debug(`path ${path} selected for sharing`)
					if (!path.startsWith('/')) {
						throw new Error(t('files', 'Invalid path selected'))
					}
					shareFile(path, this.token)
					this.$refs.advancedInput.focusInput()
				})

			// FIXME Remove this hack once it is possible to set the parent
			// element of the file picker.
			// By default the file picker is a sibling of the fullscreen
			// element, so it is not visible when in fullscreen mode. It is not
			// possible to specify the parent nor to know when the file picker
			// was actually opened, so for the time being it is reparented if
			// needed shortly after calling it.
			setTimeout(() => {
				if (this.$store.getters.isFullscreen()) {
					document.getElementById('content-vue').appendChild(document.querySelector('.oc-dialog'))
				}
			}, 1000)
		},

		/**
		 * Clicks the hidden file input when clicking the correspondent NcActionButton,
		 * thus opening the file-picker
		 */
		clickImportInput() {
			this.$refs.fileUploadInput.click()
		},

		handleFileInput(event) {
			const files = Object.values(event.target.files)

			this.handleFiles(files)

			// Clear input to ensure that the change event will be emitted if
			// the same file is picked again.
			event.target.value = ''
		},

		/**
		 * Handles files pasting event
		 *
		 * @param {File[] | FileList} files pasted files list
		 */
		async handlePastedFiles(files) {
			this.handleFiles(files, true)
		},

		/**
		 * Handles file upload
		 *
		 * @param {File[] | FileList} files pasted files list
		 * @param {boolean} rename whether to rename the files
		 * @param {boolean} isVoiceMessage indicates whether the file is a vooicemessage
		 */
		async handleFiles(files, rename = false, isVoiceMessage) {
			// Create a unique id for the upload operation
			const uploadId = new Date().getTime()
			// Uploads and shares the files
			await this.$store.dispatch('initialiseUpload', { files, token: this.token, uploadId, rename, isVoiceMessage })
		},

		/**
		 * Add selected emoji to text input area
		 *
		 * The emoji will be added at the current caret position, and any text
		 * currently selected will be replaced by the emoji. If the input area
		 * does not have the focus there will be no caret or selection; in that
		 * case the emoji will be added at the end.
		 *
		 * @param {string} emoji Emoji object
		 */
		addEmoji(emoji) {
			const selection = document.getSelection()

			const contentEditable = this.$refs.advancedInput.$refs.contentEditable

			// There is no select, or current selection does not start in the
			// content editable element, so just append the emoji at the end.
			if (!contentEditable.isSameNode(selection.anchorNode) && !contentEditable.contains(selection.anchorNode)) {
				// Browsers add a "<br>" element as soon as some rich text is
				// written in a content editable div (for example, if a new line
				// is added the div content will be "<br><br>"), so the emoji
				// has to be added before the last "<br>" (if any).
				if (this.text.endsWith('<br>')) {
					this.text = this.text.slice(0, this.text.lastIndexOf('<br>')) + emoji + '<br>'
				} else {
					this.text += emoji
				}

				return
			}

			// Although due to legacy reasons the API allows several ranges the
			// specification requires the selection to always have a single
			// range.
			// https://developer.mozilla.org/en-US/docs/Web/API/Selection#Multiple_ranges_in_a_selection
			const range = selection.getRangeAt(0)

			// Deleting the contents also collapses the range to the start.
			range.deleteContents()

			const emojiTextNode = document.createTextNode(emoji)
			range.insertNode(emojiTextNode)

			this.text = contentEditable.innerHTML

			range.setStartAfter(emojiTextNode)
		},

		handleAudioFile(payload) {
			this.handleFiles([payload], false, true)
		},

		handleRecording(payload) {
			this.isRecordingAudio = payload
		},

		toggleSimplePollsEditor(value) {
			this.showSimplePollsEditor = value
		},

		/**
		 * Manages the radio template picker change
		 *
		 * @param {number} fileid the selected template file id
		 */
		onCheck(fileid) {
			this.checked = fileid
		},

		// Create text file and share it to a conversation
		async handleCreateTextFile() {
			this.newFileError = ''
			let filePath = this.$store.getters.getAttachmentFolder() + '/' + this.textFileTitle.replace('/', '')

			if (!filePath.endsWith(this.fileTemplate.extension)) {
				filePath += this.fileTemplate.extension
			}

			let fileData
			try {
				const response = await createTextFile(
					filePath,
					this.selectedTemplate?.filename,
					this.selectedTemplate?.templateType,
				)
				fileData = response.data.ocs.data
			} catch (error) {
				console.error('Error while creating file', error)
				if (error?.response?.data?.ocs?.meta?.message) {
					showError(error.response.data.ocs.meta.message)
					this.newFileError = error.response.data.ocs.meta.message
				} else {
					showError(t('spreed', 'Error while creating file'))
				}
				return
			}

			await shareFile(filePath, this.token, '', '')

			// The Viewer expects a file to be set in the sidebar if the sidebar
			// is open.
			if (this.$store.getters.getSidebarStatus) {
				OCA.Files.Sidebar.state.file = filePath
			}

			OCA.Viewer.open({
				// Viewer expects an internal absolute path starting with "/".
				path: filePath,
				list: [
					fileData,
				],
			})

			// FIXME Remove this hack once it is possible to set the parent
			// element of the viewer.
			// By default the viewer is a sibling of the fullscreen element, so
			// it is not visible when in fullscreen mode. It is not possible to
			// specify the parent nor to know when the viewer was actually
			// opened, so for the time being it is reparented if needed shortly
			// after calling it.
			setTimeout(() => {
				if (this.$store.getters.isFullscreen()) {
					document.getElementById('content-vue').appendChild(document.getElementById('viewer'))
				}
			}, 1000)

			this.dismissTextFileCreation()
		},

		dismissTextFileCreation() {
			this.showTextFileDialog = false
			this.textFileTitle = t('spreed', 'New file')
			this.newFileError = ''
		},

		// Focus and select the text within the input field
		focusTextDialogInput() {
			// this.$refs.textFileTitleInput.$refs.inputField.$refs.input.focus()
			this.$refs.textFileTitleInput.$refs.inputField.$refs.input.select()

		},
	},
}
</script>

<style lang="scss" scoped>
@import '../../assets/variables';

.wrapper {
	display: flex;
	justify-content: center;
	padding: 12px 0;
	border-top: 1px solid var(--color-border);
	min-height: 69px;
	&--chatScrolledToBottom {
		border-top: none;
	}
}

.new-message {
	width: 100%;
	display: flex;
	justify-content: center;
	&-form {
		align-items: flex-end;
		display: flex;
		position:relative;
		flex: 0 1 700px;
		margin: 0 4px;
		&__emoji-picker {
			position: absolute;
			left: 5px;
			bottom: 1px;
			z-index: 1;
		}

		&__input {
			flex-grow: 1;
			overflow: hidden;
			position: relative;
			padding: 2px;
		}
		&__quote {
			margin: 0 16px 12px 24px;
			background-color: var(--color-background-hover);
			padding: 8px;
			border-radius: var(--border-radius-large);
		}

		// put a grey round background when popover is opened
		// or hover-focused
		&__icon:hover,
		&__icon:focus,
		&__icon:active {
			opacity: $opacity_full;
			// good looking on dark AND white bg
			background-color: $icon-focus-bg;
		}

	}
}

// Override actions styles TODO: upstream this change
// Targeting two classess for specificity
::v-deep .action-item__menutoggle.action-item__menutoggle--with-icon-slot {
	opacity: 1 !important;
	&:hover,
	&:focus {
		background-color: var(--color-background-hover) !important;
	}
	&:disabled {
		opacity: .5 !important;
	}
}

.new-text-file {
	display: flex;
	justify-content: center;
	align-items: center;
	flex-direction: column;
	gap: 28px;
	padding: 20px;

	&__buttons {
		display: flex;
		gap: 4px;
		justify-content: center;
		margin-top: 20px;
	}

	&__form {
		width: 100%;

		.templates-picker__list {
			margin-top: 20px;
			display: grid;
			grid-gap: calc(var(--margin) * 2);
			grid-auto-columns: 1fr;
			// We want maximum 5 columns. Putting 6 as we don't count the grid gap. So it will always be lower than 6
			max-width: calc(var(--fullwidth) * 6);
			grid-template-columns: repeat(auto-fit, var(--fullwidth));
			// Make sure all rows are the same height
			grid-auto-rows: 1fr;
			// Center the columns set
			justify-content: center;
		}
	}
}
</style>

<MSG> Fix parsing of temporary message

Signed-off-by: Joas Schilling <ab43a7c9cb5b2380afc4ddf8b3e2583169b39a02@schilljs.com>

<DFF> @@ -90,13 +90,15 @@ export default {
 		createTemporaryMessage() {
 			const message = Object.assign({}, {
 				id: this.createTemporaryMessageId(),
-				actorDisplayName: getCurrentUser().displayName,
 				actorId: getCurrentUser().uid,
 				actorType: 'users',
-				message: this.text,
-				token: this.token,
+				actorDisplayName: getCurrentUser().displayName,
 				timestamp: 0,
 				systemMessage: '',
+				messageType: '',
+				message: this.text,
+				messageParameters: {},
+				token: this.token,
 			})
 			/**
 			 * If the current message is a quote-reply messag, add the parent key to the

{'commit': "diff --git a/src/components/NewMessageForm/NewMessageForm.vue b/src/components/NewMessageForm/NewMessageForm.vue\nindex 82f87ee1a..75437b021 100644\n--- a/src/components/NewMessageForm/NewMessageForm.vue\n+++ b/src/components/NewMessageForm/NewMessageForm.vue\n@@ -90,13 +90,15 @@ export default {\n \t\tcreateTemporaryMessage() {\n \t\t\tconst message = Object.assign({}, {\n \t\t\t\tid: this.createTemporaryMessageId(),\n-\t\t\t\tactorDisplayName: getCurrentUser().displayName,\n \t\t\t\tactorId: getCurrentUser().uid,\n \t\t\t\tactorType: 'users',\n-\t\t\t\tmessage: this.text,\n-\t\t\t\ttoken: this.token,\n+\t\t\t\tactorDisplayName: getCurrentUser().displayName,\n \t\t\t\ttimestamp: 0,\n \t\t\t\tsystemMessage: '',\n+\t\t\t\tmessageType: '',\n+\t\t\t\tmessage: this.text,\n+\t\t\t\tmessageParameters: {},\n+\t\t\t\ttoken: this.token,\n \t\t\t})\n \t\t\t/**\n \t\t\t * If the current message is a quote-reply messag, add the parent key to the\n", 'commit_subject': 'Fix parsing of temporary message', 'repo_name': 'nextcloud/spreed', 'committer': {'name': 'Joas Schilling', 'email': 'ab43a7c9cb5b2380afc4ddf8b3e2583169b39a02@schilljs.com', 'time_sec': '1573033205', 'tz_offset': '60', 'date': {'seconds': '1573033205'}}, 'commit_tree': '47d631676156d4ea8fa2369bdb7392250e0350f5', 'commit_message': 'Fix parsing of temporary message\n\nSigned-off-by: Joas Schilling <ab43a7c9cb5b2380afc4ddf8b3e2583169b39a02@schilljs.com>\n', 'language_name': 'JavaScript', 'license': 'agpl-3.0', 'hunks': "@@ -90,13 +90,15 @@ export default {\n \t\tcreateTemporaryMessage() {\n \t\t\tconst message = Object.assign({}, {\n \t\t\t\tid: this.createTemporaryMessageId(),\n-\t\t\t\tactorDisplayName: getCurrentUser().displayName,\n \t\t\t\tactorId: getCurrentUser().uid,\n \t\t\t\tactorType: 'users',\n-\t\t\t\tmessage: this.text,\n-\t\t\t\ttoken: this.token,\n+\t\t\t\tactorDisplayName: getCurrentUser().displayName,\n \t\t\t\ttimestamp: 0,\n \t\t\t\tsystemMessage: '',\n+\t\t\t\tmessageType: '',\n+\t\t\t\tmessage: this.text,\n+\t\t\t\tmessageParameters: {},\n+\t\t\t\ttoken: this.token,\n \t\t\t})\n \t\t\t/**\n \t\t\t * If the current message is a quote-reply messag, add the parent key to the\n", 'addition_count': 5, 'deletion_count': 3, 'src_file': 'a/src/components/NewMessageForm/NewMessageForm.vue', 'tgt_file': 'b/src/components/NewMessageForm/NewMessageForm.vue', 'file_extension': '.vue', 'before_file': '<!--\n  - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>\n  -\n  - @author Marco Ambrosini <marcoambrosini@icloud.com>\n  -\n  - @license GNU AGPL version 3 or any later version\n  -\n  - This program is free software: you can redistribute it and/or modify\n  - it under the terms of the GNU Affero General Public License as\n  - published by the Free Software Foundation, either version 3 of the\n  - License, or (at your option) any later version.\n  -\n  - This program is distributed in the hope that it will be useful,\n  - but WITHOUT ANY WARRANTY; without even the implied warranty of\n  - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n  - GNU Affero General Public License for more details.\n  -\n  - You should have received a copy of the GNU Affero General Public License\n  - along with this program. If not, see <http://www.gnu.org/licenses/>.\n-->\n\n<template>\n\t<div class="wrapper"\n\t\t:class="{\'wrapper--chatScrolledToBottom\': isChatScrolledToBottom}">\n\t\t<!--native file picker, hidden -->\n\t\t<input id="file-upload"\n\t\t\tref="fileUploadInput"\n\t\t\tmultiple\n\t\t\ttype="file"\n\t\t\ttabindex="-1"\n\t\t\taria-hidden="true"\n\t\t\tclass="hidden-visually"\n\t\t\t@change="handleFileInput">\n\t\t<div class="new-message">\n\t\t\t<form class="new-message-form"\n\t\t\t\t@submit.prevent>\n\t\t\t\t<!-- Attachments menu -->\n\t\t\t\t<div v-if="canUploadFiles || canShareFiles"\n\t\t\t\t\tclass="new-message-form__upload-menu">\n\t\t\t\t\t<NcActions ref="attachmentsMenu"\n\t\t\t\t\t\t:container="container"\n\t\t\t\t\t\t:boundaries-element="containerElement"\n\t\t\t\t\t\t:disabled="disabled"\n\t\t\t\t\t\t:aria-label="t(\'spreed\', \'Share files to the conversation\')"\n\t\t\t\t\t\t:aria-haspopup="true">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Paperclip :size="16" />\n\t\t\t\t\t\t</template>\n\n\t\t\t\t\t\t<NcActionButton v-if="canUploadFiles"\n\t\t\t\t\t\t\t:close-after-click="true"\n\t\t\t\t\t\t\t@click.prevent="clickImportInput">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<Upload :size="20" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ t(\'spreed\', \'Upload from device\') }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t<NcActionButton v-if="canShareFiles"\n\t\t\t\t\t\t\t:close-after-click="true"\n\t\t\t\t\t\t\t@click.prevent="handleFileShare">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<Folder :size="20" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ shareFromNextcloudLabel }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t<template v-if="canShareFiles">\n\t\t\t\t\t\t\t<NcActionButton v-for="(provider, i) in fileTemplateOptions"\n\t\t\t\t\t\t\t\t:key="i"\n\t\t\t\t\t\t\t\t:close-after-click="true"\n\t\t\t\t\t\t\t\t:icon="provider.iconClass"\n\t\t\t\t\t\t\t\t@click.prevent="showTextFileDialog = i">\n\t\t\t\t\t\t\t\t{{ provider.label }}\n\t\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t<NcActionButton v-if="canCreatePoll"\n\t\t\t\t\t\t\t:close-after-click="true"\n\t\t\t\t\t\t\t@click.prevent="toggleSimplePollsEditor(true)">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<Poll :size="20" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ t(\'spreed\', \'Create new poll\') }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t</NcActions>\n\t\t\t\t</div>\n\t\t\t\t<!-- Input area -->\n\t\t\t\t<div class="new-message-form__input">\n\t\t\t\t\t<div class="new-message-form__emoji-picker">\n\t\t\t\t\t\t<NcEmojiPicker v-if="!disabled"\n\t\t\t\t\t\t\t:container="container"\n\t\tcreateTemporaryMessage() {\n\t\t\tconst message = Object.assign({}, {\n\t\t\t\tid: this.createTemporaryMessageId(),\n\t\t\t\tactorDisplayName: getCurrentUser().displayName,\n\t\t\t\tactorId: getCurrentUser().uid,\n\t\t\t\tactorType: \'users\',\n\t\t\t\tmessage: this.text,\n\t\t\t\ttoken: this.token,\n\t\t\t\ttimestamp: 0,\n\t\t\t\tsystemMessage: \'\',\n\t\t\t})\n\t\t\t/**\n\t\t\t * If the current message is a quote-reply messag, add the parent key to the\n\t\t\t\t\t\t\t:disabled="true">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<EmoticonOutline :size="16" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div v-if="messageToBeReplied" class="new-message-form__quote">\n\t\t\t\t\t\t<Quote :is-new-message-form-quote="true"\n\t\t\t\t\t\t\t:parent-id="messageToBeReplied.id"\n\t\t\t\t\t\t\tv-bind="messageToBeReplied" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<AdvancedInput ref="advancedInput"\n\t\t\t\t\t\tv-model="text"\n\t\t\t\t\t\t:token="token"\n\t\t\t\t\t\t:active-input="!disabled"\n\t\t\t\t\t\t:placeholder-text="placeholderText"\n\t\t\t\t\t\t:aria-label="placeholderText"\n\t\t\t\t\t\t@update:contentEditable="contentEditableToParsed"\n\t\t\t\t\t\t@submit="handleSubmit({ silent: false })"\n\t\t\t\t\t\t@files-pasted="handlePastedFiles" />\n\t\t\t\t</div>\n\n\t\t\t\t<AudioRecorder v-if="!hasText && canUploadFiles"\n\t\t\t\t\t:disabled="disabled"\n\t\t\t\t\t@recording="handleRecording"\n\t\t\t\t\t@audio-file="handleAudioFile" />\n\t\t\t\t<!-- Send buttons -->\n\t\t\t\t<template v-else>\n\t\t\t\t\t<NcActions :force-menu="true">\n\t\t\t\t\t\t<!-- Silent send -->\n\t\t\t\t\t\t<NcActionButton :close-after-click="true"\n\t\t\t\t\t\t\ticon="icon-upload"\n\t\t\t\t\t\t\t:title="t(\'spreed\', \'Send without notification\')"\n\t\t\t\t\t\t\t@click.prevent="handleSubmit({ silent: true })">\n\t\t\t\t\t\t\t{{ silentSendInfo }}\n\t\t\t\t\t\t\t<BellOff slot="icon"\n\t\t\t\t\t\t\t\t:size="16" />\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t</NcActions>\n\t\t\t\t\t<!-- Send -->\n\t\t\t\t\t<NcButton :disabled="disabled"\n\t\t\t\t\t\ttype="tertiary"\n\t\t\t\t\t\tnative-type="submit"\n\t\t\t\t\t\t:title="t(\'spreed\', \'Send message\')"\n\t\t\t\t\t\t:aria-label="t(\'spreed\', \'Send message\')"\n\t\t\t\t\t\t@click.prevent="handleSubmit({ silent: false })">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Send :size="16" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</NcButton>\n\t\t\t\t</template>\n\t\t\t</form>\n\t\t</div>\n\n\t\t<SimplePollsEditor v-if="showSimplePollsEditor"\n\t\t\t:token="token"\n\t\t\t@close="toggleSimplePollsEditor(false)" />\n\n\t\t<!-- Text file creation dialog -->\n\t\t<NcModal v-if="showTextFileDialog !== false"\n\t\t\tsize="normal"\n\t\t\t:container="$store.getters.getMainContainerSelector()"\n\t\t\tclass="templates-picker"\n\t\t\t@close="dismissTextFileCreation">\n\t\t\t<div class="new-text-file">\n\t\t\t\t<h2>\n\t\t\t\t\t{{ t(\'spreed\', \'Create and share a new file\') }}\n\t\t\t\t</h2>\n\t\t\t\t<form class="new-text-file__form templates-picker__form"\n\t\t\t\t\t:style="style"\n\t\t\t\t\t@submit.prevent="handleCreateTextFile">\n\t\t\t\t\t<NcTextField id="new-file-form-name"\n\t\t\t\t\t\tref="textFileTitleInput"\n\t\t\t\t\t\t:error="!!newFileError"\n\t\t\t\t\t\t:helper-text="newFileError"\n\t\t\t\t\t\t:label="t(\'spreed\', \'Name of the new file\')"\n\t\t\t\t\t\t:placeholder="textFileTitle"\n\t\t\t\t\t\t:value.sync="textFileTitle" />\n\n\t\t\t\t\t<template v-if="fileTemplate.templates.length">\n\t\t\t\t\t\t<ul class="templates-picker__list">\n\t\t\t\t\t\t\t<TemplatePreview v-bind="emptyTemplate"\n\t\t\t\t\t\t\t\t:checked="checked === emptyTemplate.fileid"\n\t\t\t\t\t\t\t\t@check="onCheck" />\n\n\t\t\t\t\t\t\t<TemplatePreview v-for="template in fileTemplate.templates"\n\t\t\t\t\t\t\t\t:key="template.fileid"\n\t\t\t\t\t\t\t\tv-bind="template"\n\t\t\t\t\t\t\t\t:checked="checked === template.fileid"\n\t\t\t\t\t\t\t\t:ratio="fileTemplate.ratio"\n\t\t\t\t\t\t\t\t@check="onCheck" />\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t</template>\n\n\t\t\t\t\t<div class="new-text-file__buttons">\n\t\t\t\t\t\t<NcButton type="tertiary"\n\t\t\t\t\t\t\t@click="dismissTextFileCreation">\n\t\t\t\t\t\t\t{{ t(\'spreed\', \'Close\') }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t<NcButton type="primary"\n\t\t\t\t\t\t\t@click="handleCreateTextFile">\n\t\t\t\t\t\t\t{{ t(\'spreed\', \'Create file\') }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</div>\n\t\t\t\t</form>\n\t\t\t</div>\n\t\t</NcModal>\n\t</div>\n</template>\n\n<script>\nimport AdvancedInput from \'./AdvancedInput/AdvancedInput.vue\'\nimport { getFilePickerBuilder, showError } from \'@nextcloud/dialogs\'\nimport { getCapabilities } from \'@nextcloud/capabilities\'\nimport Quote from \'../Quote.vue\'\nimport NcButton from \'@nextcloud/vue/dist/Components/NcButton.js\'\nimport NcActions from \'@nextcloud/vue/dist/Components/NcActions.js\'\nimport NcActionButton from \'@nextcloud/vue/dist/Components/NcActionButton.js\'\nimport NcEmojiPicker from \'@nextcloud/vue/dist/Components/NcEmojiPicker.js\'\nimport { EventBus } from \'../../services/EventBus.js\'\nimport { shareFile, createTextFile } from \'../../services/filesSharingServices.js\'\nimport { CONVERSATION, PARTICIPANT } from \'../../constants.js\'\nimport Paperclip from \'vue-material-design-icons/Paperclip.vue\'\nimport EmoticonOutline from \'vue-material-design-icons/EmoticonOutline.vue\'\nimport Send from \'vue-material-design-icons/Send.vue\'\nimport BellOff from \'vue-material-design-icons/BellOff.vue\'\nimport AudioRecorder from \'./AudioRecorder/AudioRecorder.vue\'\nimport SimplePollsEditor from \'./SimplePollsEditor/SimplePollsEditor.vue\'\nimport Poll from \'vue-material-design-icons/Poll.vue\'\nimport NcModal from \'@nextcloud/vue/dist/Components/NcModal.js\'\nimport Folder from \'vue-material-design-icons/Folder.vue\'\nimport Upload from \'vue-material-design-icons/Upload.vue\'\nimport NcTextField from \'@nextcloud/vue/dist/Components/NcTextField.js\'\nimport TemplatePreview from \'./TemplatePreview.vue\'\n\nconst picker = getFilePickerBuilder(t(\'spreed\', \'File to share\'))\n\t.setMultiSelect(false)\n\t.setModal(true)\n\t.setType(1)\n\t.allowDirectories()\n\t.build()\n\nconst border = 2\nconst margin = 8\nconst width = margin * 20\n\nexport default {\n\n\tname: \'NewMessageForm\',\n\n\tcomponents: {\n\t\tAdvancedInput,\n\t\tQuote,\n\t\tNcActions,\n\t\tNcActionButton,\n\t\tNcButton,\n\t\tPaperclip,\n\t\tNcEmojiPicker,\n\t\tEmoticonOutline,\n\t\tSend,\n\t\tAudioRecorder,\n\t\tBellOff,\n\t\tSimplePollsEditor,\n\t\tPoll,\n\t\tNcModal,\n\t\tFolder,\n\t\tUpload,\n\t\tTemplatePreview,\n\t\tNcTextField,\n\t},\n\n\tprops: {\n\t\tisChatScrolledToBottom: {\n\t\t\ttype: Boolean,\n\t\t\trequired: true,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\ttext: \'\',\n\t\t\tparsedText: \'\',\n\t\t\tconversationIsFirstInList: false,\n\t\t\t// True when the audiorecorder component is recording\n\t\t\tisRecordingAudio: false,\n\t\t\tshowSimplePollsEditor: false,\n\t\t\tshowTextFileDialog: false,\n\t\t\ttextFileTitle: t(\'spreed\', \'New file\'),\n\t\t\tnewFileError: \'\',\n\n\t\t\t// Check empty template by default\n\t\t\tchecked: -1,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The current conversation token\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\ttoken() {\n\t\t\treturn this.$store.getters.getToken()\n\t\t},\n\n\t\tconversation() {\n\t\t\treturn this.$store.getters.conversation(this.token) || {\n\t\t\t\treadOnly: CONVERSATION.STATE.READ_WRITE,\n\t\t\t}\n\t\t},\n\n\t\tisReadOnly() {\n\t\t\treturn this.conversation.readOnly === CONVERSATION.STATE.READ_ONLY\n\t\t},\n\n\t\tnoChatPermission() {\n\t\t\treturn (this.conversation.permissions & PARTICIPANT.PERMISSIONS.CHAT) === 0\n\t\t},\n\n\t\tdisabled() {\n\t\t\treturn this.isReadOnly || this.noChatPermission || this.isReadOnly || !this.currentConversationIsJoined || this.isRecordingAudio\n\t\t},\n\n\t\tplaceholderText() {\n\t\t\tif (this.isReadOnly) {\n\t\t\t\treturn t(\'spreed\', \'This conversation has been locked\')\n\t\t\t} else if (this.noChatPermission) {\n\t\t\t\treturn t(\'spreed\', \'No permission to post messages in this conversation\')\n\t\t\t} else if (!this.currentConversationIsJoined) {\n\t\t\t\treturn t(\'spreed\', \'Joining conversation …\')\n\t\t\t} else {\n\t\t\t\treturn t(\'spreed\', \'Write message, @ to mention someone …\')\n\t\t\t}\n\t\t},\n\n\t\tmessageToBeReplied() {\n\t\t\treturn this.$store.getters.getMessageToBeReplied(this.token)\n\t\t},\n\n\t\tcurrentUserIsGuest() {\n\t\t\treturn this.$store.getters.getUserId() === null\n\t\t},\n\n\t\tcanShareFiles() {\n\t\t\treturn !this.currentUserIsGuest\n\t\t},\n\n\t\tcanUploadFiles() {\n\t\t\tconst allowed = getCapabilities()?.spreed?.config?.attachments?.allowed\n\t\t\treturn allowed\n\t\t\t\t&& this.attachmentFolderFreeSpace !== 0\n\t\t\t\t&& this.canShareFiles\n\t\t},\n\n\t\tcanCreatePoll() {\n\t\t\treturn !this.isOneToOne && !this.noChatPermission\n\t\t},\n\n\t\tattachmentFolderFreeSpace() {\n\t\t\treturn this.$store.getters.getAttachmentFolderFreeSpace()\n\t\t},\n\n\t\tcurrentConversationIsJoined() {\n\t\t\treturn this.$store.getters.currentConversationIsJoined\n\t\t},\n\n\t\thasText() {\n\t\t\treturn this.parsedText !== \'\'\n\t\t},\n\n\t\tcontainer() {\n\t\t\treturn this.$store.getters.getMainContainerSelector()\n\t\t},\n\n\t\tcontainerElement() {\n\t\t\treturn document.querySelector(this.container)\n\t\t},\n\n\t\tisOneToOne() {\n\t\t\treturn this.conversation.type === CONVERSATION.TYPE.ONE_TO_ONE\n\t\t},\n\n\t\tsilentSendInfo() {\n\t\t\tif (this.isOneToOne) {\n\t\t\t\treturn t(\'spreed\', \'The participant will not be notified about this message\')\n\t\t\t} else {\n\t\t\t\treturn t(\'spreed\', \'The participants will not be notified about this message\')\n\t\t\t}\n\t\t},\n\n\t\tshareFromNextcloudLabel() {\n\t\t\treturn t(\'spreed\', \'Share from {nextcloud}\', { nextcloud: window.oc_defaults.productName })\n\t\t},\n\n\t\tfileTemplateOptions() {\n\t\t\treturn this.$store.getters.getFileTemplates()\n\t\t},\n\n\t\tfileTemplate() {\n\t\t\treturn this.fileTemplateOptions[this.showTextFileDialog]\n\t\t},\n\n\t\temptyTemplate() {\n\t\t\treturn {\n\t\t\t\tbasename: t(\'files\', \'Blank\'),\n\t\t\t\tfileid: -1,\n\t\t\t\tfilename: t(\'files\', \'Blank\'),\n\t\t\t\thasPreview: false,\n\t\t\t\tmime: this.fileTemplate?.mimetypes[0] || this.fileTemplate?.mimetypes,\n\t\t\t}\n\t\t},\n\n\t\tselectedTemplate() {\n\t\t\treturn this.fileTemplate.templates.find(template => template.fileid === this.checked)\n\t\t},\n\n\t\tstyle() {\n\t\t\treturn {\n\t\t\t\t\'--margin\': margin + \'px\',\n\t\t\t\t\'--width\': width + \'px\',\n\t\t\t\t\'--border\': border + \'px\',\n\t\t\t\t\'--fullwidth\': width + 2 * margin + 2 * border + \'px\',\n\t\t\t\t\'--height\': this.fileTemplate.ratio ? Math.round(width / this.fileTemplate.ratio) + \'px\' : null,\n\t\t\t}\n\t\t},\n\t},\n\n\twatch: {\n\t\tcurrentConversationIsJoined(newValue) {\n\t\t\tthis.$refs.advancedInput.focusInput()\n\t\t},\n\n\t\ttext(newValue) {\n\t\t\tthis.$store.dispatch(\'setCurrentMessageInput\', { token: this.token, text: newValue })\n\t\t},\n\n\t\ttoken(token) {\n\t\t\tif (token) {\n\t\t\t\tthis.text = this.$store.getters.currentMessageInput(token) || \'\'\n\t\t\t} else {\n\t\t\t\tthis.text = \'\'\n\t\t\t}\n\t\t},\n\n\t\tshowTextFileDialog(newValue) {\n\t\t\tif (newValue !== false) {\n\t\t\t\tconst fileTemplate = this.fileTemplateOptions[newValue]\n\t\t\t\tthis.textFileTitle = fileTemplate.label + fileTemplate.extension\n\t\t\t\tthis.$nextTick(() => {\n\t\t\t\t\tthis.focusTextDialogInput()\n\t\t\t\t})\n\t\t\t}\n\t\t},\n\t},\n\n\tmounted() {\n\t\tEventBus.$on(\'upload-start\', this.handleUploadStart)\n\t\tEventBus.$on(\'retry-message\', this.handleRetryMessage)\n\t\tthis.text = this.$store.getters.currentMessageInput(this.token) || \'\'\n\t\t// this.startRecording()\n\n\t\tif (!this.$store.getters.areFileTemplatesInitialised) {\n\t\t\tthis.$store.dispatch(\'getFileTemplates\')\n\t\t}\n\t},\n\n\tbeforeDestroy() {\n\t\tEventBus.$off(\'upload-start\', this.handleUploadStart)\n\t\tEventBus.$off(\'retry-message\', this.handleRetryMessage)\n\t},\n\n\tmethods: {\n\t\thandleUploadStart() {\n\t\t\t// refocus on upload start as the user might want to type again\n\t\t\t// while the upload is running\n\t\t\tthis.$refs.advancedInput.focusInput()\n\t\t},\n\n\t\tcontentEditableToParsed(contentEditable) {\n\t\t\tconst mentions = contentEditable.querySelectorAll(\'span[data-at-embedded]\')\n\t\t\tmentions.forEach(mention => {\n\t\t\t\t// FIXME Adding a space after the mention should be improved to\n\t\t\t\t// do it or not based on the next element instead of always\n\t\t\t\t// adding it.\n\t\t\t\tmention.replaceWith(\' @\' + mention.firstElementChild.attributes[\'data-mention-id\'].value + \' \')\n\t\t\t})\n\n\t\t\tthis.parsedText = this.rawToParsed(contentEditable.innerHTML)\n\t\t},\n\t\t/**\n\t\t * Returns a parsed version of the given raw text of the content\n\t\t * editable div.\n\t\t *\n\t\t * The given raw text contains a plain text representation of HTML\n\t\t * content (like "first&nbsp;line<br>second&nbsp;line"). The returned\n\t\t * parsed text replaces the (known) HTML content with the format\n\t\t * expected by the server (like "first line\\nsecond line").\n\t\t *\n\t\t * The parsed text is also trimmed.\n\t\t *\n\t\t * @param {string} text the raw text\n\t\t * @return {string} the parsed text\n\t\t */\n\t\trawToParsed(text) {\n\t\t\ttext = text.replace(/<br>/g, \'\\n\')\n\t\t\ttext = text.replace(/<div>/g, \'\\n\')\n\t\t\ttext = text.replace(/<\\/div>/g, \'\')\n\t\t\ttext = text.replace(/&nbsp;/g, \' \')\n\n\t\t\t// Since we used innerHTML to get the content of the div.contenteditable\n\t\t\t// it is escaped. With this little trick from https://stackoverflow.com/a/7394787\n\t\t\t// We unescape the code again, so if you write `<strong>` we can display\n\t\t\t// it again instead of `&lt;strong&gt;`\n\t\t\tconst temp = document.createElement(\'textarea\')\n\t\t\ttemp.innerHTML = text\n\t\t\ttext = temp.value\n\n\t\t\t// Although the text is fully trimmed, at the very least the last\n\t\t\t// "\\n" occurrence should be always removed, as browsers add a\n\t\t\t// "<br>" element as soon as some rich text is written in a content\n\t\t\t// editable div (for example, if a new line is added the div content\n\t\t\t// will be "<br><br>").\n\t\t\treturn text.trim()\n\t\t},\n\n\t\t/**\n\t\t * Sends the new message\n\t\t *\n\t\t * @param {object} options the submit options\n\t\t */\n\t\tasync handleSubmit(options) {\n\t\t\tif (OC.debug && this.parsedText.startsWith(\'/spam \')) {\n\t\t\t\tconst pattern = /^\\/spam (\\d+) messages$/i\n\t\t\t\tconst match = pattern.exec(this.parsedText)\n\t\t\t\t// Escape HTML\n\t\t\t\tif (match) {\n\t\t\t\t\tawait this.handleSubmitSpam(match[1])\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (this.parsedText !== \'\') {\n\t\t\t\tconst temporaryMessage = await this.$store.dispatch(\'createTemporaryMessage\', { text: this.parsedText, token: this.token })\n\t\t\t\t// FIXME: move "addTemporaryMessage" into "postNewMessage" as it\'s a pre-requisite anyway ?\n\t\t\t\tthis.$store.dispatch(\'addTemporaryMessage\', temporaryMessage)\n\t\t\t\tthis.text = \'\'\n\t\t\t\tthis.parsedText = \'\'\n\t\t\t\t// Scrolls the message list to the last added message\n\t\t\t\tEventBus.$emit(\'smooth-scroll-chat-to-bottom\')\n\t\t\t\t// Also remove the message to be replied for this conversation\n\t\t\t\tthis.$store.dispatch(\'removeMessageToBeReplied\', this.token)\n\t\t\t\tawait this.$store.dispatch(\'postNewMessage\', { temporaryMessage, options })\n\t\t\t}\n\t\t},\n\n\t\tasync handleSubmitSpam(numberOfMessages) {\n\t\t\tconsole.debug(\'Sending \' + numberOfMessages + \' lorem ipsum messages\')\n\t\t\tfor (let i = 0; i < numberOfMessages; i++) {\n\t\t\t\tconst randomNumber = parseInt(Math.random() * 500, 10)\n\t\t\t\tconsole.debug(\'[\' + i + \'/\' + numberOfMessages + \'] Sleeping \' + randomNumber + \'ms\')\n\t\t\t\tawait this.sleep(randomNumber)\n\n\t\t\t\tconst loremIpsum = \'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.\\n\\nDuis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.\'\n\t\t\t\tthis.parsedText = loremIpsum.slice(0, 25 + randomNumber)\n\t\t\t\tawait this.handleSubmit()\n\t\t\t}\n\t\t},\n\n\t\tsleep(ms) {\n\t\t\treturn new Promise(resolve => setTimeout(resolve, ms))\n\t\t},\n\n\t\thandleRetryMessage(temporaryMessageId) {\n\t\t\tif (this.parsedText === \'\') {\n\t\t\t\tconst temporaryMessage = this.$store.getters.message(this.token, temporaryMessageId)\n\t\t\t\tif (temporaryMessage) {\n\t\t\t\t\tthis.text = temporaryMessage.message || this.text\n\t\t\t\t\tthis.parsedText = temporaryMessage.message || this.parsedText\n\n\t\t\t\t\t// Restore the parent/quote message\n\t\t\t\t\tif (temporaryMessage.parent) {\n\t\t\t\t\t\tconst temporaryParent = this.$store.getters.message(this.token, temporaryMessage.parent)\n\n\t\t\t\t\t\tthis.$store.dispatch(\'addMessageToBeReplied\', {\n\t\t\t\t\t\t\tid: temporaryParent.id,\n\t\t\t\t\t\t\tactorId: temporaryParent.actorId,\n\t\t\t\t\t\t\tactorType: temporaryParent.actorType,\n\t\t\t\t\t\t\tactorDisplayName: temporaryParent.actorDisplayName,\n\t\t\t\t\t\t\ttimestamp: temporaryParent.timestamp,\n\t\t\t\t\t\t\tsystemMessage: temporaryParent.systemMessage,\n\t\t\t\t\t\t\tmessageType: temporaryParent.messageType,\n\t\t\t\t\t\t\tmessage: temporaryParent.message,\n\t\t\t\t\t\t\tmessageParameters: temporaryParent.messageParameters,\n\t\t\t\t\t\t\ttoken: temporaryParent.token,\n\t\t\t\t\t\t\tpreviousMessageId: temporaryParent.previousMessageId,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.$store.dispatch(\'removeTemporaryMessageFromStore\', temporaryMessage)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tasync handleFileShare() {\n\t\t\tpicker.pick()\n\t\t\t\t.then(async (path) => {\n\t\t\t\t\tconsole.debug(`path ${path} selected for sharing`)\n\t\t\t\t\tif (!path.startsWith(\'/\')) {\n\t\t\t\t\t\tthrow new Error(t(\'files\', \'Invalid path selected\'))\n\t\t\t\t\t}\n\t\t\t\t\tshareFile(path, this.token)\n\t\t\t\t\tthis.$refs.advancedInput.focusInput()\n\t\t\t\t})\n\n\t\t\t// FIXME Remove this hack once it is possible to set the parent\n\t\t\t// element of the file picker.\n\t\t\t// By default the file picker is a sibling of the fullscreen\n\t\t\t// element, so it is not visible when in fullscreen mode. It is not\n\t\t\t// possible to specify the parent nor to know when the file picker\n\t\t\t// was actually opened, so for the time being it is reparented if\n\t\t\t// needed shortly after calling it.\n\t\t\tsetTimeout(() => {\n\t\t\t\tif (this.$store.getters.isFullscreen()) {\n\t\t\t\t\tdocument.getElementById(\'content-vue\').appendChild(document.querySelector(\'.oc-dialog\'))\n\t\t\t\t}\n\t\t\t}, 1000)\n\t\t},\n\n\t\t/**\n\t\t * Clicks the hidden file input when clicking the correspondent NcActionButton,\n\t\t * thus opening the file-picker\n\t\t */\n\t\tclickImportInput() {\n\t\t\tthis.$refs.fileUploadInput.click()\n\t\t},\n\n\t\thandleFileInput(event) {\n\t\t\tconst files = Object.values(event.target.files)\n\n\t\t\tthis.handleFiles(files)\n\n\t\t\t// Clear input to ensure that the change event will be emitted if\n\t\t\t// the same file is picked again.\n\t\t\tevent.target.value = \'\'\n\t\t},\n\n\t\t/**\n\t\t * Handles files pasting event\n\t\t *\n\t\t * @param {File[] | FileList} files pasted files list\n\t\t */\n\t\tasync handlePastedFiles(files) {\n\t\t\tthis.handleFiles(files, true)\n\t\t},\n\n\t\t/**\n\t\t * Handles file upload\n\t\t *\n\t\t * @param {File[] | FileList} files pasted files list\n\t\t * @param {boolean} rename whether to rename the files\n\t\t * @param {boolean} isVoiceMessage indicates whether the file is a vooicemessage\n\t\t */\n\t\tasync handleFiles(files, rename = false, isVoiceMessage) {\n\t\t\t// Create a unique id for the upload operation\n\t\t\tconst uploadId = new Date().getTime()\n\t\t\t// Uploads and shares the files\n\t\t\tawait this.$store.dispatch(\'initialiseUpload\', { files, token: this.token, uploadId, rename, isVoiceMessage })\n\t\t},\n\n\t\t/**\n\t\t * Add selected emoji to text input area\n\t\t *\n\t\t * The emoji will be added at the current caret position, and any text\n\t\t * currently selected will be replaced by the emoji. If the input area\n\t\t * does not have the focus there will be no caret or selection; in that\n\t\t * case the emoji will be added at the end.\n\t\t *\n\t\t * @param {string} emoji Emoji object\n\t\t */\n\t\taddEmoji(emoji) {\n\t\t\tconst selection = document.getSelection()\n\n\t\t\tconst contentEditable = this.$refs.advancedInput.$refs.contentEditable\n\n\t\t\t// There is no select, or current selection does not start in the\n\t\t\t// content editable element, so just append the emoji at the end.\n\t\t\tif (!contentEditable.isSameNode(selection.anchorNode) && !contentEditable.contains(selection.anchorNode)) {\n\t\t\t\t// Browsers add a "<br>" element as soon as some rich text is\n\t\t\t\t// written in a content editable div (for example, if a new line\n\t\t\t\t// is added the div content will be "<br><br>"), so the emoji\n\t\t\t\t// has to be added before the last "<br>" (if any).\n\t\t\t\tif (this.text.endsWith(\'<br>\')) {\n\t\t\t\t\tthis.text = this.text.slice(0, this.text.lastIndexOf(\'<br>\')) + emoji + \'<br>\'\n\t\t\t\t} else {\n\t\t\t\t\tthis.text += emoji\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Although due to legacy reasons the API allows several ranges the\n\t\t\t// specification requires the selection to always have a single\n\t\t\t// range.\n\t\t\t// https://developer.mozilla.org/en-US/docs/Web/API/Selection#Multiple_ranges_in_a_selection\n\t\t\tconst range = selection.getRangeAt(0)\n\n\t\t\t// Deleting the contents also collapses the range to the start.\n\t\t\trange.deleteContents()\n\n\t\t\tconst emojiTextNode = document.createTextNode(emoji)\n\t\t\trange.insertNode(emojiTextNode)\n\n\t\t\tthis.text = contentEditable.innerHTML\n\n\t\t\trange.setStartAfter(emojiTextNode)\n\t\t},\n\n\t\thandleAudioFile(payload) {\n\t\t\tthis.handleFiles([payload], false, true)\n\t\t},\n\n\t\thandleRecording(payload) {\n\t\t\tthis.isRecordingAudio = payload\n\t\t},\n\n\t\ttoggleSimplePollsEditor(value) {\n\t\t\tthis.showSimplePollsEditor = value\n\t\t},\n\n\t\t/**\n\t\t * Manages the radio template picker change\n\t\t *\n\t\t * @param {number} fileid the selected template file id\n\t\t */\n\t\tonCheck(fileid) {\n\t\t\tthis.checked = fileid\n\t\t},\n\n\t\t// Create text file and share it to a conversation\n\t\tasync handleCreateTextFile() {\n\t\t\tthis.newFileError = \'\'\n\t\t\tlet filePath = this.$store.getters.getAttachmentFolder() + \'/\' + this.textFileTitle.replace(\'/\', \'\')\n\n\t\t\tif (!filePath.endsWith(this.fileTemplate.extension)) {\n\t\t\t\tfilePath += this.fileTemplate.extension\n\t\t\t}\n\n\t\t\tlet fileData\n\t\t\ttry {\n\t\t\t\tconst response = await createTextFile(\n\t\t\t\t\tfilePath,\n\t\t\t\t\tthis.selectedTemplate?.filename,\n\t\t\t\t\tthis.selectedTemplate?.templateType,\n\t\t\t\t)\n\t\t\t\tfileData = response.data.ocs.data\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\'Error while creating file\', error)\n\t\t\t\tif (error?.response?.data?.ocs?.meta?.message) {\n\t\t\t\t\tshowError(error.response.data.ocs.meta.message)\n\t\t\t\t\tthis.newFileError = error.response.data.ocs.meta.message\n\t\t\t\t} else {\n\t\t\t\t\tshowError(t(\'spreed\', \'Error while creating file\'))\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tawait shareFile(filePath, this.token, \'\', \'\')\n\n\t\t\t// The Viewer expects a file to be set in the sidebar if the sidebar\n\t\t\t// is open.\n\t\t\tif (this.$store.getters.getSidebarStatus) {\n\t\t\t\tOCA.Files.Sidebar.state.file = filePath\n\t\t\t}\n\n\t\t\tOCA.Viewer.open({\n\t\t\t\t// Viewer expects an internal absolute path starting with "/".\n\t\t\t\tpath: filePath,\n\t\t\t\tlist: [\n\t\t\t\t\tfileData,\n\t\t\t\t],\n\t\t\t})\n\n\t\t\t// FIXME Remove this hack once it is possible to set the parent\n\t\t\t// element of the viewer.\n\t\t\t// By default the viewer is a sibling of the fullscreen element, so\n\t\t\t// it is not visible when in fullscreen mode. It is not possible to\n\t\t\t// specify the parent nor to know when the viewer was actually\n\t\t\t// opened, so for the time being it is reparented if needed shortly\n\t\t\t// after calling it.\n\t\t\tsetTimeout(() => {\n\t\t\t\tif (this.$store.getters.isFullscreen()) {\n\t\t\t\t\tdocument.getElementById(\'content-vue\').appendChild(document.getElementById(\'viewer\'))\n\t\t\t\t}\n\t\t\t}, 1000)\n\n\t\t\tthis.dismissTextFileCreation()\n\t\t},\n\n\t\tdismissTextFileCreation() {\n\t\t\tthis.showTextFileDialog = false\n\t\t\tthis.textFileTitle = t(\'spreed\', \'New file\')\n\t\t\tthis.newFileError = \'\'\n\t\t},\n\n\t\t// Focus and select the text within the input field\n\t\tfocusTextDialogInput() {\n\t\t\t// this.$refs.textFileTitleInput.$refs.inputField.$refs.input.focus()\n\t\t\tthis.$refs.textFileTitleInput.$refs.inputField.$refs.input.select()\n\n\t\t},\n\t},\n}\n</script>\n\n<style lang="scss" scoped>\n@import \'../../assets/variables\';\n\n.wrapper {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 12px 0;\n\tborder-top: 1px solid var(--color-border);\n\tmin-height: 69px;\n\t&--chatScrolledToBottom {\n\t\tborder-top: none;\n\t}\n}\n\n.new-message {\n\twidth: 100%;\n\tdisplay: flex;\n\tjustify-content: center;\n\t&-form {\n\t\talign-items: flex-end;\n\t\tdisplay: flex;\n\t\tposition:relative;\n\t\tflex: 0 1 700px;\n\t\tmargin: 0 4px;\n\t\t&__emoji-picker {\n\t\t\tposition: absolute;\n\t\t\tleft: 5px;\n\t\t\tbottom: 1px;\n\t\t\tz-index: 1;\n\t\t}\n\n\t\t&__input {\n\t\t\tflex-grow: 1;\n\t\t\toverflow: hidden;\n\t\t\tposition: relative;\n\t\t\tpadding: 2px;\n\t\t}\n\t\t&__quote {\n\t\t\tmargin: 0 16px 12px 24px;\n\t\t\tbackground-color: var(--color-background-hover);\n\t\t\tpadding: 8px;\n\t\t\tborder-radius: var(--border-radius-large);\n\t\t}\n\n\t\t// put a grey round background when popover is opened\n\t\t// or hover-focused\n\t\t&__icon:hover,\n\t\t&__icon:focus,\n\t\t&__icon:active {\n\t\t\topacity: $opacity_full;\n\t\t\t// good looking on dark AND white bg\n\t\t\tbackground-color: $icon-focus-bg;\n\t\t}\n\n\t}\n}\n\n// Override actions styles TODO: upstream this change\n// Targeting two classess for specificity\n::v-deep .action-item__menutoggle.action-item__menutoggle--with-icon-slot {\n\topacity: 1 !important;\n\t&:hover,\n\t&:focus {\n\t\tbackground-color: var(--color-background-hover) !important;\n\t}\n\t&:disabled {\n\t\topacity: .5 !important;\n\t}\n}\n\n.new-text-file {\n\tdisplay: flex;\n\tjustify-content: center;\n\talign-items: center;\n\tflex-direction: column;\n\tgap: 28px;\n\tpadding: 20px;\n\n\t&__buttons {\n\t\tdisplay: flex;\n\t\tgap: 4px;\n\t\tjustify-content: center;\n\t\tmargin-top: 20px;\n\t}\n\n\t&__form {\n\t\twidth: 100%;\n\n\t\t.templates-picker__list {\n\t\t\tmargin-top: 20px;\n\t\t\tdisplay: grid;\n\t\t\tgrid-gap: calc(var(--margin) * 2);\n\t\t\tgrid-auto-columns: 1fr;\n\t\t\t// We want maximum 5 columns. Putting 6 as we don\'t count the grid gap. So it will always be lower than 6\n\t\t\tmax-width: calc(var(--fullwidth) * 6);\n\t\t\tgrid-template-columns: repeat(auto-fit, var(--fullwidth));\n\t\t\t// Make sure all rows are the same height\n\t\t\tgrid-auto-rows: 1fr;\n\t\t\t// Center the columns set\n\t\t\tjustify-content: center;\n\t\t}\n\t}\n}\n</style>\n', 'message': 'Fix parsing of temporary message\n\nSigned-off-by: Joas Schilling <ab43a7c9cb5b2380afc4ddf8b3e2583169b39a02@schilljs.com>\n', 'author_name': 'Joas Schilling', 'lang': 'vue'}