Merge pull request #7491 from nextcloud/feature/#5330/create-text-document-within-conversation

🗒️ Create text document within conversation
This commit is contained in:
Joas Schilling 2022-08-31 15:11:32 +02:00 коммит произвёл GitHub
Родитель 841e158f0a f33f908a17
Коммит 2fb3ee8220
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 559 добавлений и 16 удалений

Просмотреть файл

@ -376,7 +376,7 @@ export default {
const availableHandlers = OCA.Viewer.availableHandlers
for (let i = 0; i < availableHandlers.length; i++) {
if (availableHandlers[i]?.mimes?.includes(this.mimetype)) {
if (availableHandlers[i]?.mimes?.includes && availableHandlers[i].mimes.includes(this.mimetype)) {
return true
}
}

Просмотреть файл

@ -43,27 +43,41 @@
:disabled="disabled"
:aria-label="t('spreed', 'Share files to the conversation')"
:aria-haspopup="true">
<Paperclip slot="icon"
:size="16" />
<template #icon>
<Paperclip :size="16" />
</template>
<NcActionButton v-if="canUploadFiles"
:close-after-click="true"
icon="icon-upload"
@click.prevent="clickImportInput">
{{ t('spreed', 'Upload new files') }}
<template #icon>
<Upload :size="20" />
</template>
{{ t('spreed', 'Upload from device') }}
</NcActionButton>
<NcActionButton v-if="canShareFiles"
:close-after-click="true"
icon="icon-folder"
@click.prevent="handleFileShare">
{{ t('spreed', 'Share from Files') }}
<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)">
<Poll slot="icon"
:size="20"
decorative
title="" />
<template #icon>
<Poll :size="20" />
</template>
{{ t('spreed', 'Create new poll') }}
</NcActionButton>
</NcActions>
@ -141,15 +155,65 @@
</template>
</form>
</div>
<SimplePollsEditor v-if="showSimplePollsEditor"
:token="token"
@close="toggleSimplePollsEditor(false)" />
<!-- Text file creation dialog -->
<NcModal v-if="showTextFileDialog !== false"
size="normal"
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 } from '@nextcloud/dialogs'
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'
@ -157,7 +221,7 @@ 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 } from '../../services/filesSharingServices.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'
@ -166,6 +230,11 @@ 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)
@ -174,8 +243,14 @@ const picker = getFilePickerBuilder(t('spreed', 'File to share'))
.allowDirectories()
.build()
const border = 2
const margin = 8
const width = margin * 20
export default {
name: 'NewMessageForm',
components: {
AdvancedInput,
Quote,
@ -190,6 +265,11 @@ export default {
BellOff,
SimplePollsEditor,
Poll,
NcModal,
Folder,
Upload,
TemplatePreview,
NcTextField,
},
props: {
@ -207,6 +287,12 @@ export default {
// 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,
}
},
@ -304,7 +390,42 @@ export default {
} 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,
}
},
},
@ -324,6 +445,16 @@ export default {
this.text = ''
}
},
showTextFileDialog(newValue) {
if (newValue !== false) {
const fileTemplate = this.fileTemplateOptions[newValue]
this.textFileTitle = fileTemplate.label + fileTemplate.extension
this.$nextTick(() => {
this.focusTextDialogInput()
})
}
},
},
mounted() {
@ -331,6 +462,10 @@ export default {
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() {
@ -575,6 +710,88 @@ export default {
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>
@ -648,4 +865,38 @@ export default {
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>

Просмотреть файл

@ -0,0 +1,240 @@
<!--
- @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
-
- @author John Molakvoæ <skjnldsv@protonmail.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/>.
-
-->
<!-- FIXME COPY FROM apps/files/src/components/TemplatePreview.vue should be deduplicated -->
<template>
<li class="template-picker__item">
<input :id="id"
:checked="checked"
type="radio"
class="radio"
name="template-picker"
@change="onCheck">
<label :for="id" class="template-picker__label">
<div class="template-picker__preview"
:class="failedPreview ? 'template-picker__preview--failed' : ''">
<img class="template-picker__image"
:src="realPreviewUrl"
alt=""
draggable="false"
@error="onFailure">
</div>
<span class="template-picker__title">
{{ nameWithoutExt }}
</span>
</label>
</li>
</template>
<script>
import { generateUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
const encodeFilePath = function(path) {
const pathSections = (path.startsWith('/') ? path : `/${path}`).split('/')
let relativePath = ''
pathSections.forEach((section) => {
if (section !== '') {
relativePath += '/' + encodeURIComponent(section)
}
})
return relativePath
}
const isPublic = function() {
return !getCurrentUser()
}
const getToken = function() {
return document.getElementById('sharingToken') && document.getElementById('sharingToken').value
}
// preview width generation
const previewWidth = 256
export default {
name: 'TemplatePreview',
inheritAttrs: false,
props: {
basename: {
type: String,
required: true,
},
checked: {
type: Boolean,
default: false,
},
fileid: {
type: [String, Number],
required: true,
},
filename: {
type: String,
required: true,
},
previewUrl: {
type: String,
default: null,
},
hasPreview: {
type: Boolean,
default: true,
},
mime: {
type: String,
required: true,
},
ratio: {
type: Number,
default: null,
},
},
data() {
return {
failedPreview: false,
}
},
computed: {
/**
* Strip away extension from name
*
* @return {string}
*/
nameWithoutExt() {
return this.basename.indexOf('.') > -1 ? this.basename.split('.').slice(0, -1).join('.') : this.basename
},
id() {
return `template-picker-${this.fileid}`
},
realPreviewUrl() {
// If original preview failed, fallback to mime icon
if (this.failedPreview && this.mimeIcon) {
return this.mimeIcon
}
if (this.previewUrl) {
return this.previewUrl
}
// TODO: find a nicer standard way of doing this?
if (isPublic()) {
return generateUrl(`/apps/files_sharing/publicpreview/${getToken()}?fileId=${this.fileid}&file=${encodeFilePath(this.filename)}&x=${previewWidth}&y=${previewWidth}&a=1`)
}
return generateUrl(`/core/preview?fileId=${this.fileid}&x=${previewWidth}&y=${previewWidth}&a=1`)
},
mimeIcon() {
return OC.MimeType.getIconUrl(this.mime)
},
},
methods: {
onCheck() {
this.$emit('check', this.fileid)
},
onFailure() {
this.failedPreview = true
},
},
}
</script>
<style lang="scss" scoped>
.template-picker {
&__item {
display: flex;
}
&__label {
display: flex;
// Align in the middle of the grid
align-items: center;
flex: 1 1;
flex-direction: column;
&, * {
cursor: pointer;
user-select: none;
}
&::before {
display: none !important;
}
}
&__preview {
display: block;
overflow: hidden;
// Stretch so all entries are the same width
flex: 1 1;
width: var(--width);
min-height: var(--height);
max-height: var(--height);
padding: 0;
border: var(--border) solid var(--color-border);
border-radius: var(--border-radius-large);
input:checked + label > & {
border-color: var(--color-primary);
}
&--failed {
// Make sure to properly center fallback icon
display: flex;
}
}
&__image {
max-width: 100%;
background-color: var(--color-main-background);
object-fit: cover;
}
// Failed preview, fallback to mime icon
&__preview--failed &__image {
width: calc(var(--margin) * 8);
// Center mime icon
margin: auto;
background-color: transparent !important;
object-fit: initial;
}
&__title {
overflow: hidden;
// also count preview border
max-width: calc(var(--width) + 2*2px);
padding: var(--margin);
white-space: nowrap;
text-overflow: ellipsis;
}
}
</style>

Просмотреть файл

@ -30,11 +30,11 @@ import { showError } from '@nextcloud/dialogs'
* @param {string} token The conversation's token
* e.g. `/myfile.txt`
* @param {string} referenceId An optional reference id to recognize the message later
* @param {Array} metadata the metadata json encoded array
* @param {string} metadata the metadata json encoded array
*/
const shareFile = async function(path, token, referenceId, metadata) {
try {
return axios.post(
return await axios.post(
generateOcsUrl('apps/files_sharing/api/v1/shares'),
{
shareType: 10, // OC.Share.SHARE_TYPE_ROOM,
@ -55,6 +55,28 @@ const shareFile = async function(path, token, referenceId, metadata) {
}
}
const getFileTemplates = async () => {
return await axios.get(generateOcsUrl('apps/files/api/v1/templates'))
}
/**
* Share a text file to a conversation
*
* @param {string} filePath The new file destination path
* @param {string} templatePath The template source path
* @param {string} templateType The template type e.g 'user'
* @return { object } the file object
*/
const createTextFile = async function(filePath, templatePath, templateType) {
return await axios.post(generateOcsUrl('apps/files/api/v1/templates/create'), {
filePath,
templatePath,
templateType,
})
}
export {
shareFile,
getFileTemplates,
createTextFile,
}

Просмотреть файл

@ -27,7 +27,10 @@ import fromStateOr from './helper.js'
import { findUniquePath, getFileExtension } from '../utils/fileUpload.js'
import moment from '@nextcloud/moment'
import { EventBus } from '../services/EventBus.js'
import { shareFile } from '../services/filesSharingServices.js'
import {
getFileTemplates,
shareFile,
} from '../services/filesSharingServices.js'
import { setAttachmentFolder } from '../services/settingsService.js'
const state = {
@ -36,6 +39,9 @@ const state = {
uploads: {
},
currentUploadId: undefined,
fileTemplatesInitialised: false,
fileTemplates: [],
}
const getters = {
@ -93,6 +99,14 @@ const getters = {
currentUploadId: (state) => {
return state.currentUploadId
},
areFileTemplatesInitialised: (state) => {
return state.fileTemplatesInitialised
},
getFileTemplates: (state) => () => {
return state.fileTemplates
},
}
const mutations = {
@ -182,6 +196,11 @@ const mutations = {
discardUpload(state, { uploadId }) {
Vue.delete(state.uploads, uploadId)
},
storeFilesTemplates(state, { template }) {
state.fileTemplates.push(template)
state.fileTemplatesInitialised = true
},
}
const actions = {
@ -406,6 +425,17 @@ const actions = {
commit('removeFileFromSelection', temporaryMessageId)
},
async getFileTemplates({ commit }) {
try {
const response = await getFileTemplates()
response.data.ocs.data.forEach(template => {
commit('storeFilesTemplates', { template })
})
} catch (error) {
console.error('An error happened when trying to load the templates', error)
}
},
}
export default { state, mutations, getters, actions }