Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>pull/23173/head
parent
3d2024faf9
commit
e7f5516b4d
@ -0,0 +1,35 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
/** |
||||
* @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/>. |
||||
* |
||||
*/ |
||||
namespace OCA\Comments\Event; |
||||
|
||||
use OCP\EventDispatcher\Event; |
||||
|
||||
/** |
||||
* This event is used to load and init the comments app |
||||
* |
||||
* @since 21.0.0 |
||||
*/ |
||||
class LoadCommentsApp extends Event { |
||||
} |
@ -0,0 +1,55 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/** |
||||
* @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/>. |
||||
* |
||||
*/ |
||||
|
||||
namespace OCA\Comments\Listener; |
||||
|
||||
use OCA\Comments\AppInfo\Application; |
||||
use OCA\Comments\Event\LoadCommentsApp; |
||||
use OCP\AppFramework\Services\IInitialState; |
||||
use OCP\Comments\IComment; |
||||
use OCP\EventDispatcher\Event; |
||||
use OCP\EventDispatcher\IEventListener; |
||||
use OCP\Util; |
||||
|
||||
class LoadCommentsAppListener implements IEventListener { |
||||
|
||||
/** @var IInitialState */ |
||||
private $initialStateService; |
||||
|
||||
public function __construct(IInitialState $initialStateService) { |
||||
$this->initialStateService = $initialStateService; |
||||
} |
||||
|
||||
public function handle(Event $event): void { |
||||
if (!($event instanceof LoadCommentsApp)) { |
||||
return; |
||||
} |
||||
|
||||
$this->initialStateService->provideInitialState('max-message-length', IComment::MAX_MESSAGE_LENGTH); |
||||
|
||||
Util::addScript(Application::APP_ID, 'comments-app'); |
||||
} |
||||
} |
@ -0,0 +1,32 @@ |
||||
/** |
||||
* @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/>.
|
||||
* |
||||
*/ |
||||
|
||||
import CommentsInstance from './services/CommentsInstance' |
||||
|
||||
// Init Comments
|
||||
if (window.OCA && !window.OCA.Comments) { |
||||
Object.assign(window.OCA, { Comments: {} }) |
||||
} |
||||
|
||||
// Init Comments App view
|
||||
Object.assign(window.OCA.Comments, { View: CommentsInstance }) |
||||
console.debug('OCA.Comments.View initialized') |
@ -0,0 +1,58 @@ |
||||
/** |
||||
* @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/>.
|
||||
* |
||||
*/ |
||||
|
||||
// Init Comments tab component
|
||||
let TabInstance = null |
||||
const commentTab = new OCA.Files.Sidebar.Tab({ |
||||
id: 'comments', |
||||
name: t('comments', 'Comments'), |
||||
icon: 'icon-comment', |
||||
|
||||
async mount(el, fileInfo, context) { |
||||
if (TabInstance) { |
||||
TabInstance.$destroy() |
||||
} |
||||
TabInstance = new OCA.Comments.View('files', { |
||||
// Better integration with vue parent component
|
||||
parent: context, |
||||
}) |
||||
// Only mount after we have all the info we need
|
||||
await TabInstance.update(fileInfo.id) |
||||
TabInstance.$mount(el) |
||||
}, |
||||
update(fileInfo) { |
||||
TabInstance.update(fileInfo.id) |
||||
}, |
||||
destroy() { |
||||
TabInstance.$destroy() |
||||
TabInstance = null |
||||
}, |
||||
scrollBottomReached() { |
||||
TabInstance.onScrollBottomReached() |
||||
}, |
||||
}) |
||||
|
||||
window.addEventListener('DOMContentLoaded', function() { |
||||
if (OCA.Files && OCA.Files.Sidebar) { |
||||
OCA.Files.Sidebar.registerTab(commentTab) |
||||
} |
||||
}) |
@ -0,0 +1,295 @@ |
||||
<!-- |
||||
- @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/>. |
||||
- |
||||
--> |
||||
<template> |
||||
<div v-show="!deleted" |
||||
:class="{'comment--loading': loading}" |
||||
class="comment"> |
||||
<!-- Comment header toolbar --> |
||||
<div class="comment__header"> |
||||
<!-- Author --> |
||||
<Avatar class="comment__avatar" |
||||
:display-name="actorDisplayName" |
||||
:user="actorId" |
||||
:size="32" /> |
||||
<span class="comment__author">{{ actorDisplayName }}</span> |
||||
|
||||
<!-- Comment actions, |
||||
show if we have a message id and current user is author --> |
||||
<Actions v-if="isOwnComment && id && !loading" class="comment__actions"> |
||||
<template v-if="!editing"> |
||||
<ActionButton |
||||
:close-after-click="true" |
||||
icon="icon-rename" |
||||
@click="onEdit"> |
||||
{{ t('comments', 'Edit comment') }} |
||||
</ActionButton> |
||||
<ActionSeparator /> |
||||
<ActionButton |
||||
:close-after-click="true" |
||||
icon="icon-delete" |
||||
@click="onDeleteWithUndo"> |
||||
{{ t('comments', 'Delete comment') }} |
||||
</ActionButton> |
||||
</template> |
||||
|
||||
<ActionButton v-else |
||||
icon="icon-close" |
||||
@click="onEditCancel"> |
||||
{{ t('comments', 'Cancel edit') }} |
||||
</ActionButton> |
||||
</Actions> |
||||
|
||||
<!-- Show loading if we're editing or deleting, not on new ones --> |
||||
<div v-if="id && loading" class="comment_loading icon-loading-small" /> |
||||
|
||||
<!-- Relative time to the comment creation --> |
||||
<Moment v-else-if="creationDateTime" class="comment__timestamp" :timestamp="timestamp" /> |
||||
</div> |
||||
|
||||
<!-- Message editor --> |
||||
<div class="comment__message" v-if="editor || editing"> |
||||
<RichContenteditable v-model="localMessage" :auto-complete="autoComplete" :contenteditable="!loading" /> |
||||
<input v-tooltip="t('comments', 'Post comment')" |
||||
:class="loading ? 'icon-loading-small' :'icon-confirm'" |
||||
class="comment__submit" |
||||
type="submit" |
||||
:disabled="isEmptyMessage" |
||||
value="" |
||||
@click="onSubmit"> |
||||
</div> |
||||
|
||||
<!-- Message content --> |
||||
<!-- The html is escaped and sanitized before rendering --> |
||||
<!-- eslint-disable-next-line vue/no-v-html--> |
||||
<div v-else class="comment__message" v-html="renderedContent" /> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import { getCurrentUser } from '@nextcloud/auth' |
||||
import moment from 'moment' |
||||
|
||||
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' |
||||
import Actions from '@nextcloud/vue/dist/Components/Actions' |
||||
import ActionSeparator from '@nextcloud/vue/dist/Components/ActionSeparator' |
||||
import Avatar from '@nextcloud/vue/dist/Components/Avatar' |
||||
import RichContenteditable from '@nextcloud/vue/dist/Components/RichContenteditable' |
||||
import RichEditorMixin from '@nextcloud/vue/dist/Mixins/richEditor' |
||||
|
||||
import Moment from './Moment' |
||||
import CommentMixin from '../mixins/CommentMixin' |
||||
|
||||
export default { |
||||
name: 'Comment', |
||||
|
||||
components: { |
||||
ActionButton, |
||||
Actions, |
||||
ActionSeparator, |
||||
Avatar, |
||||
Moment, |
||||
RichContenteditable, |
||||
}, |
||||
mixins: [RichEditorMixin, CommentMixin], |
||||
|
||||
inheritAttrs: false, |
||||
|
||||
props: { |
||||
source: { |
||||
type: Object, |
||||
default: () => ({}), |
||||
}, |
||||
actorDisplayName: { |
||||
type: String, |
||||
required: true, |
||||
}, |
||||
actorId: { |
||||
type: String, |
||||
required: true, |
||||
}, |
||||
creationDateTime: { |
||||
type: String, |
||||
default: null, |
||||
}, |
||||
|
||||
/** |
||||
* Force the editor display |
||||
*/ |
||||
editor: { |
||||
type: Boolean, |
||||
default: false, |
||||
}, |
||||
|
||||
/** |
||||
* Provide the autocompletion data |
||||
*/ |
||||
autoComplete: { |
||||
type: Function, |
||||
required: true, |
||||
}, |
||||
}, |
||||
|
||||
data() { |
||||
return { |
||||
// Only change data locally and update the original |
||||
// parent data when the request is sent and resolved |
||||
localMessage: '', |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
|
||||
/** |
||||
* Is the current user the author of this comment |
||||
* @returns {boolean} |
||||
*/ |
||||
isOwnComment() { |
||||
return getCurrentUser().uid === this.actorId |
||||
}, |
||||
|
||||
/** |
||||
* Rendered content as html string |
||||
* @returns {string} |
||||
*/ |
||||
renderedContent() { |
||||
if (this.isEmptyMessage) { |
||||
return '' |
||||
} |
||||
return this.renderContent(this.localMessage) |
||||
}, |
||||
|
||||
isEmptyMessage() { |
||||
return !this.localMessage || this.localMessage.trim() === '' |
||||
}, |
||||
|
||||
timestamp() { |
||||
// seconds, not milliseconds |
||||
return parseInt(moment(this.creationDateTime).format('x'), 10) / 1000 |
||||
}, |
||||
}, |
||||
|
||||
watch: { |
||||
// If the data change, update the local value |
||||
message(message) { |
||||
this.updateLocalMessage(message) |
||||
}, |
||||
}, |
||||
|
||||
beforeMount() { |
||||
// Init localMessage |
||||
this.updateLocalMessage(this.message) |
||||
}, |
||||
|
||||
methods: { |
||||
/** |
||||
* Update local Message on outer change |
||||
* @param {string} message the message to set |
||||
*/ |
||||
updateLocalMessage(message) { |
||||
this.localMessage = message.toString() |
||||
}, |
||||
|
||||
/** |
||||
* Dispatch message between edit and create |
||||
*/ |
||||
onSubmit() { |
||||
if (this.editor) { |
||||
this.onNewComment(this.localMessage) |
||||
return |
||||
} |
||||
this.onEditComment(this.localMessage) |
||||
}, |
||||
}, |
||||
|
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
$comment-padding: 10px; |
||||
|
||||
.comment { |
||||
position: relative; |
||||
padding: $comment-padding 0 $comment-padding * 1.5; |
||||
|
||||
&__header { |
||||
display: flex; |
||||
align-items: center; |
||||
min-height: 44px; |
||||
padding: $comment-padding / 2 0; |
||||
} |
||||
|
||||
&__author, |
||||
&__actions { |
||||
margin-left: $comment-padding !important; |
||||
} |
||||
|
||||
&__author { |
||||
overflow: hidden; |
||||
white-space: nowrap; |
||||
text-overflow: ellipsis; |
||||
color: var(--color-text-maxcontrast); |
||||
} |
||||
|
||||
&_loading, |
||||
&__timestamp { |
||||
margin-left: auto; |
||||
color: var(--color-text-maxcontrast); |
||||
} |
||||
|
||||
&__message { |
||||
position: relative; |
||||
// Avatar size, align with author name |
||||
padding-left: 32px + $comment-padding; |
||||
} |
||||
|
||||
&__submit { |
||||
position: absolute; |
||||
right: 0; |
||||
bottom: 0; |
||||
width: 44px; |
||||
height: 44px; |
||||
// Align with input border |
||||
margin: 1px; |
||||
cursor: pointer; |
||||
opacity: .7; |
||||
border: none; |
||||
background-color: transparent !important; |
||||
|
||||
&:disabled { |
||||
cursor: not-allowed; |
||||
opacity: .5; |
||||
} |
||||
|
||||
&:focus, |
||||
&:hover { |
||||
opacity: 1; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.rich-contenteditable__input { |
||||
margin: 0; |
||||
padding: $comment-padding; |
||||
min-height: 44px; |
||||
} |
||||
|
||||
</style> |
@ -0,0 +1,31 @@ |
||||
<!-- TODO: Move to vue components --> |
||||
|
||||
<template> |
||||
<span class="live-relative-timestamp" :data-timestamp="timestamp * 1000" :title="title">{{ formatted }}</span> |
||||
</template> |
||||
|
||||
<script> |
||||
import moment from '@nextcloud/moment' |
||||
|
||||
export default { |
||||
name: 'Moment', |
||||
props: { |
||||
timestamp: { |
||||
type: Number, |
||||
required: true, |
||||
}, |
||||
format: { |
||||
type: String, |
||||
default: 'LLL', |
||||
}, |
||||
}, |
||||
computed: { |
||||
title() { |
||||
return moment.unix(this.timestamp).format(this.format) |
||||
}, |
||||
formatted() { |
||||
return moment.unix(this.timestamp).fromNow() |
||||
}, |
||||
}, |
||||
} |
||||
</script> |
@ -0,0 +1,117 @@ |
||||
/** |
||||
* @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/>.
|
||||
* |
||||
*/ |
||||
|
||||
import NewComment from '../services/NewComment' |
||||
import DeleteComment from '../services/DeleteComment' |
||||
import EditComment from '../services/EditComment' |
||||
import { showError, showUndo, TOAST_UNDO_TIMEOUT } from '@nextcloud/dialogs' |
||||
|
||||
export default { |
||||
props: { |
||||
id: { |
||||
type: Number, |
||||
default: null, |
||||
}, |
||||
message: { |
||||
// GenFileInfo can convert message as numbers if they doesn't contains text
|
||||
type: [String, Number], |
||||
default: '', |
||||
}, |
||||
ressourceId: { |
||||
type: [String, Number], |
||||
required: true, |
||||
}, |
||||
}, |
||||
|
||||
data() { |
||||
return { |
||||
deleted: false, |
||||
editing: false, |
||||
loading: false, |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
// EDITION
|
||||
onEdit() { |
||||
this.editing = true |
||||
}, |
||||
onEditCancel() { |
||||
this.editing = false |
||||
// Restore original value
|
||||
this.updateLocalMessage(this.message) |
||||
}, |
||||
async onEditComment(message) { |
||||
this.loading = true |
||||
try { |
||||
await EditComment(this.commentsType, this.ressourceId, this.id, message) |
||||
this.logger.debug('Comment edited', { commentsType: this.commentsType, ressourceId: this.ressourceId, id: this.id, message }) |
||||
this.$emit('update:message', message) |
||||
this.editing = false |
||||
} catch (error) { |
||||
showError(t('comments', 'An error occurred while trying to edit the comment')) |
||||
console.error(error) |
||||
} finally { |
||||
this.loading = false |
||||
} |
||||
}, |
||||
|
||||
// DELETION
|
||||
onDeleteWithUndo() { |
||||
this.deleted = true |
||||
const timeOutDelete = setTimeout(this.onDelete, TOAST_UNDO_TIMEOUT) |
||||
showUndo(t('comments', 'Comment deleted'), () => { |
||||
clearTimeout(timeOutDelete) |
||||
this.deleted = false |
||||
}) |
||||
}, |
||||
async onDelete() { |
||||
try { |
||||
await DeleteComment(this.commentsType, this.ressourceId, this.id) |
||||
this.logger.debug('Comment deleted', { commentsType: this.commentsType, ressourceId: this.ressourceId, id: this.id }) |
||||
this.$emit('delete', this.id) |
||||
} catch (error) { |
||||
showError(t('comments', 'An error occurred while trying to delete the comment')) |
||||
console.error(error) |
||||
this.deleted = false |
||||
} |
||||
}, |
||||
|
||||
// CREATION
|
||||
async onNewComment(message) { |
||||
this.loading = true |
||||
try { |
||||
const newComment = await NewComment(this.commentsType, this.ressourceId, message) |
||||
this.logger.debug('New comment posted', { commentsType: this.commentsType, ressourceId: this.ressourceId, newComment }) |
||||
this.$emit('new', newComment) |
||||
// Clear old content
|
||||
this.$emit('update:message', '') |
||||
this.localMessage = '' |
||||
} catch (error) { |
||||
showError(t('comments', 'An error occurred while trying to create the comment')) |
||||
console.error(error) |
||||
} finally { |
||||
this.loading = false |
||||
} |
||||
}, |
||||
}, |
||||
} |
@ -0,0 +1,69 @@ |
||||
/** |
||||
* @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/>.
|
||||
* |
||||
*/ |
||||
|
||||
import { getLoggerBuilder } from '@nextcloud/logger' |
||||
import { translate as t, translatePlural as n } from '@nextcloud/l10n' |
||||
import CommentsApp from '../views/Comments' |
||||
import Vue from 'vue' |
||||
|
||||
const logger = getLoggerBuilder() |
||||
.setApp('comments') |
||||
.detectUser() |
||||
.build() |
||||
|
||||
// Add translates functions
|
||||
Vue.mixin({ |
||||
data() { |
||||
return { |
||||
logger, |
||||
} |
||||
}, |
||||
methods: { |
||||
t, |
||||
n, |
||||
}, |
||||
}) |
||||
|
||||
export default class CommentInstance { |
||||
|
||||
/** |
||||
* Initialize a new Comments instance for the desired type |
||||
* |
||||
* @param {string} commentsType the comments endpoint type |
||||
* @param {Object} options the vue options (propsData, parent, el...) |
||||
*/ |
||||
constructor(commentsType = 'files', options) { |
||||
// Add comments type as a global mixin
|
||||
Vue.mixin({ |
||||
data() { |
||||
return { |
||||
commentsType, |
||||
} |
||||
}, |
||||
}) |
||||
|
||||
// Init Comments component
|
||||
const View = Vue.extend(CommentsApp) |
||||
return new View(options) |
||||
} |
||||
|
||||
} |
@ -0,0 +1,37 @@ |
||||
/** |
||||
* @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/>.
|
||||
* |
||||
*/ |
||||
|
||||
import webdav from 'webdav' |
||||
import axios from '@nextcloud/axios' |
||||
import { getRootPath } from '../utils/davUtils' |
||||
|
||||
// Add this so the server knows it is an request from the browser
|
||||
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest' |
||||
|
||||
// force our axios
|
||||
const patcher = webdav.getPatcher() |
||||
patcher.patch('request', axios) |
||||
|
||||
// init webdav client
|
||||
const client = webdav.createClient(getRootPath()) |
||||
|
||||
export default client |
@ -0,0 +1,37 @@ |
||||
/** |
||||
* @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/>.
|
||||
* |
||||
*/ |
||||
|
||||
import client from './DavClient' |
||||
|
||||
/** |
||||
* Delete a comment |
||||
* |
||||
* @param {string} commentsType the ressource type |
||||
* @param {number} ressourceId the ressource ID |
||||
* @param {number} commentId the comment iD |
||||
*/ |
||||
export default async function(commentsType, ressourceId, commentId) { |
||||
const commentPath = ['', commentsType, ressourceId, commentId].join('/') |
||||
|
||||
// Fetch newly created comment data
|
||||
await client.deleteFile(commentPath) |
||||
} |
@ -0,0 +1,49 @@ |
||||
/** |
||||
* @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/>.
|
||||
* |
||||
*/ |
||||
|
||||
import client from './DavClient' |
||||
|
||||
/** |
||||
* Edit an existing comment |
||||
* |
||||
* @param {string} commentsType the ressource type |
||||
* @param {number} ressourceId the ressource ID |
||||
* @param {number} commentId the comment iD |
||||
* @param {string} message the message content |
||||
*/ |
||||
export default async function(commentsType, ressourceId, commentId, message) { |
||||
const commentPath = ['', commentsType, ressourceId, commentId].join('/') |
||||
|
||||
return await client.customRequest(commentPath, Object.assign({ |
||||
method: 'PROPPATCH', |
||||
data: `<?xml version="1.0"?>
|
||||
<d:propertyupdate |
||||
xmlns:d="DAV:" |
||||
xmlns:oc="http://owncloud.org/ns"> |
||||
<d:set> |
||||
<d:prop> |
||||
<oc:message>${message}</oc:message> |
||||
</d:prop> |
||||
</d:set> |
||||
</d:propertyupdate>`, |
||||
})) |
||||
} |
@ -0,0 +1,80 @@ |
||||
/** |
||||
* @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/>.
|
||||
* |
||||
*/ |
||||
|
||||
import { parseXML, prepareFileFromProps } from 'webdav/dist/node/interface/dav' |
||||
import { processResponsePayload } from 'webdav/dist/node/response' |
||||
import client from './DavClient' |
||||
import { genFileInfo } from '../utils/fileUtils' |
||||
|
||||
export const DEFAULT_LIMIT = 5 |
||||
/** |
||||
* Retrieve the comments list |
||||
* |
||||
* @param {Object} data destructuring object |
||||
* @param {string} data.commentsType the ressource type |
||||
* @param {number} data.ressourceId the ressource ID |
||||
* @param {Object} [options] optional options for axios |
||||
* @returns {Object[]} the comments list |
||||
*/ |
||||
export default async function({ commentsType, ressourceId }, options = {}) { |
||||
let response = null |
||||
const ressourcePath = ['', commentsType, ressourceId].join('/') |
||||
|
||||
return await client.customRequest(ressourcePath, Object.assign({ |
||||
method: 'REPORT', |
||||
data: `<?xml version="1.0"?>
|
||||
<oc:filter-comments |
||||
xmlns:d="DAV:" |
||||
xmlns:oc="http://owncloud.org/ns" |
||||
xmlns:nc="http://nextcloud.org/ns" |
||||
xmlns:ocs="http://open-collaboration-services.org/ns"> |
||||
<oc:limit>${DEFAULT_LIMIT}</oc:limit> |
||||
<oc:offset>${options.offset || 0}</oc:offset> |
||||
</oc:filter-comments>`, |
||||
}, options)) |
||||
// See example on how it's done normaly
|
||||
// https://github.com/perry-mitchell/webdav-client/blob/9de2da4a2599e06bd86c2778145b7ade39fe0b3c/source/interface/stat.js#L19
|
||||
// Waiting for proper REPORT integration https://github.com/perry-mitchell/webdav-client/issues/207
|
||||
.then(res => { |
||||
response = res |
||||
return res.data |
||||
}) |
||||
.then(parseXML) |
||||
.then(xml => processMultistatus(xml, true)) |
||||
.then(comments => processResponsePayload(response, comments, true)) |
||||
.then(response => response.data.map(genFileInfo)) |
||||
} |
||||
|
||||
// https://github.com/perry-mitchell/webdav-client/blob/9de2da4a2599e06bd86c2778145b7ade39fe0b3c/source/interface/directoryContents.js#L32
|
||||
function processMultistatus(result, isDetailed = false) { |
||||
// Extract the response items (directory contents)
|
||||
const { |
||||
multistatus: { response: responseItems }, |
||||
} = result |
||||
return responseItems.map(item => { |
||||
// Each item should contain a stat object
|
||||
const { |
||||
propstat: { prop: props }, |
||||
} = item |
||||
return prepareFileFromProps(props, props.id.toString(), isDetailed) |
||||
}) |
||||
} |
@ -0,0 +1,60 @@ |
||||
/** |
||||
* @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/>.
|
||||
* |
||||
*/ |
||||
|
||||
import { genFileInfo } from '../utils/fileUtils' |
||||
import { getCurrentUser } from '@nextcloud/auth' |
||||
import { getRootPath } from '../utils/davUtils' |
||||
import axios from '@nextcloud/axios' |
||||
import client from './DavClient' |
||||
|
||||
/** |
||||
* Retrieve the comments list |
||||
* |
||||
* @param {string} commentsType the ressource type |
||||
* @param {number} ressourceId the ressource ID |
||||
* @param {string} message the message |
||||
* @returns {Object} the new comment |
||||
*/ |
||||
export default async function(commentsType, ressourceId, message) { |
||||
const ressourcePath = ['', commentsType, ressourceId].join('/') |
||||
|
||||
const response = await axios.post(getRootPath() + ressourcePath, { |
||||
actorDisplayName: getCurrentUser().displayName, |
||||
actorId: getCurrentUser().uid, |
||||
actorType: 'users', |
||||
creationDateTime: (new Date()).toUTCString(), |
||||
message, |
||||
objectType: 'files', |
||||
verb: 'comment', |
||||
}) |
||||
|
||||
// Retrieve comment id from ressource location
|
||||
const commentId = parseInt(response.headers['content-location'].split('/').pop()) |
||||
const commentPath = ressourcePath + '/' + commentId |
||||
|
||||
// Fetch newly created comment data
|
||||
const comment = await client.stat(commentPath, { |
||||
details: true, |
||||
}) |
||||
|
||||
return genFileInfo(comment) |
||||
} |
@ -0,0 +1,62 @@ |
||||
/** |
||||
* @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/>.
|
||||
* |
||||
*/ |
||||
|
||||
import axios from '@nextcloud/axios' |
||||
|
||||
/** |
||||
* Create a cancel token |
||||
* @returns {CancelTokenSource} |
||||
*/ |
||||
const createCancelToken = () => axios.CancelToken.source() |
||||
|
||||
/** |
||||
* Creates a cancelable axios 'request object'. |
||||
* |
||||
* @param {function} request the axios promise request |
||||
* @returns {Object} |
||||
*/ |
||||
const cancelableRequest = function(request) { |
||||
/** |
||||
* Generate an axios cancel token |
||||
*/ |
||||
const cancelToken = createCancelToken() |
||||
|
||||
/** |
||||
* Execute the request |
||||
* |
||||
* @param {string} url the url to send the request to |
||||
* @param {Object} [options] optional config for the request |
||||
*/ |
||||
const fetch = async function(url, options) { |
||||
return request( |
||||
url, |
||||
Object.assign({ cancelToken: cancelToken.token }, options) |
||||
) |
||||
} |
||||
|
||||
return { |
||||
request: fetch, |
||||
cancel: cancelToken.cancel, |
||||
} |
||||
} |
||||
|
||||
export default cancelableRequest |
@ -0,0 +1,29 @@ |
||||
/** |
||||
* @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/>.
|
||||
* |
||||
*/ |
||||
|
||||
import { generateRemoteUrl } from '@nextcloud/router' |
||||
|
||||
const getRootPath = function() { |
||||
return generateRemoteUrl('dav/comments') |
||||
} |
||||
|
||||
export { getRootPath } |
@ -0,0 +1,122 @@ |
||||
/** |
||||
* @copyright Copyright (c) 2019 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/>.
|
||||
* |
||||
*/ |
||||
import camelcase from 'camelcase' |
||||
import { isNumber } from './numberUtil' |
||||
|
||||
/** |
||||
* Get an url encoded path |
||||
* |
||||
* @param {String} path the full path |
||||
* @returns {string} url encoded file path |
||||
*/ |
||||
const encodeFilePath = function(path) { |
||||
const pathSections = (path.startsWith('/') ? path : `/${path}`).split('/') |
||||
let relativePath = '' |
||||
pathSections.forEach((section) => { |
||||
if (section !== '') { |
||||
relativePath += '/' + encodeURIComponent(section) |
||||
} |
||||
}) |
||||
return relativePath |
||||
} |
||||
|
||||
/** |
||||
* Extract dir and name from file path |
||||
* |
||||
* @param {String} path the full path |
||||
* @returns {String[]} [dirPath, fileName] |
||||
*/ |
||||
const extractFilePaths = function(path) { |
||||
const pathSections = path.split('/') |
||||
const fileName = pathSections[pathSections.length - 1] |
||||
const dirPath = pathSections.slice(0, pathSections.length - 1).join('/') |
||||
return [dirPath, fileName] |
||||
} |
||||
|
||||
/** |
||||
* Sorting comparison function |
||||
* |
||||
* @param {Object} fileInfo1 file 1 fileinfo |
||||
* @param {Object} fileInfo2 file 2 fileinfo |
||||
* @param {string} key key to sort with |
||||
* @param {boolean} [asc=true] sort ascending? |
||||
* @returns {number} |
||||
*/ |
||||
const sortCompare = function(fileInfo1, fileInfo2, key, asc = true) { |
||||
|
||||
if (fileInfo1.isFavorite && !fileInfo2.isFavorite) { |
||||
return -1 |
||||
} else if (!fileInfo1.isFavorite && fileInfo2.isFavorite) { |
||||
return 1 |
||||
} |
||||
|
||||
// if this is a number, let's sort by integer
|
||||
if (isNumber(fileInfo1[key]) && isNumber(fileInfo2[key])) { |
||||
return Number(fileInfo1[key]) - Number(fileInfo2[key]) |
||||
} |
||||
|
||||
// else we sort by string, so let's sort directories first
|
||||
if (fileInfo1.type === 'directory' && fileInfo2.type !== 'directory') { |
||||
return -1 |
||||
} else if (fileInfo1.type !== 'directory' && fileInfo2.type === 'directory') { |
||||
return 1 |
||||
} |
||||
|
||||
// finally sort by name
|
||||
return asc |
||||
? fileInfo1[key].localeCompare(fileInfo2[key], OC.getLanguage()) |
||||
: -fileInfo1[key].localeCompare(fileInfo2[key], OC.getLanguage()) |
||||
} |
||||
|
||||
/** |
||||
* Generate a fileinfo object based on the full dav properties |
||||
* It will flatten everything and put all keys to camelCase |
||||
* |
||||
* @param {Object} obj the object |
||||
* @returns {Object} |
||||
*/ |
||||
const genFileInfo = function(obj) { |
||||
const fileInfo = {} |
||||
|
||||
Object.keys(obj).forEach(key => { |
||||
const data = obj[key] |
||||
|
||||
// flatten object if any
|
||||
if (!!data && typeof data === 'object' && !Array.isArray(data)) { |
||||
Object.assign(fileInfo, genFileInfo(data)) |
||||
} else { |
||||
// format key and add it to the fileInfo
|
||||
if (data === 'false') { |
||||
fileInfo[camelcase(key)] = false |
||||
} else if (data === 'true') { |
||||
fileInfo[camelcase(key)] = true |
||||
} else { |
||||
fileInfo[camelcase(key)] = isNumber(data) |
||||
? Number(data) |
||||
: data |
||||
} |
||||
} |
||||
}) |
||||
return fileInfo |
||||
} |
||||
|
||||
export { encodeFilePath, extractFilePaths, sortCompare, genFileInfo } |
@ -0,0 +1,30 @@ |
||||
/** |
||||
* @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/>.
|
||||
* |
||||
*/ |
||||
|
||||
const isNumber = function(num) { |
||||
if (!num) { |
||||
return false |
||||
} |
||||
return Number(num).toString() === num.toString() |
||||
} |
||||
|
||||
export { isNumber } |
@ -0,0 +1,264 @@ |
||||
<!-- |
||||
- @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/>. |
||||
- |
||||
--> |
||||
|
||||
<template> |
||||
<div class="comments" :class="{ 'icon-loading': isFirstLoading }"> |
||||
<!-- Editor --> |
||||
<Comment v-bind="editorData" |
||||
:auto-complete="autoComplete" |
||||
:editor="true" |
||||
:ressource-id="ressourceId" |
||||
class="comments__writer" |
||||
@new="onNewComment" /> |
||||
|
||||
<template v-if="!isFirstLoading"> |
||||
<EmptyContent v-if="!hasComments && done" icon="icon-comment"> |
||||
{{ t('comments', 'No comments yet, start the conversation!') }} |
||||
</EmptyContent> |
||||
|
||||
<!-- Comments --> |
||||
<Comment v-for="comment in comments" |
||||
v-else |
||||
:key="comment.id" |
||||
v-bind="comment" |
||||
:auto-complete="autoComplete" |
||||
:ressource-id="ressourceId" |
||||
:message.sync="comment.message" |
||||
class="comments__list" |
||||
@delete="onDelete" /> |
||||
|
||||
<!-- Loading more message --> |
||||
<div v-if="loading && !isFirstLoading" class="comments__info icon-loading" /> |
||||
|
||||
<div v-else-if="hasComments && done" class="comments__info"> |
||||
{{ t('comments', 'No more messages') }} |
||||
</div> |
||||
|
||||
<!-- Error message --> |
||||
<EmptyContent v-else-if="error" class="comments__error" icon="icon-error"> |
||||
{{ error }} |
||||
<template #desc> |
||||
<button icon="icon-history" @click="getComments"> |
||||
{{ t('comments', 'Retry') }} |
||||
</button> |
||||
</template> |
||||
</EmptyContent> |
||||
</template> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import { generateOcsUrl } from '@nextcloud/router' |
||||
import { getCurrentUser } from '@nextcloud/auth' |
||||
import axios from '@nextcloud/axios' |
||||
import VTooltip from 'v-tooltip' |
||||
import Vue from 'vue' |
||||
|
||||
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent' |
||||
|
||||
import Comment from '../components/Comment' |
||||
import getComments, { DEFAULT_LIMIT } from '../services/GetComments' |
||||
import cancelableRequest from '../utils/cancelableRequest' |
||||
|
||||
Vue.use(VTooltip) |
||||
|
||||
export default { |
||||
name: 'Comments', |
||||
|
||||
components: { |
||||
// Avatar, |
||||
Comment, |
||||
EmptyContent, |
||||
}, |
||||
|
||||
data() { |
||||
return { |
||||
error: '', |
||||
loading: false, |
||||
done: false, |
||||
|
||||
ressourceId: null, |
||||
offset: 0, |
||||
comments: [], |
||||
|
||||
cancelRequest: () => {}, |
||||
|
||||
editorData: { |
||||
actorDisplayName: getCurrentUser().displayName, |
||||
actorId: getCurrentUser().uid, |
||||
key: 'editor', |
||||
}, |
||||
|
||||
Comment, |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
hasComments() { |
||||
return this.comments.length > 0 |
||||
}, |
||||
isFirstLoading() { |
||||
return this.loading && this.offset === 0 |
||||
}, |
||||
}, |
||||
|
||||
methods: { |
||||
/** |
||||
* Update current ressourceId and fetch new data |
||||
* @param {Number} ressourceId the current ressourceId (fileId...) |
||||
*/ |
||||
async update(ressourceId) { |
||||
this.ressourceId = ressourceId |
||||
this.resetState() |
||||
this.getComments() |
||||
}, |
||||
|
||||
/** |
||||
* Ran when the bottom of the tab is reached |
||||
*/ |
||||
onScrollBottomReached() { |
||||
/** |
||||
* Do not fetch more if we: |
||||
* - are showing an error |
||||
* - already fetched everything |
||||
* - are currently loading |
||||
*/ |
||||
if (this.error || this.done || this.loading) { |
||||
return |
||||
} |
||||
this.getComments() |
||||
}, |
||||
|
||||
/** |
||||
* Get the existing shares infos |
||||
*/ |
||||
async getComments() { |
||||
// Cancel any ongoing request |
||||
this.cancelRequest('cancel') |
||||
|
||||
try { |
||||
this.loading = true |
||||
this.error = '' |
||||
|
||||
// Init cancellable request |
||||
const { request, cancel } = cancelableRequest(getComments) |
||||
this.cancelRequest = cancel |
||||
|
||||
// Fetch comments |
||||
const comments = await request({ |
||||
commentsType: this.commentsType, |
||||
ressourceId: this.ressourceId, |
||||
}, { offset: this.offset }) |
||||
|
||||
this.logger.debug(`Processed ${comments.length} comments`, { comments }) |
||||
|
||||
// We received less than the requested amount, |
||||
// we're done fetching comments |
||||
if (comments.length < DEFAULT_LIMIT) { |
||||
this.done = true |
||||
} |
||||
|
||||
// Insert results |
||||
this.comments.push(...comments) |
||||
|
||||
// Increase offset for next fetch |
||||
this.offset += DEFAULT_LIMIT |
||||
} catch (error) { |
||||
if (error.message === 'cancel') { |
||||
return |
||||
} |
||||
// Reverting offset |
||||
this.error = t('comments', 'Unable to load the comments list') |
||||
console.error('Error loading the comments list', error) |
||||
} finally { |
||||
this.loading = false |
||||
} |
||||
}, |
||||
|
||||
/** |
||||
* Autocomplete @mentions |
||||
* @param {string} search the query |
||||
* @param {Function} callback the callback to process the results with |
||||
*/ |
||||
async autoComplete(search, callback) { |
||||
const results = await axios.get(generateOcsUrl('core', 2) + 'autocomplete/get', { |
||||
params: { |
||||
search, |
||||
itemType: 'files', |
||||
itemId: this.ressourceId, |
||||
sorter: 'commenters|share-recipients', |
||||
limit: OC.appConfig?.comments?.maxAutoCompleteResults || 25, |
||||
}, |
||||
}) |
||||
return callback(results.data.ocs.data) |
||||
}, |
||||
|
||||
/** |
||||
* Add newly created comment to the list |
||||
* @param {Object} comment the new comment |
||||
*/ |
||||
onNewComment(comment) { |
||||
this.comments.unshift(comment) |
||||
}, |
||||
|
||||
/** |
||||
* Remove deleted comment from the list |
||||
* @param {number} id the deleted comment |
||||
*/ |
||||
onDelete(id) { |
||||
const index = this.comments.findIndex(comment => comment.id === id) |
||||
if (index > -1) { |
||||
this.comments.splice(index, 1) |
||||
} else { |
||||
console.error('Could not find the deleted comment in the list', id) |
||||
} |
||||
}, |
||||
|
||||
/** |
||||
* Reset the current view to its default state |
||||
*/ |
||||
resetState() { |
||||
this.error = '' |
||||
this.loading = false |
||||
this.done = false |
||||
this.offset = 0 |
||||
this.comments = [] |
||||
}, |
||||
}, |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.comments { |
||||
// Do not add emptycontent top margin |
||||
&__error{ |
||||
margin-top: 0; |
||||
} |
||||
|
||||
&__info { |
||||
height: 60px; |
||||
color: var(--color-text-maxcontrast); |
||||
text-align: center; |
||||
line-height: 60px; |
||||
} |
||||
} |
||||
</style> |
@ -1,14 +1,18 @@ |
||||
const path = require('path') |
||||
|
||||
module.exports = { |
||||
entry: path.join(__dirname, 'src', 'comments.js'), |
||||
entry: { |
||||
comments: path.join(__dirname, 'src', 'comments.js'), |
||||
'comments-app': path.join(__dirname, 'src', 'comments-app.js'), |
||||
'comments-tab': path.join(__dirname, 'src', 'comments-tab.js'), |
||||
}, |
||||
output: { |
||||
path: path.resolve(__dirname, './js'), |
||||
publicPath: '/js/', |
||||
filename: 'comments.js', |
||||
jsonpFunction: 'webpackJsonpComments' |
||||
filename: '[name].js', |
||||
jsonpFunction: 'webpackJsonpComments', |
||||
}, |
||||
externals: { |
||||
jquery: 'jQuery' |
||||
} |
||||
jquery: 'jQuery', |
||||
}, |
||||
} |
||||
|
Loading…
Reference in new issue