diff --git a/.eslintrc.js b/.eslintrc.js index 89bcc517..e1468702 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,8 +1,8 @@ module.exports = { extends: [ - '@nextcloud' + '@nextcloud', ], globals: { - appName: true - } -}; + appName: true, + }, +} diff --git a/.github/workflows/lint-eslint.yml b/.github/workflows/lint-eslint.yml new file mode 100644 index 00000000..c08763ea --- /dev/null +++ b/.github/workflows/lint-eslint.yml @@ -0,0 +1,46 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization + +name: Lint + +on: pull_request + +permissions: + contents: read + +concurrency: + group: lint-eslint-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + + name: eslint + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Read package.json node and npm engines version + uses: skjnldsv/read-package-engines-version-actions@v1.2 + id: versions + with: + fallbackNode: '^12' + fallbackNpm: '^6' + + - name: Set up node ${{ steps.versions.outputs.nodeVersion }} + uses: actions/setup-node@v3 + with: + node-version: ${{ steps.versions.outputs.nodeVersion }} + + - name: Set up npm ${{ steps.versions.outputs.npmVersion }} + run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}" + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint diff --git a/.github/workflows/lint-stylelint.yml b/.github/workflows/lint-stylelint.yml new file mode 100644 index 00000000..17b7aebb --- /dev/null +++ b/.github/workflows/lint-stylelint.yml @@ -0,0 +1,46 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization + +name: Lint + +on: pull_request + +permissions: + contents: read + +concurrency: + group: lint-stylelint-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + + name: stylelint + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Read package.json node and npm engines version + uses: skjnldsv/read-package-engines-version-actions@v1.2 + id: versions + with: + fallbackNode: '^12' + fallbackNpm: '^6' + + - name: Set up node ${{ steps.versions.outputs.nodeVersion }} + uses: actions/setup-node@v3 + with: + node-version: ${{ steps.versions.outputs.nodeVersion }} + + - name: Set up npm ${{ steps.versions.outputs.npmVersion }} + run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}" + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run stylelint diff --git a/src/components/Composer/Composer.vue b/src/components/Composer/Composer.vue index fc33ac23..c5bb230c 100644 --- a/src/components/Composer/Composer.vue +++ b/src/components/Composer/Composer.vue @@ -126,7 +126,7 @@ - + {{ postTo }} diff --git a/src/components/Emoji.vue b/src/components/Emoji.vue index 9964413f..a4eab5ae 100644 --- a/src/components/Emoji.vue +++ b/src/components/Emoji.vue @@ -22,12 +22,19 @@ export default { return {} }, computed: { + /** + * @return {string} + */ icon() { return twemoji.convert.toCodePoint(this.emoji.indexOf(U200D) < 0 ? this.emoji.replace(UFE0Fg, '') : this.emoji ) }, + + /** + * @return {string} + */ emojiUrl() { return generateFilePath('social', 'img', 'twemoji/' + this.icon + '.svg') }, diff --git a/src/components/MessageContent.js b/src/components/MessageContent.js index c297c8d3..ff8ccbac 100644 --- a/src/components/MessageContent.js +++ b/src/components/MessageContent.js @@ -1,6 +1,12 @@ import Vue from 'vue' import Emoji from './Emoji.vue' +/** + * @typedef {object} MessageSource + * @property {Array} tag + * @property {string} content + */ + export default Vue.component('MessageContent', { props: { source: { @@ -23,8 +29,8 @@ export default Vue.component('MessageContent', { * * All attributes other than `href` for links are stripped from the source * - * @param createElement - * @param source + * @param {Function} createElement + * @param {MessageSource} source */ export function formatMessage(createElement, source) { if (!source.tag) { @@ -42,9 +48,9 @@ export function formatMessage(createElement, source) { /** * - * @param createElement - * @param node - * @param context + * @param {Function} createElement + * @param {HTMLElement} node + * @param {object} context */ function domToVue(createElement, node, context) { switch (node.tagName) { @@ -55,9 +61,10 @@ function domToVue(createElement, node, context) { case 'SPAN': return cleanCopy(createElement, node, context) case 'A': + // @ts-ignore - if tagName === 'A' then node is instance of HTMLLinkElement return cleanLink(createElement, node, context) default: - return transformText(createElement, node.textContent) + return transformText(createElement, node.textContent ?? '') } } @@ -66,8 +73,8 @@ const hashTagRegex = /(\W|^)(#\w+)/i /** * - * @param createElement - * @param text + * @param {Function} createElement + * @param {string} text */ function transformText(createElement, text) { return transformTextRegex(text, [ @@ -124,9 +131,9 @@ function transformText(createElement, text) { /** * copy a node without any attributes and cleaning all children * - * @param createElement - * @param node - * @param context + * @param {Function} createElement + * @param {HTMLElement} node + * @param {object} context */ function cleanCopy(createElement, node, context) { const children = Array.from(node.childNodes).map(node => domToVue(createElement, node, context)) @@ -135,17 +142,18 @@ function cleanCopy(createElement, node, context) { /** * - * @param createElement - * @param node - * @param context + * @param {Function} createElement + * @param {HTMLLinkElement} node + * @param {object} context + * @param {Array} context.mentions */ function cleanLink(createElement, node, context) { const type = getLinkType(node.className) const attributes = {} + const tag = matchMention(context.mentions, node.getAttribute('href') ?? '', node.textContent ?? '') switch (type) { case 'mention': - var tag = matchMention(context.mentions, node.getAttribute('href'), node.textContent) if (tag) { attributes.rel = 'nofollow noopener noreferrer' attributes.target = '_blank' @@ -163,7 +171,7 @@ function cleanLink(createElement, node, context) { props: { to: { name: 'tags', - params: { tag: node.textContent.slice(1) }, + params: { tag: node.textContent?.slice(1) }, }, }, }, @@ -180,7 +188,7 @@ function cleanLink(createElement, node, context) { /** * - * @param className + * @param {string} className */ function getLinkType(className) { const parts = className.split(' ') @@ -195,9 +203,9 @@ function getLinkType(className) { /** * - * @param tags - * @param mentionHref - * @param mentionText + * @param {Array} tags + * @param {string} mentionHref + * @param {string} mentionText */ function matchMention(tags, mentionHref, mentionText) { const mentionUrl = new URL(mentionHref) @@ -225,8 +233,8 @@ const emojiRe = /(?:\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68 /** * - * @param text - * @param handlers + * @param {string} text + * @param {Array} handlers */ function transformTextRegex(text, handlers) { const parts = [] diff --git a/src/components/TimelinePost.vue b/src/components/TimelinePost.vue index ccbd06fb..05809747 100644 --- a/src/components/TimelinePost.vue +++ b/src/components/TimelinePost.vue @@ -91,7 +91,6 @@ import HeartOutline from 'vue-material-design-icons/HeartOutline.vue' import logger from '../services/logger.js' import moment from '@nextcloud/moment' import { generateUrl } from '@nextcloud/router' -import RichText from '@nextcloud/vue-richtext' import MessageContent from './MessageContent.js' export default { @@ -105,7 +104,6 @@ export default { Reply, Heart, HeartOutline, - RichText, MessageContent, }, mixins: [currentUser], @@ -154,7 +152,7 @@ export default { }, methods: { /** - * @param e + * @param {MouseEvent} e - The click event * @function getSinglePostTimeline * @description Opens the timeline of the post clicked */ diff --git a/src/directives/focusOnCreate.js b/src/directives/focusOnCreate.js index c68568bd..e61b6fdc 100644 --- a/src/directives/focusOnCreate.js +++ b/src/directives/focusOnCreate.js @@ -1,9 +1,9 @@ -/* +/** * @copyright Copyright (c) 2018 Julius Härtl * * @author Julius Härtl * - * @license GNU AGPL version 3 or any later version + * @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 @@ -20,11 +20,11 @@ * */ -import Vue from 'vue' +import { nextTick } from 'vue' export default { bind(el) { - Vue.nextTick(() => { + nextTick(() => { el.focus() }) }, diff --git a/src/main.js b/src/main.js index deee8a7c..979e8e7b 100644 --- a/src/main.js +++ b/src/main.js @@ -3,7 +3,7 @@ * * @author John Molakvoæ * - * @license GNU AGPL version 3 or any later version + * @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 diff --git a/src/mixins/accountMixins.js b/src/mixins/accountMixins.js index a4805860..238eb97e 100644 --- a/src/mixins/accountMixins.js +++ b/src/mixins/accountMixins.js @@ -1,9 +1,9 @@ -/* +/** * @copyright Copyright (c) 2019 Cyrille Bollu * * @author Cyrille Bollu * - * @license GNU AGPL version 3 or any later version + * @license AGPL-3.0-or-later * * @file provides global account related methods * @@ -31,16 +31,18 @@ export default { serverData, ], computed: { - /** @function Returns the complete account name */ + /** @return {string} the complete account name */ profileAccount() { return (this.uid.indexOf('@') === -1) ? this.uid + '@' + this.hostname : this.uid }, - /** @functions Returns detailed information about an account (account must be loaded in the store first) */ + + /** @return detailed information about an account (account must be loaded in the store first) */ accountInfo() { return this.$store.getters.getAccount(this.profileAccount) }, + /** - * @function Somewhat duplicate with accountInfo(), but needed (for some reason) to avoid glitches + * Somewhat duplicate with accountInfo(), but needed (for some reason) to avoid glitches * where components would first show "user not found" before display an account's account info */ accountLoaded() { diff --git a/src/mixins/serverData.js b/src/mixins/serverData.js index d0b60bda..d7bebb2b 100644 --- a/src/mixins/serverData.js +++ b/src/mixins/serverData.js @@ -1,4 +1,4 @@ -/* +/** * @copyright Copyright (c) 2018 Julius Härtl * * @file Provides global methods for using the serverData structure. @@ -7,7 +7,7 @@ * * @author Julius Härtl * - * @license GNU AGPL version 3 or any later version + * @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 @@ -24,18 +24,22 @@ * */ +/** + * @typedef {object} ServerData + * @property {string} account - The account that the user wants to follow (Only in 'OStatus.vue') + * @property {string} cliUrl + * @property {string} cloudAddress + * @property {boolean} firstrun + * @property {boolean} isAdmin + * @property {string} local - The local part of the account that the user wants to follow + * @property {boolean} public - False when the page is accessed by an authenticated user. True otherwise + * @property setup + */ + export default { computed: { /** - * @description Returns the serverData object - * @property {string} account - The account that the user wants to follow (Only in 'OStatus.vue') - * @property cliUrl - * @property cloudAddress - * @property firstrun - * @property isAdmin - * @property {string} local - The local part of the account that the user wants to follow - * @property {boolean} public - False when the page is accessed by an authenticated user. True otherwise - * @property setup + * @return {Partial} Returns the serverData object */ serverData() { if (!this.$store) { diff --git a/src/oauth.js b/src/oauth.js index 7138183e..9d4120a3 100644 --- a/src/oauth.js +++ b/src/oauth.js @@ -1,6 +1,7 @@ /** * @copyright 2022 Carl Schwan - * @license GNU AGPL version 3 or any later version + * + * @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 diff --git a/src/router.js b/src/router.js index ab82bbfd..08af4fef 100644 --- a/src/router.js +++ b/src/router.js @@ -5,7 +5,7 @@ * @author Julius Härtl * @author John Molakvoæ * - * @license GNU AGPL version 3 or any later version + * @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 diff --git a/src/store/account.js b/src/store/account.js index 7f35e00a..a4dee30f 100644 --- a/src/store/account.js +++ b/src/store/account.js @@ -1,9 +1,9 @@ -/* +/** * @copyright Copyright (c) 2018 Julius Härtl * * @author Julius Härtl * - * @license GNU AGPL version 3 or any later version + * @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 @@ -21,7 +21,7 @@ */ import axios from '@nextcloud/axios' -import Vue from 'vue' +import { set } from 'vue' import { generateUrl } from '@nextcloud/router' const state = { @@ -30,7 +30,7 @@ const state = { accountIdMap: {}, } const addAccount = (state, { actorId, data }) => { - Vue.set(state.accounts, actorId, Object.assign({ + set(state.accounts, actorId, Object.assign({ followersList: [], followingList: [], details: { @@ -38,7 +38,7 @@ const addAccount = (state, { actorId, data }) => { follower: false, }, }, state.accounts[actorId], data)) - Vue.set(state.accountIdMap, data.account, data.id) + set(state.accountIdMap, data.account, data.id) } const _getActorIdForAccount = (account) => state.accountIdMap[account] @@ -61,7 +61,7 @@ const mutations = { }) } } - Vue.set(state.accounts[_getActorIdForAccount(account)], 'followersList', users) + set(state.accounts[_getActorIdForAccount(account)], 'followersList', users) }, addFollowing(state, { account, data }) { const users = [] @@ -75,13 +75,13 @@ const mutations = { }) } } - Vue.set(state.accounts[_getActorIdForAccount(account)], 'followingList', users) + set(state.accounts[_getActorIdForAccount(account)], 'followingList', users) }, followAccount(state, accountToFollow) { - Vue.set(state.accounts[_getActorIdForAccount(accountToFollow)].details, 'following', true) + set(state.accounts[_getActorIdForAccount(accountToFollow)].details, 'following', true) }, unfollowAccount(state, accountToUnfollow) { - Vue.set(state.accounts[_getActorIdForAccount(accountToUnfollow)].details, 'following', false) + set(state.accounts[_getActorIdForAccount(accountToUnfollow)].details, 'following', false) }, } diff --git a/src/store/index.js b/src/store/index.js index c51e562e..40b51949 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,10 +1,10 @@ -/* +/** * @copyright Copyright (c) 2018 John Molakvoæ * * @author John Molakvoæ * @author Julius Härtl * - * @license GNU AGPL version 3 or any later version + * @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 @@ -22,7 +22,7 @@ */ import Vue from 'vue' -import Vuex from 'vuex' +import Vuex, { Store } from 'vuex' import timeline from './timeline.js' import account from './account.js' import settings from './settings.js' @@ -31,7 +31,7 @@ Vue.use(Vuex) const debug = process.env.NODE_ENV !== 'production' -export default new Vuex.Store({ +export default new Store({ modules: { timeline, account, diff --git a/src/store/settings.js b/src/store/settings.js index 87c66e8b..8616e5cc 100644 --- a/src/store/settings.js +++ b/src/store/settings.js @@ -36,6 +36,5 @@ const getters = { return state.serverData }, } -const actions = {} -export default { state, mutations, getters, actions } +export default { state, mutations, getters } diff --git a/src/store/timeline.js b/src/store/timeline.js index 9ea012ed..0a14fe98 100644 --- a/src/store/timeline.js +++ b/src/store/timeline.js @@ -1,4 +1,4 @@ -/* +/** * @copyright Copyright (c) 2018 Julius Härtl * * @file Timeline related store @@ -6,7 +6,7 @@ * @author Julius Härtl * @author Jonas Sulzer * - * @license GNU AGPL version 3 or any later version + * @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 @@ -30,7 +30,7 @@ import { generateUrl } from '@nextcloud/router' /** * @property {object} timeline - The posts' collection - * @property {int} since - Time (EPOCH) of the most recent post + * @property {number} since - Time (EPOCH) of the most recent post * @property {string} type - Timeline's type: 'home', 'single-post',... * @property {object} params - Timeline's parameters * @property {string} account - diff --git a/src/views/OAuth2Authorize.vue b/src/views/OAuth2Authorize.vue index 01041d5a..7ce535d9 100644 --- a/src/views/OAuth2Authorize.vue +++ b/src/views/OAuth2Authorize.vue @@ -28,7 +28,7 @@ name="requesttoken" :value="OC.requestToken">
- + {{ t('social', 'Authorize') }} diff --git a/src/views/Timeline.vue b/src/views/Timeline.vue index 8e4585e8..1dcf9dd3 100644 --- a/src/views/Timeline.vue +++ b/src/views/Timeline.vue @@ -4,7 +4,7 @@