diff --git a/lib/Listener/LoadSidebarScripts.php b/lib/Listener/LoadSidebarScripts.php
index 96194bbc3..e3c8b5a52 100644
--- a/lib/Listener/LoadSidebarScripts.php
+++ b/lib/Listener/LoadSidebarScripts.php
@@ -44,6 +44,6 @@ public function handle(Event $event): void {
// TODO: make sure to only include the sidebar script when
// we properly split it between files list and sidebar
Util::addStyle(Application::APP_ID, 'style');
- Util::addScript(Application::APP_ID, 'activity-sidebar');
+ Util::addScript(Application::APP_ID, 'activity-sidebar', 'files');
}
}
diff --git a/src/components/Comment.vue b/src/components/Comment.vue
new file mode 100644
index 000000000..e8ae9a88e
--- /dev/null
+++ b/src/components/Comment.vue
@@ -0,0 +1,356 @@
+
+
+
+
+
+
+
+
diff --git a/src/components/Moment.vue b/src/components/Moment.vue
new file mode 100644
index 000000000..a91ed8b9c
--- /dev/null
+++ b/src/components/Moment.vue
@@ -0,0 +1,31 @@
+
+
+
+ {{ formatted }}
+
+
+
diff --git a/src/mixins/CommentMixin.js b/src/mixins/CommentMixin.js
new file mode 100644
index 000000000..012d0d18d
--- /dev/null
+++ b/src/mixins/CommentMixin.js
@@ -0,0 +1,117 @@
+/**
+ * @copyright Copyright (c) 2020 John Molakvoæ
+ *
+ * @author John Molakvoæ
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * 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 .
+ *
+ */
+
+import NewComment from '../services/NewComment.js'
+import DeleteComment from '../services/DeleteComment.js'
+import EditComment from '../services/EditComment.js'
+import { showError, showUndo, TOAST_UNDO_TIMEOUT } from '@nextcloud/dialogs'
+
+export default {
+ props: {
+ id: {
+ type: Number,
+ default: null,
+ },
+ message: {
+ type: String,
+ 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)
+ const newComment = await NewComment('files', 3, 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'))
+ } finally {
+ this.loading = false
+ }
+ },
+ },
+}
diff --git a/src/services/CommentsInstance.js b/src/services/CommentsInstance.js
new file mode 100644
index 000000000..e283e833c
--- /dev/null
+++ b/src/services/CommentsInstance.js
@@ -0,0 +1,68 @@
+/**
+ * @copyright Copyright (c) 2020 John Molakvoæ
+ *
+ * @author John Molakvoæ
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * 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 .
+ *
+ */
+
+import { translate as t, translatePlural as n } from '@nextcloud/l10n'
+import { getRequestToken } from '@nextcloud/auth'
+import Vue from 'vue'
+import CommentsApp from '../views/Comments.vue'
+import logger from '../logger.js'
+
+// eslint-disable-next-line camelcase
+__webpack_nonce__ = btoa(getRequestToken())
+
+// 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)
+ }
+
+}
diff --git a/src/services/DavClient.js b/src/services/DavClient.js
new file mode 100644
index 000000000..5c2fc96e4
--- /dev/null
+++ b/src/services/DavClient.js
@@ -0,0 +1,37 @@
+/**
+ * @copyright Copyright (c) 2021 John Molakvoæ
+ *
+ * @author John Molakvoæ
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * 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 .
+ *
+ */
+
+import { createClient } from 'webdav'
+import { getRootPath } from '../utils/davUtils.js'
+import { getRequestToken } from '@nextcloud/auth'
+
+// init webdav client
+const client = createClient(getRootPath(), {
+ headers: {
+ // Add this so the server knows it is an request from the browser
+ 'X-Requested-With': 'XMLHttpRequest',
+ // Inject user auth
+ requesttoken: getRequestToken() ?? '',
+ },
+})
+
+export default client
diff --git a/src/services/DeleteComment.js b/src/services/DeleteComment.js
new file mode 100644
index 000000000..43d53129f
--- /dev/null
+++ b/src/services/DeleteComment.js
@@ -0,0 +1,37 @@
+/**
+ * @copyright Copyright (c) 2020 John Molakvoæ
+ *
+ * @author John Molakvoæ
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * 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 .
+ *
+ */
+
+import client from './DavClient.js'
+
+/**
+ * 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)
+}
diff --git a/src/services/EditComment.js b/src/services/EditComment.js
new file mode 100644
index 000000000..51d0d4cca
--- /dev/null
+++ b/src/services/EditComment.js
@@ -0,0 +1,49 @@
+/**
+ * @copyright Copyright (c) 2020 John Molakvoæ
+ *
+ * @author John Molakvoæ
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * 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 .
+ *
+ */
+
+import client from './DavClient.js'
+
+/**
+ * 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: `
+
+
+
+ ${message}
+
+
+ `,
+ }))
+}
diff --git a/src/services/GetComments.ts b/src/services/GetComments.ts
new file mode 100644
index 000000000..d74e92bce
--- /dev/null
+++ b/src/services/GetComments.ts
@@ -0,0 +1,83 @@
+/**
+ * @copyright Copyright (c) 2020 John Molakvoæ
+ *
+ * @author John Molakvoæ
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * 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 .
+ *
+ */
+
+import { parseXML, type DAVResult, type FileStat } from 'webdav'
+
+// https://github.com/perry-mitchell/webdav-client/issues/339
+import { processResponsePayload } from '../../../../node_modules/webdav/dist/node/response.js'
+import { prepareFileFromProps } from '../../../../node_modules/webdav/dist/node/tools/dav.js'
+import client from './DavClient.js'
+
+export const DEFAULT_LIMIT = 20
+
+/**
+ * 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
+ * @param {number} [options.offset] the pagination offset
+ * @return {object[]} the comments list
+ */
+export const getComments = async function({ commentsType, ressourceId }, options: { offset: number }) {
+ const ressourcePath = ['', commentsType, ressourceId].join('/')
+
+ const response = await client.customRequest(ressourcePath, Object.assign({
+ method: 'REPORT',
+ data: `
+
+ ${DEFAULT_LIMIT}
+ ${options.offset || 0}
+ `,
+ }, options))
+
+ const responseData = await response.text()
+ const result = await parseXML(responseData)
+ const stat = getDirectoryFiles(result, true)
+ return processResponsePayload(response, stat, true)
+}
+
+// https://github.com/perry-mitchell/webdav-client/blob/8d9694613c978ce7404e26a401c39a41f125f87f/source/operations/directoryContents.ts
+const getDirectoryFiles = function(
+ result: DAVResult,
+ isDetailed = false,
+): Array {
+ // Extract the response items (directory contents)
+ const {
+ multistatus: { response: responseItems },
+ } = result
+
+ // Map all items to a consistent output structure (results)
+ return responseItems.map(item => {
+ // Each item should contain a stat object
+ const {
+ propstat: { prop: props },
+ } = item
+
+ return prepareFileFromProps(props, props.id.toString(), isDetailed)
+ })
+}
diff --git a/src/services/NewComment.js b/src/services/NewComment.js
new file mode 100644
index 000000000..a7fb58e32
--- /dev/null
+++ b/src/services/NewComment.js
@@ -0,0 +1,67 @@
+/**
+ * @copyright Copyright (c) 2020 John Molakvoæ
+ *
+ * @author John Molakvoæ
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * 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 .
+ *
+ */
+
+import { getCurrentUser } from '@nextcloud/auth'
+import { getRootPath } from '../utils/davUtils.js'
+import { decodeHtmlEntities } from '../utils/decodeHtmlEntities.js'
+import axios from '@nextcloud/axios'
+import client from './DavClient.js'
+
+/**
+ * Retrieve the comments list
+ *
+ * @param {string} commentsType the ressource type
+ * @param {number} ressourceId the ressource ID
+ * @param {string} message the message
+ * @return {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,
+ })
+
+ const props = comment.data.props
+ // Decode twice to handle potentially double-encoded entities
+ // FIXME Remove this once https://github.com/nextcloud/server/issues/29306
+ // is resolved
+ props.actorDisplayName = decodeHtmlEntities(props.actorDisplayName, 2)
+ props.message = decodeHtmlEntities(props.message, 2)
+
+ return comment.data
+}
diff --git a/src/services/ReadComments.ts b/src/services/ReadComments.ts
new file mode 100644
index 000000000..4c7e44fe2
--- /dev/null
+++ b/src/services/ReadComments.ts
@@ -0,0 +1,55 @@
+/**
+ * @copyright 2023 Christopher Ng
+ *
+ * @author Christopher Ng
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * 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 .
+ *
+ */
+
+import client from './DavClient.js'
+
+import type { Response } from 'webdav'
+
+/**
+ * Mark comments older than the date timestamp as read
+ *
+ * @param commentsType the ressource type
+ * @param ressourceId the ressource ID
+ * @param date the date object
+ */
+export const markCommentsAsRead = (
+ commentsType: string,
+ ressourceId: number,
+ date: Date,
+): Promise => {
+ const ressourcePath = ['', commentsType, ressourceId].join('/')
+ const readMarker = date.toUTCString()
+
+ return client.customRequest(ressourcePath, {
+ method: 'PROPPATCH',
+ data: `
+
+
+
+ ${readMarker}
+
+
+ `,
+ })
+}
diff --git a/src/sidebar.js b/src/sidebar.js
index b56852890..8415afe57 100644
--- a/src/sidebar.js
+++ b/src/sidebar.js
@@ -68,9 +68,11 @@ const activityTab = new OCA.Files.Sidebar.Tab({
})
window.addEventListener('DOMContentLoaded', async function() {
+ console.debug('OCA.Activity initializing')
if (OCA.Files && OCA.Files.Sidebar) {
OCA.Files.Sidebar.registerTab(activityTab)
const { default: ActivityTab } = await import(/* webpackPreload: true */ './views/ActivityTab.vue')
ActivityTabView = ActivityTabView ?? Vue.extend(ActivityTab)
+ console.debug('OCA.Activity initialized')
}
})
diff --git a/src/utils/cancelableRequest.js b/src/utils/cancelableRequest.js
new file mode 100644
index 000000000..1973de389
--- /dev/null
+++ b/src/utils/cancelableRequest.js
@@ -0,0 +1,53 @@
+/**
+ * @copyright Copyright (c) 2020 John Molakvoæ
+ *
+ * @author John Molakvoæ
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * 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 .
+ *
+ */
+
+/**
+ * Creates a cancelable axios 'request object'.
+ *
+ * @param {Function} request the axios promise request
+ * @return {object}
+ */
+const cancelableRequest = function(request) {
+ const controller = new AbortController()
+ const signal = controller.signal
+
+ /**
+ * 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) {
+ const response = await request(
+ url,
+ Object.assign({ signal }, options)
+ )
+ return response
+ }
+
+ return {
+ request: fetch,
+ abort: () => controller.abort(),
+ }
+}
+
+export default cancelableRequest
diff --git a/src/utils/davUtils.js b/src/utils/davUtils.js
new file mode 100644
index 000000000..1a2686bbd
--- /dev/null
+++ b/src/utils/davUtils.js
@@ -0,0 +1,29 @@
+/**
+ * @copyright Copyright (c) 2020 John Molakvoæ
+ *
+ * @author John Molakvoæ
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * 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 .
+ *
+ */
+
+import { generateRemoteUrl } from '@nextcloud/router'
+
+const getRootPath = function() {
+ return generateRemoteUrl('dav/comments')
+}
+
+export { getRootPath }
diff --git a/js/activity-389.js.LICENSE.txt b/src/utils/decodeHtmlEntities.js
similarity index 59%
rename from js/activity-389.js.LICENSE.txt
rename to src/utils/decodeHtmlEntities.js
index 0d1e018ae..60c08163f 100644
--- a/js/activity-389.js.LICENSE.txt
+++ b/src/utils/decodeHtmlEntities.js
@@ -1,23 +1,7 @@
-/*!
- * Determine if an object is a Buffer
- *
- * @author Feross Aboukhadijeh
- * @license MIT
- */
-
-/*!
- * The buffer module from node.js, for the browser.
- *
- * @author Feross Aboukhadijeh
- * @license MIT
- */
-
-/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */
-
/**
- * @copyright Copyright (c) 2021 Louis Chemineau
+ * @copyright Copyright (c) 2021 Christopher Ng
*
- * @author Louis Chemineau
+ * @author Christopher Ng
*
* @license AGPL-3.0-or-later
*
@@ -36,6 +20,15 @@
*
*/
-//! moment.js
-
-//! moment.js locale configuration
+/**
+ * @param {any} value -
+ * @param {any} passes -
+ */
+export function decodeHtmlEntities(value, passes = 1) {
+ const parser = new DOMParser()
+ let decoded = value
+ for (let i = 0; i < passes; i++) {
+ decoded = parser.parseFromString(decoded, 'text/html').documentElement.textContent
+ }
+ return decoded
+}
diff --git a/src/utils/numberUtil.js b/src/utils/numberUtil.js
new file mode 100644
index 000000000..cbbd69640
--- /dev/null
+++ b/src/utils/numberUtil.js
@@ -0,0 +1,30 @@
+/**
+ * @copyright Copyright (c) 2020 John Molakvoæ
+ *
+ * @author John Molakvoæ
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * 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 .
+ *
+ */
+
+const isNumber = function(num) {
+ if (!num) {
+ return false
+ }
+ return Number(num).toString() === num.toString()
+}
+
+export { isNumber }
diff --git a/src/views/ActivityTab.vue b/src/views/ActivityTab.vue
index 1969e4512..d8142d109 100644
--- a/src/views/ActivityTab.vue
+++ b/src/views/ActivityTab.vue
@@ -2,6 +2,7 @@
- @copyright Copyright (c) 2021 Louis Chemineau
-
- @author Louis Chemineau
+ - @author Stephan Orbaugh
-
- @license AGPL-3.0-or-later
-
@@ -22,6 +23,15 @@
+
+
+
@@ -48,6 +58,7 @@