diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 3de57a360..000000000 --- a/.dockerignore +++ /dev/null @@ -1,12 +0,0 @@ -node_modules/ -dist/ -logs/ -.DS_Store -.git/ -config/local.json -pleroma-backend/ -test/e2e/reports/ -test/e2e-playwright/test-results/ -test/e2e-playwright/playwright-report/ -__screenshots__/ - diff --git a/.gitignore b/.gitignore index c4a96ee1e..01ffda9a8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,11 +4,8 @@ dist/ npm-debug.log test/unit/coverage test/e2e/reports -test/e2e-playwright/test-results -test/e2e-playwright/playwright-report selenium-debug.log .idea/ -.gitlab-ci-local/ config/local.json src/assets/emoji.json logs/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 06fbf45f9..99c85dd36 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -34,23 +34,12 @@ check-changelog: - apk add git - sh ./tools/check-changelog -lint-eslint: +lint: stage: lint script: - yarn - - yarn ci-eslint - -lint-biome: - stage: lint - script: - - yarn - - yarn ci-biome - -lint-stylelint: - stage: lint - script: - - yarn - - yarn ci-stylelint + - yarn lint + - yarn stylelint test: stage: test @@ -71,135 +60,6 @@ test: - test/**/__screenshots__ when: on_failure -e2e-pleroma: - stage: test - image: mcr.microsoft.com/playwright:v1.57.0-jammy - services: - - name: postgres:15-alpine - alias: db - - name: $PLEROMA_IMAGE - alias: pleroma - entrypoint: ["/bin/ash", "-c"] - command: - - | - set -eu - - SEED_SENTINEL_PATH=/var/lib/pleroma/.e2e_seeded - CONFIG_OVERRIDE_PATH=/var/lib/pleroma/config.exs - - echo '-- Waiting for database...' - while ! pg_isready -U ${DB_USER:-pleroma} -d postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma} -t 1; do - sleep 1s - done - - echo '-- Writing E2E config overrides...' - cat > $CONFIG_OVERRIDE_PATH </dev/null; then - kill -TERM $PLEROMA_PID - wait $PLEROMA_PID || true - fi - } - - trap cleanup INT TERM - - echo '-- Waiting for API...' - api_ok=false - for _i in $(seq 1 120); do - if wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null 2>&1; then - api_ok=true - break - fi - sleep 1s - done - - if [ $api_ok != true ]; then - echo 'Timed out waiting for Pleroma API to become available' - exit 1 - fi - - if [ ! -f $SEED_SENTINEL_PATH ]; then - if [ -n ${E2E_ADMIN_USERNAME:-} ] && [ -n ${E2E_ADMIN_PASSWORD:-} ] && [ -n ${E2E_ADMIN_EMAIL:-} ]; then - echo '-- Seeding admin user' $E2E_ADMIN_USERNAME '...' - if ! /opt/pleroma/bin/pleroma_ctl user new $E2E_ADMIN_USERNAME $E2E_ADMIN_EMAIL --admin --password $E2E_ADMIN_PASSWORD -y; then - echo '-- User already exists or creation failed, ensuring admin + confirmed...' - /opt/pleroma/bin/pleroma_ctl user set $E2E_ADMIN_USERNAME --admin --confirmed - fi - else - echo '-- Skipping admin seeding (missing E2E_ADMIN_* env)' - fi - - touch $SEED_SENTINEL_PATH - fi - - wait $PLEROMA_PID - tags: - - amd64 - - himem - variables: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" - FF_NETWORK_PER_BUILD: "true" - PLEROMA_IMAGE: git.pleroma.social:5050/pleroma/pleroma:stable - POSTGRES_USER: pleroma - POSTGRES_PASSWORD: pleroma - POSTGRES_DB: pleroma - DB_USER: pleroma - DB_PASS: pleroma - DB_NAME: pleroma - DB_HOST: db - DB_PORT: 5432 - DOMAIN: localhost - INSTANCE_NAME: Pleroma E2E - E2E_ADMIN_USERNAME: admin - E2E_ADMIN_PASSWORD: adminadmin - E2E_ADMIN_EMAIL: admin@example.com - ADMIN_EMAIL: $E2E_ADMIN_EMAIL - NOTIFY_EMAIL: $E2E_ADMIN_EMAIL - VITE_PROXY_TARGET: http://pleroma:4000 - VITE_PROXY_ORIGIN: http://localhost:4000 - E2E_BASE_URL: http://localhost:8080 - script: - - npm install -g yarn@1.22.22 - - yarn --frozen-lockfile - - | - echo "-- Waiting for Pleroma API..." - api_ok="false" - for _i in $(seq 1 120); do - if wget -qO- http://pleroma:4000/api/v1/instance >/dev/null 2>&1; then - api_ok="true" - break - fi - sleep 1s - done - if [ "$api_ok" != "true" ]; then - echo "Timed out waiting for Pleroma API to become available" - exit 1 - fi - - yarn e2e:pw - artifacts: - when: on_failure - paths: - - test/e2e-playwright/test-results - - test/e2e-playwright/playwright-report - build: stage: build tags: diff --git a/.gitlab/merge_request_templates/Release.md b/.gitlab/merge_request_templates/Release.md deleted file mode 100644 index d02e14a73..000000000 --- a/.gitlab/merge_request_templates/Release.md +++ /dev/null @@ -1,8 +0,0 @@ -### Release checklist -* [ ] Bump version in `package.json` -* [ ] Compile a changelog with the `tools/collect-changelog` script -* [ ] Create an MR with an announcement to pleroma.social -#### post-merge -* [ ] Tag the release on the merge commit -* [ ] Make the tag into a Gitlab Releaseā„¢ -* [ ] Merge `master` into `develop` (in case the fixes are already in develop, use `git merge -s ours --no-commit` and manually merge the changelogs) diff --git a/.stylelintrc.json b/.stylelintrc.json index afdfd5f5b..c91107595 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -12,8 +12,6 @@ "custom-property-pattern": null, "keyframes-name-pattern": null, "scss/operator-no-newline-after": null, - "declaration-property-value-no-unknown": true, - "scss/declaration-property-value-no-unknown": true, "declaration-block-no-redundant-longhand-properties": [ true, { diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eb5a9cb4..c2f0e7d17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,76 +2,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - -## 2.10.1 -### Fixed -- fixed being unable to set actor type from profile page -- fixed error when clicking mute menu itself (instead of submenu items) -- fixed mute -> domain status submenu not working - -### Internal -- Add playwright E2E-tests with an optional docker-based backend - -## 2.10.0 -### Changed -- Temporary changes modal now shows actual countdown instead of fixed timeout -- Disabled elements are more disabled now -- Rearranged and split settings to make more sense and be less of a wall of text -- On mobile settings now take up full width and presented in navigation style -improved styles for settings - -### Added -- Most of the remaining AdminFE tabs were added into Admin Dashboard -- It's now possible to customize PWA Manfiest from PleromaFE -- Make every configuration option default-overridable by instance admins - -### Fixed -- Fixed settings not appearing if user never touched "show advanced" toggle -- Fix display of the broken/deleted/banned users -- Fixed incorrect emoji display in post interaction lists -- Fixed list title not being saved when editing -- Fixed poll notifications not being expandable - - -## 2.9.3 -### Fixed -- Being unable to update profile - -## 2.9.2 -### Changed -- BREAKING: due to some internal technical changes logging into AdminFE through PleromaFE is no longer possible -- User card/profile got an overhaul -- Profile editing overhaul -- Visually combined subject and content fields in post form -- Moved post form's emoji button into input field -- Minor visual changes and fixes -- Clicking on fav/rt/emoji notifications' contents expands/collapses it -- Reduced time taken processing theme by half -- Splash screen only appears if loading takes more than 2 seconds - -### Added -- Mutes received an update, adding support for regex, muting based on username and expiration time. -- Mutes are now synchronized across sessions -- Support for expiring mutes and blocks (if available) -- Clicking on emoji shows bigger version of it alongside with its shortcode - - Admins also are able to copy it into a local pack -- Added support for Akkoma and IceShrimp.NET backends -- Compatibility with stricter CSP (Akkoma backend) -- Added a way to upload new packs from a URL or ZIP file via the Admin Dashboard -- Unify show/hide content buttons -- Add support for detachable scrollTop button -- Option to left-align user bio -- Cache assets and emojis with service worker -- Indicate currently active V3 theme as a body element class -- Add arithmetic blend ISS function - -### Fixed -- Display counter for status action buttons when they are in the menu -- Fix bookmark button alignment in the extra actions menu -- Instance favicons are no longer stretched -- A lot more scalable UI fixes - - Emoji picker now should work fine when emoji size is increased - ## 2.8.0 ### Changed - BREAKING: static/img/nsfw.2958239.png is now static/img/nsfw.DepQPhG0.png, which may affect people who specify exactly this path as the cover image @@ -104,8 +34,8 @@ This does not guarantee that browsers will or will not work. - Support displaying time in absolute format - Add draft management system - Compress most kinds of images on upload. -- Added option to always convert images to JPEG format instead of using WebP when compressing images. -- Added configurable image compression option in general settings, allowing users to control whether images are compressed before upload. +- Added option to always convert images to JPEG format instead of using WebP when compressing images. +- Added configurable image compression option in general settings, allowing users to control whether images are compressed before upload. - Inform users that Smithereen public polls are public - Splash screen + loading indicator to make process of identifying initialization issues and load performance - UI for making v3 themes and palettes, support for bundling v3 themes diff --git a/biome.json b/biome.json deleted file mode 100644 index 6a464a0e5..000000000 --- a/biome.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true - }, - "files": { - "includes": ["**", "!!**/dist", "!!tools/emojis.json"] - }, - "formatter": { - "enabled": true, - "indentStyle": "space" - }, - "linter": { - "enabled": true, - "domains": { - "vue": "recommended" - }, - "rules": { - "recommended": false, - "complexity": { - "noAdjacentSpacesInRegex": "error", - "noExtraBooleanCast": "error", - "noUselessCatch": "error", - "noUselessEscapeInRegex": "error" - }, - "correctness": { - "noConstAssign": "error", - "noConstantCondition": "error", - "noEmptyCharacterClassInRegex": "error", - "noEmptyPattern": "error", - "noGlobalObjectCalls": "error", - "noInvalidBuiltinInstantiation": "error", - "noInvalidConstructorSuper": "error", - "noNonoctalDecimalEscape": "error", - "noPrecisionLoss": "error", - "noSelfAssign": "error", - "noSetterReturn": "error", - "noSwitchDeclarations": "error", - "noUndeclaredVariables": "error", - "noUnreachable": "error", - "noUnreachableSuper": "error", - "noUnsafeFinally": "error", - "noUnsafeOptionalChaining": "error", - "noUnusedLabels": "error", - "noUnusedPrivateClassMembers": "error", - "noUnusedVariables": "error", - "useIsNan": "error", - "useValidForDirection": "error", - "useValidTypeof": "error", - "useYield": "error" - }, - "suspicious": { - "noAsyncPromiseExecutor": "error", - "noCatchAssign": "error", - "noClassAssign": "error", - "noCompareNegZero": "error", - "noConstantBinaryExpressions": "error", - "noControlCharactersInRegex": "error", - "noDebugger": "error", - "noDuplicateCase": "error", - "noDuplicateClassMembers": "error", - "noDuplicateElseIf": "error", - "noDuplicateObjectKeys": "error", - "noDuplicateParameters": "error", - "noEmptyBlockStatements": "error", - "noFallthroughSwitchClause": "error", - "noFunctionAssign": "error", - "noGlobalAssign": "error", - "noImportAssign": "error", - "noIrregularWhitespace": "error", - "noMisleadingCharacterClass": "error", - "noPrototypeBuiltins": "error", - "noRedeclare": "error", - "noShadowRestrictedNames": "error", - "noSparseArray": "error", - "noUnsafeNegation": "error", - "noUselessRegexBackrefs": "error", - "noWith": "error", - "useGetterReturn": "error" - } - } - }, - "javascript": { - "formatter": { - "quoteStyle": "single", - "semicolons": "asNeeded" - }, - "globals": [] - }, - "overrides": [ - { - "includes": ["**/*.spec.js", "test/fixtures/*.js"], - "javascript": { - "globals": [ - "vi", - "describe", - "it", - "test", - "expect", - "before", - "beforeEach", - "after", - "afterEach" - ] - } - }, - { - "includes": ["**/*.vue"], - "linter": { - "rules": { - "style": { - "useConst": "off", - "useImportType": "off" - }, - "correctness": { - "noUnusedVariables": "off", - "noUnusedImports": "off" - } - } - } - } - ], - "assist": { - "enabled": true, - "actions": { - "source": { - "organizeImports": { - "level": "on", - "options": { - "groups": [ - [":NODE:", ":PACKAGE:", "!src/**", "!@fortawesome/**"], - ":BLANK_LINE:", - [":PATH:", "src/components/**"], - ":BLANK_LINE:", - [":PATH:", "src/stores/**"], - ":BLANK_LINE:", - [":PATH:", "src/**", "src/stores/**", "src/components/**"], - ":BLANK_LINE:", - "@fortawesome/fontawesome-svg-core", - "@fortawesome/*" - ] - } - } - } - } - } -} diff --git a/build/check-versions.mjs b/build/check-versions.mjs index 8c5968a30..73c1eeb15 100644 --- a/build/check-versions.mjs +++ b/build/check-versions.mjs @@ -1,5 +1,5 @@ -import chalk from 'chalk' import semver from 'semver' +import chalk from 'chalk' import packageConfig from '../package.json' with { type: 'json' } @@ -7,8 +7,8 @@ var versionRequirements = [ { name: 'node', currentVersion: semver.clean(process.version), - versionRequirement: packageConfig.engines.node, - }, + versionRequirement: packageConfig.engines.node + } ] export default function () { @@ -16,22 +16,15 @@ export default function () { for (let i = 0; i < versionRequirements.length; i++) { const mod = versionRequirements[i] if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { - warnings.push( - mod.name + - ': ' + - chalk.red(mod.currentVersion) + - ' should be ' + - chalk.green(mod.versionRequirement), + warnings.push(mod.name + ': ' + + chalk.red(mod.currentVersion) + ' should be ' + + chalk.green(mod.versionRequirement) ) } } if (warnings.length) { - console.warn( - chalk.yellow( - '\nTo use this template, you must update following to modules:\n', - ), - ) + console.warn(chalk.yellow('\nTo use this template, you must update following to modules:\n')) for (let i = 0; i < warnings.length; i++) { const warning = warnings[i] console.warn(' ' + warning) diff --git a/build/commit_hash.js b/build/commit_hash.js index c60355804..c104af5d9 100644 --- a/build/commit_hash.js +++ b/build/commit_hash.js @@ -1,8 +1,8 @@ import childProcess from 'child_process' -export const getCommitHash = () => { - const subst = '$Format:%h$' - if (!subst.match(/Format:/)) { +export const getCommitHash = (() => { + const subst = "$Format:%h$" + if(!subst.match(/Format:/)) { return subst } else { try { @@ -15,4 +15,4 @@ export const getCommitHash = () => { return 'UNKNOWN' } } -} +}) diff --git a/build/copy_plugin.js b/build/copy_plugin.js index 4f020f359..a783fe7ff 100644 --- a/build/copy_plugin.js +++ b/build/copy_plugin.js @@ -1,8 +1,8 @@ -import { cp } from 'node:fs/promises' -import { resolve } from 'node:path' import serveStatic from 'serve-static' +import { resolve } from 'node:path' +import { cp } from 'node:fs/promises' -const getPrefix = (s) => { +const getPrefix = s => { const padEnd = s.endsWith('/') ? s : s + '/' return padEnd.startsWith('/') ? padEnd : '/' + padEnd } @@ -13,31 +13,28 @@ const copyPlugin = ({ inUrl, inFs }) => { let copyTarget const handler = serveStatic(inFs) - return [ - { - name: 'copy-plugin-serve', - apply: 'serve', - configureServer(server) { - server.middlewares.use(prefix, handler) - }, + return [{ + name: 'copy-plugin-serve', + apply: 'serve', + configureServer (server) { + server.middlewares.use(prefix, handler) + } + }, { + name: 'copy-plugin-build', + apply: 'build', + configResolved (config) { + copyTarget = resolve(config.root, config.build.outDir, subdir) }, - { - name: 'copy-plugin-build', - apply: 'build', - configResolved(config) { - copyTarget = resolve(config.root, config.build.outDir, subdir) - }, - closeBundle: { - order: 'post', - sequential: true, - async handler() { - console.info(`Copying '${inFs}' to ${copyTarget}...`) - await cp(inFs, copyTarget, { recursive: true }) - console.info('Done.') - }, - }, - }, - ] + closeBundle: { + order: 'post', + sequential: true, + async handler () { + console.log(`Copying '${inFs}' to ${copyTarget}...`) + await cp(inFs, copyTarget, { recursive: true }) + console.log('Done.') + } + } + }] } export default copyPlugin diff --git a/build/emojis_plugin.js b/build/emojis_plugin.js index 7979086dd..aed52066d 100644 --- a/build/emojis_plugin.js +++ b/build/emojis_plugin.js @@ -1,23 +1,21 @@ -import { access } from 'node:fs/promises' import { resolve } from 'node:path' - -import { languages } from '../src/i18n/languages.js' +import { access } from 'node:fs/promises' +import { languages, langCodeToCldrName } from '../src/i18n/languages.js' const annotationsImportPrefix = '@kazvmoe-infra/unicode-emoji-json/annotations/' const specialAnnotationsLocale = { - ja_easy: 'ja', + ja_easy: 'ja' } -const internalToAnnotationsLocale = (internal) => - specialAnnotationsLocale[internal] || internal +const internalToAnnotationsLocale = (internal) => specialAnnotationsLocale[internal] || internal // This gets all the annotations that are accessible (whose language // can be chosen in the settings). Data for other languages are // discarded because there is no way for it to be fetched. const getAllAccessibleAnnotations = async (projectRoot) => { - const imports = ( - await Promise.all( - languages.map(async (lang) => { + const imports = (await Promise.all( + languages + .map(async lang => { const destLang = internalToAnnotationsLocale(lang) const importModule = `${annotationsImportPrefix}${destLang}.json` const importFile = resolve(projectRoot, 'node_modules', importModule) @@ -25,14 +23,11 @@ const getAllAccessibleAnnotations = async (projectRoot) => { await access(importFile) return `'${lang}': () => import('${importModule}')` } catch (e) { - console.error(e) return } - }), - ) - ) - .filter((k) => k) - .join(',\n') + }))) + .filter(k => k) + .join(',\n') return ` export const annotationsLoader = { @@ -48,21 +43,21 @@ const emojisPlugin = () => { let projectRoot return { name: 'emojis-plugin', - configResolved(conf) { + configResolved (conf) { projectRoot = conf.root }, - resolveId(id) { + resolveId (id) { if (id === emojiAnnotationsId) { return emojiAnnotationsIdResolved } return null }, - async load(id) { + async load (id) { if (id === emojiAnnotationsIdResolved) { return await getAllAccessibleAnnotations(projectRoot) } return null - }, + } } } diff --git a/build/msw_plugin.js b/build/msw_plugin.js index c4e9098c5..f544348fc 100644 --- a/build/msw_plugin.js +++ b/build/msw_plugin.js @@ -1,5 +1,5 @@ -import { readFile } from 'node:fs/promises' import { resolve } from 'node:path' +import { readFile } from 'node:fs/promises' const target = 'node_modules/msw/lib/mockServiceWorker.js' @@ -8,10 +8,10 @@ const mswPlugin = () => { return { name: 'msw-plugin', apply: 'serve', - configResolved(conf) { + configResolved (conf) { projectRoot = conf.root }, - configureServer(server) { + configureServer (server) { server.middlewares.use(async (req, res, next) => { if (req.path === '/mockServiceWorker.js') { const file = await readFile(resolve(projectRoot, target)) @@ -21,7 +21,7 @@ const mswPlugin = () => { next() } }) - }, + } } } diff --git a/build/service_worker_messages.js b/build/service_worker_messages.js index 0948aa919..c078e8563 100644 --- a/build/service_worker_messages.js +++ b/build/service_worker_messages.js @@ -1,12 +1,11 @@ +import { languages, langCodeToJsonName } from '../src/i18n/languages.js' import { readFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' -import { langCodeToJsonName, languages } from '../src/i18n/languages.js' - const i18nDir = resolve( dirname(dirname(fileURLToPath(import.meta.url))), - 'src/i18n', + 'src/i18n' ) export const i18nFiles = languages.reduce((acc, lang) => { @@ -17,15 +16,13 @@ export const i18nFiles = languages.reduce((acc, lang) => { }, {}) export const generateServiceWorkerMessages = async () => { - const msgArray = await Promise.all( - Object.entries(i18nFiles).map(async ([lang, file]) => { - const fileContent = await readFile(file, 'utf-8') - const msg = { - notifications: JSON.parse(fileContent).notifications || {}, - } - return [lang, msg] - }), - ) + const msgArray = await Promise.all(Object.entries(i18nFiles).map(async ([lang, file]) => { + const fileContent = await readFile(file, 'utf-8') + const msg = { + notifications: JSON.parse(fileContent).notifications || {} + } + return [lang, msg] + })) return msgArray.reduce((acc, [lang, msg]) => { acc[lang] = msg return acc diff --git a/build/sw_plugin.js b/build/sw_plugin.js index 03c5978d7..a2c792b7d 100644 --- a/build/sw_plugin.js +++ b/build/sw_plugin.js @@ -1,13 +1,9 @@ -import { readFile } from 'node:fs/promises' -import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' -import * as esbuild from 'esbuild' +import { dirname, resolve } from 'node:path' +import { readFile } from 'node:fs/promises' import { build } from 'vite' - -import { - generateServiceWorkerMessages, - i18nFiles, -} from './service_worker_messages.js' +import * as esbuild from 'esbuild' +import { generateServiceWorkerMessages, i18nFiles } from './service_worker_messages.js' const getSWMessagesAsText = async () => { const messages = await generateServiceWorkerMessages() @@ -18,10 +14,14 @@ const projectRoot = dirname(dirname(fileURLToPath(import.meta.url))) const swEnvName = 'virtual:pleroma-fe/service_worker_env' const swEnvNameResolved = '\0' + swEnvName const getDevSwEnv = () => `self.serviceWorkerOption = { assets: [] };` -const getProdSwEnv = ({ assets }) => - `self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };` +const getProdSwEnv = ({ assets }) => `self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };` -export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => { +export const devSwPlugin = ({ + swSrc, + swDest, + transformSW, + alias +}) => { const swFullSrc = resolve(projectRoot, swSrc) const esbuildAlias = {} Object.entries(alias).forEach(([source, dest]) => { @@ -31,10 +31,9 @@ export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => { return { name: 'dev-sw-plugin', apply: 'serve', - configResolved() { - /* no-op */ + configResolved (conf) { }, - resolveId(id) { + resolveId (id) { const name = id.startsWith('/') ? id.slice(1) : id if (name === swDest) { return swFullSrc @@ -43,7 +42,7 @@ export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => { } return null }, - async load(id) { + async load (id) { if (id === swFullSrc) { return readFile(swFullSrc, 'utf-8') } else if (id === swEnvNameResolved) { @@ -56,7 +55,7 @@ export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => { * during dev, and firefox does not support ESM as service worker * https://bugzilla.mozilla.org/show_bug.cgi?id=1360870 */ - async transform(code, id) { + async transform (code, id) { if (id === swFullSrc && transformSW) { const res = await esbuild.build({ entryPoints: [swSrc], @@ -64,54 +63,52 @@ export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => { write: false, outfile: 'sw-pleroma.js', alias: esbuildAlias, - plugins: [ - { - name: 'vite-like-root-resolve', - setup(b) { - b.onResolve({ filter: new RegExp(/^\//) }, (args) => ({ - path: resolve(projectRoot, args.path.slice(1)), + plugins: [{ + name: 'vite-like-root-resolve', + setup (b) { + b.onResolve( + { filter: new RegExp(/^\//) }, + args => ({ + path: resolve(projectRoot, args.path.slice(1)) + }) + ) + } + }, { + name: 'sw-messages', + setup (b) { + b.onResolve( + { filter: new RegExp('^' + swMessagesName + '$') }, + args => ({ + path: args.path, + namespace: 'sw-messages' })) - }, - }, - { - name: 'sw-messages', - setup(b) { - b.onResolve( - { filter: new RegExp('^' + swMessagesName + '$') }, - (args) => ({ - path: args.path, - namespace: 'sw-messages', - }), - ) - b.onLoad( - { filter: /.*/, namespace: 'sw-messages' }, - async () => ({ - contents: await getSWMessagesAsText(), - }), - ) - }, - }, - { - name: 'sw-env', - setup(b) { - b.onResolve( - { filter: new RegExp('^' + swEnvName + '$') }, - (args) => ({ - path: args.path, - namespace: 'sw-env', - }), - ) - b.onLoad({ filter: /.*/, namespace: 'sw-env' }, () => ({ - contents: getDevSwEnv(), + b.onLoad( + { filter: /.*/, namespace: 'sw-messages' }, + async () => ({ + contents: await getSWMessagesAsText() })) - }, - }, - ], + } + }, { + name: 'sw-env', + setup (b) { + b.onResolve( + { filter: new RegExp('^' + swEnvName + '$') }, + args => ({ + path: args.path, + namespace: 'sw-env' + })) + b.onLoad( + { filter: /.*/, namespace: 'sw-env' }, + () => ({ + contents: getDevSwEnv() + })) + } + }] }) const text = res.outputFiles[0].text return text } - }, + } } } @@ -121,13 +118,16 @@ export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => { // however, we must compile the service worker to iife because of browser support. // Run another vite build just for the service worker targeting iife at // the end of the build. -export const buildSwPlugin = ({ swSrc, swDest }) => { +export const buildSwPlugin = ({ + swSrc, + swDest, +}) => { let config return { name: 'build-sw-plugin', enforce: 'post', apply: 'build', - configResolved(resolvedConfig) { + configResolved (resolvedConfig) { config = { define: resolvedConfig.define, resolve: resolvedConfig.resolve, @@ -138,50 +138,50 @@ export const buildSwPlugin = ({ swSrc, swDest }) => { lib: { entry: swSrc, formats: ['iife'], - name: 'sw_pleroma', + name: 'sw_pleroma' }, emptyOutDir: false, rollupOptions: { output: { - entryFileNames: swDest, - }, - }, + entryFileNames: swDest + } + } }, - configFile: false, + configFile: false } }, generateBundle: { order: 'post', sequential: true, - async handler(_, bundle) { + async handler (_, bundle) { const assets = Object.keys(bundle) - .filter((name) => !/\.map$/.test(name)) - .map((name) => '/' + name) + .filter(name => !/\.map$/.test(name)) + .map(name => '/' + name) config.plugins.push({ name: 'build-sw-env-plugin', - resolveId(id) { + resolveId (id) { if (id === swEnvName) { return swEnvNameResolved } return null }, - load(id) { + load (id) { if (id === swEnvNameResolved) { return getProdSwEnv({ assets }) } return null - }, + } }) - }, + } }, closeBundle: { order: 'post', sequential: true, - async handler() { - console.info('Building service worker for production') + async handler () { + console.log('Building service worker for production') await build(config) - }, - }, + } + } } } @@ -191,9 +191,9 @@ const swMessagesNameResolved = '\0' + swMessagesName export const swMessagesPlugin = () => { return { name: 'sw-messages-plugin', - resolveId(id) { + resolveId (id) { if (id === swMessagesName) { - Object.values(i18nFiles).forEach((f) => { + Object.values(i18nFiles).forEach(f => { this.addWatchFile(f) }) return swMessagesNameResolved @@ -201,11 +201,11 @@ export const swMessagesPlugin = () => { return null } }, - async load(id) { + async load (id) { if (id === swMessagesNameResolved) { return await getSWMessagesAsText() } return null - }, + } } } diff --git a/build/update-emoji.js b/build/update-emoji.js index 4ff7e1de8..5d578ba61 100644 --- a/build/update-emoji.js +++ b/build/update-emoji.js @@ -1,21 +1,22 @@ -import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with { - type: 'json', -} + +import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with { type: 'json' } import fs from 'fs' -Object.keys(emojis).map((k) => { - emojis[k].map((e) => { - delete e.unicode_version - delete e.emoji_version - delete e.skin_tone_support_unicode_version +Object.keys(emojis) + .map(k => { + emojis[k].map(e => { + delete e.unicode_version + delete e.emoji_version + delete e.skin_tone_support_unicode_version + }) }) -}) const res = {} -Object.keys(emojis).map((k) => { - const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase() - res[groupId] = emojis[k] -}) +Object.keys(emojis) + .map(k => { + const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase() + res[groupId] = emojis[k] + }) console.info('Updating emojis...') fs.writeFileSync('src/assets/emoji.json', JSON.stringify(res)) diff --git a/changelog.d/action-button-extra-counter.add b/changelog.d/action-button-extra-counter.add new file mode 100644 index 000000000..7d5c77447 --- /dev/null +++ b/changelog.d/action-button-extra-counter.add @@ -0,0 +1 @@ +Display counter for status action buttons when they are on the menu diff --git a/changelog.d/akkoma-sharkey-net-support.add b/changelog.d/akkoma-sharkey-net-support.add new file mode 100644 index 000000000..4b4bff7fe --- /dev/null +++ b/changelog.d/akkoma-sharkey-net-support.add @@ -0,0 +1 @@ +Added support for Akkoma and IceShrimp.NET backend diff --git a/changelog.d/instance-store-migration.skip b/changelog.d/akkoma.skip similarity index 100% rename from changelog.d/instance-store-migration.skip rename to changelog.d/akkoma.skip diff --git a/changelog.d/arithmetic-blend.add b/changelog.d/arithmetic-blend.add new file mode 100644 index 000000000..c579dca28 --- /dev/null +++ b/changelog.d/arithmetic-blend.add @@ -0,0 +1,2 @@ +Add arithmetic blend ISS function + diff --git a/changelog.d/better-scroll-button.add b/changelog.d/better-scroll-button.add new file mode 100644 index 000000000..b206869d1 --- /dev/null +++ b/changelog.d/better-scroll-button.add @@ -0,0 +1 @@ +Add support for detachable scrollTop button diff --git a/changelog.d/bookmark-button-align.fix b/changelog.d/bookmark-button-align.fix new file mode 100644 index 000000000..64bc2c807 --- /dev/null +++ b/changelog.d/bookmark-button-align.fix @@ -0,0 +1 @@ +Fix bookmark button alignment in the extra actions menu diff --git a/changelog.d/csp.add b/changelog.d/csp.add new file mode 100644 index 000000000..260337b97 --- /dev/null +++ b/changelog.d/csp.add @@ -0,0 +1 @@ +Compatibility with stricter CSP (Akkoma backend) diff --git a/changelog.d/filter-fixes.skip b/changelog.d/filter-fixes.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/fix-wrap.skip b/changelog.d/fix-wrap.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/migrate-auth-flow-pinia.skip b/changelog.d/migrate-auth-flow-pinia.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/migrate-oauth-tokens-module-to-pinia-store.skip b/changelog.d/migrate-oauth-tokens-module-to-pinia-store.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/mutes-sync.add b/changelog.d/mutes-sync.add new file mode 100644 index 000000000..e8e0e462a --- /dev/null +++ b/changelog.d/mutes-sync.add @@ -0,0 +1 @@ +Synchronized mutes, advanced mute control (regexp, expiry, naming) diff --git a/changelog.d/profile-error.fix b/changelog.d/profile-error.fix new file mode 100644 index 000000000..f123db5ae --- /dev/null +++ b/changelog.d/profile-error.fix @@ -0,0 +1 @@ +Fix error styling for user profiles diff --git a/changelog.d/small-fixes.skip b/changelog.d/small-fixes.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/sw-cache-assets.add b/changelog.d/sw-cache-assets.add new file mode 100644 index 000000000..5f7414eee --- /dev/null +++ b/changelog.d/sw-cache-assets.add @@ -0,0 +1 @@ +Cache assets and emojis with service worker diff --git a/changelog.d/theme3-body-class.add b/changelog.d/theme3-body-class.add new file mode 100644 index 000000000..f3d36fd70 --- /dev/null +++ b/changelog.d/theme3-body-class.add @@ -0,0 +1 @@ +Indicate currently active V3 theme as a body element class diff --git a/changelog.d/unify-show-hide-buttons.add b/changelog.d/unify-show-hide-buttons.add new file mode 100644 index 000000000..663bc38a5 --- /dev/null +++ b/changelog.d/unify-show-hide-buttons.add @@ -0,0 +1 @@ +Unify show/hide content buttons diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml deleted file mode 100644 index 75a4979a1..000000000 --- a/docker-compose.e2e.yml +++ /dev/null @@ -1,57 +0,0 @@ -services: - db: - image: postgres:15-alpine - environment: - POSTGRES_USER: pleroma - POSTGRES_PASSWORD: pleroma - POSTGRES_DB: pleroma - healthcheck: - test: ["CMD-SHELL", "pg_isready -U pleroma -d pleroma"] - interval: 2s - timeout: 2s - retries: 30 - - pleroma: - image: ${PLEROMA_IMAGE:-git.pleroma.social:5050/pleroma/pleroma:stable} - environment: - DB_USER: pleroma - DB_PASS: pleroma - DB_NAME: pleroma - DB_HOST: db - DB_PORT: 5432 - DOMAIN: localhost - INSTANCE_NAME: Pleroma E2E - ADMIN_EMAIL: ${E2E_ADMIN_EMAIL:-admin@example.com} - NOTIFY_EMAIL: ${E2E_ADMIN_EMAIL:-admin@example.com} - E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-admin} - E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin} - E2E_ADMIN_EMAIL: ${E2E_ADMIN_EMAIL:-admin@example.com} - depends_on: - db: - condition: service_healthy - volumes: - - ./docker/pleroma/entrypoint.e2e.sh:/opt/pleroma/entrypoint.e2e.sh:ro - entrypoint: ["/bin/ash", "/opt/pleroma/entrypoint.e2e.sh"] - healthcheck: - # NOTE: "localhost" may resolve to ::1 in some images (IPv6) while Pleroma only - # listens on IPv4 in this container. Use 127.0.0.1 to avoid false negatives. - test: ["CMD-SHELL", "test -f /var/lib/pleroma/.e2e_seeded && wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null || exit 1"] - interval: 5s - timeout: 3s - retries: 60 - - e2e: - build: - context: . - dockerfile: docker/e2e/Dockerfile.e2e - depends_on: - pleroma: - condition: service_healthy - environment: - CI: "1" - VITE_PROXY_TARGET: http://pleroma:4000 - VITE_PROXY_ORIGIN: http://localhost:4000 - E2E_BASE_URL: http://localhost:8080 - E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-admin} - E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin} - command: ["yarn", "e2e:pw"] diff --git a/docker/e2e/Dockerfile.e2e b/docker/e2e/Dockerfile.e2e deleted file mode 100644 index e84359ceb..000000000 --- a/docker/e2e/Dockerfile.e2e +++ /dev/null @@ -1,16 +0,0 @@ -FROM mcr.microsoft.com/playwright:v1.57.0-jammy - -WORKDIR /app - -ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 - -RUN npm install -g yarn@1.22.22 - -COPY package.json yarn.lock ./ -RUN yarn --frozen-lockfile - -COPY . . - -ENV CI=1 - -CMD ["yarn", "e2e:pw"] diff --git a/docker/pleroma/entrypoint.e2e.sh b/docker/pleroma/entrypoint.e2e.sh deleted file mode 100644 index 96920eeae..000000000 --- a/docker/pleroma/entrypoint.e2e.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/ash - -set -eu - -SEED_SENTINEL_PATH="/var/lib/pleroma/.e2e_seeded" -CONFIG_OVERRIDE_PATH="/var/lib/pleroma/config.exs" - -echo "-- Waiting for database..." -while ! pg_isready -U "${DB_USER:-pleroma}" -d "postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma}" -t 1; do - sleep 1s -done - -echo "-- Writing E2E config overrides..." -cat > "$CONFIG_OVERRIDE_PATH" <<'EOF' -import Config - -config :pleroma, Pleroma.Captcha, - enabled: false - -config :pleroma, :instance, - registrations_open: true, - account_activation_required: false, - approval_required: false -EOF - -echo "-- Running migrations..." -/opt/pleroma/bin/pleroma_ctl migrate - -echo "-- Starting!" -/opt/pleroma/bin/pleroma start & -PLEROMA_PID="$!" - -cleanup() { - if [ -n "${PLEROMA_PID:-}" ] && kill -0 "$PLEROMA_PID" 2>/dev/null; then - kill -TERM "$PLEROMA_PID" - wait "$PLEROMA_PID" || true - fi -} - -trap cleanup INT TERM - -echo "-- Waiting for API..." -api_ok="false" -for _i in $(seq 1 120); do - if wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null 2>&1; then - api_ok="true" - break - fi - sleep 1s -done - -if [ "$api_ok" != "true" ]; then - echo "Timed out waiting for Pleroma API to become available" - exit 1 -fi - -if [ ! -f "$SEED_SENTINEL_PATH" ]; then - if [ -n "${E2E_ADMIN_USERNAME:-}" ] && [ -n "${E2E_ADMIN_PASSWORD:-}" ] && [ -n "${E2E_ADMIN_EMAIL:-}" ]; then - echo "-- Seeding admin user (${E2E_ADMIN_USERNAME})..." - if ! /opt/pleroma/bin/pleroma_ctl user new "$E2E_ADMIN_USERNAME" "$E2E_ADMIN_EMAIL" --admin --password "$E2E_ADMIN_PASSWORD" -y; then - echo "-- User already exists (or creation failed), ensuring admin + confirmed..." - /opt/pleroma/bin/pleroma_ctl user set "$E2E_ADMIN_USERNAME" --admin --confirmed - fi - else - echo "-- Skipping admin seeding (missing E2E_ADMIN_* env)" - fi - - touch "$SEED_SENTINEL_PATH" -fi - -wait "$PLEROMA_PID" diff --git a/eslint.config.mjs b/eslint.config.mjs index 417ff8cf3..01bdb2038 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,34 +1,37 @@ -import js from '@eslint/js' -import { defineConfig, globalIgnores } from 'eslint/config' -import vue from 'eslint-plugin-vue' -import globals from 'globals' +import vue from "eslint-plugin-vue"; +import js from "@eslint/js"; +import globals from "globals"; -export default defineConfig([ + +export default [ ...vue.configs['flat/recommended'], - globalIgnores(['**/*.js', 'build/', 'dist/', 'config/']), + js.configs.recommended, { - files: ['src/**/*.vue'], - plugins: { js }, - extends: ['js/recommended'], + files: ["**/*.js", "**/*.mjs", "**/*.vue"], + ignores: ["build/*.js", "config/*.js"], + languageOptions: { ecmaVersion: 2024, - sourceType: 'module', + sourceType: "module", parserOptions: { - parser: '@babel/eslint-parser', + parser: "@babel/eslint-parser", }, globals: { ...globals.browser, ...globals.vitest, ...globals.chai, ...globals.commonjs, - ...globals.serviceworker, - }, + ...globals.serviceworker + } }, rules: { + 'arrow-parens': 0, + 'generator-star-spacing': 0, + 'no-debugger': 0, 'vue/require-prop-types': 0, 'vue/multi-word-component-names': 0, - }, - }, -]) + } + } +] diff --git a/index.html b/index.html index 26eeee19b..96c20c4b7 100644 --- a/index.html +++ b/index.html @@ -11,12 +11,14 @@ - + + + -
+
diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue index cbe3dd80f..c8bba4c44 100644 --- a/src/components/checkbox/checkbox.vue +++ b/src/components/checkbox/checkbox.vue @@ -1,7 +1,7 @@ diff --git a/src/components/contrast_ratio/contrast_ratio.vue b/src/components/contrast_ratio/contrast_ratio.vue index 1f8b62809..93799e4c2 100644 --- a/src/components/contrast_ratio/contrast_ratio.vue +++ b/src/components/contrast_ratio/contrast_ratio.vue @@ -63,68 +63,54 @@ import { library } from '@fortawesome/fontawesome-svg-core' import { faAdjust, faExclamationTriangle, - faThumbsUp, + faThumbsUp } from '@fortawesome/free-solid-svg-icons' -library.add(faAdjust, faExclamationTriangle, faThumbsUp) +library.add( + faAdjust, + faExclamationTriangle, + faThumbsUp +) export default { components: { - Tooltip, + Tooltip }, props: { large: { required: false, type: Boolean, - default: false, + default: false }, // TODO: Make theme switcher compute theme initially so that contrast // component won't be called without contrast data contrast: { required: false, type: Object, - default: () => ({ - /* no-op */ - }), + default: () => ({}) }, showRatio: { required: false, type: Boolean, - default: false, - }, + default: false + } }, computed: { - hint() { - const levelVal = this.contrast.aaa - ? 'aaa' - : this.contrast.aa - ? 'aa' - : 'bad' + hint () { + const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad') const level = this.$t(`settings.style.common.contrast.level.${levelVal}`) const context = this.$t('settings.style.common.contrast.context.text') const ratio = this.contrast.text - return this.$t('settings.style.common.contrast.hint', { - level, - context, - ratio, - }) + return this.$t('settings.style.common.contrast.hint', { level, context, ratio }) }, - hint_18pt() { - const levelVal = this.contrast.laaa - ? 'aaa' - : this.contrast.laa - ? 'aa' - : 'bad' + hint_18pt () { + const levelVal = this.contrast.laaa ? 'aaa' : (this.contrast.laa ? 'aa' : 'bad') const level = this.$t(`settings.style.common.contrast.level.${levelVal}`) const context = this.$t('settings.style.common.contrast.context.18pt') const ratio = this.contrast.text - return this.$t('settings.style.common.contrast.hint', { - level, - context, - ratio, - }) - }, - }, + return this.$t('settings.style.common.contrast.hint', { level, context, ratio }) + } + } } diff --git a/src/components/conversation-page/conversation-page.js b/src/components/conversation-page/conversation-page.js index d4705303e..8f996be12 100644 --- a/src/components/conversation-page/conversation-page.js +++ b/src/components/conversation-page/conversation-page.js @@ -2,13 +2,13 @@ import Conversation from '../conversation/conversation.vue' const conversationPage = { components: { - Conversation, + Conversation }, computed: { - statusId() { + statusId () { return this.$route.params.id - }, - }, + } + } } export default conversationPage diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index cb7cf4782..491a8543f 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -1,23 +1,25 @@ -import { clone, filter, findIndex, get, reduce } from 'lodash' -import { mapState as mapPiniaState } from 'pinia' -import { mapGetters, mapState } from 'vuex' - -import { WSConnectionStatus } from '../../services/api/api.service.js' -import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.vue' -import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue' +import { reduce, filter, findIndex, clone, get } from 'lodash' import Status from '../status/status.vue' import ThreadTree from '../thread_tree/thread_tree.vue' - +import { WSConnectionStatus } from '../../services/api/api.service.js' +import { mapGetters, mapState } from 'vuex' +import { mapState as mapPiniaState } from 'pinia' +import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.vue' +import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue' import { useInterfaceStore } from 'src/stores/interface' import { library } from '@fortawesome/fontawesome-svg-core' import { faAngleDoubleDown, faAngleDoubleLeft, - faChevronLeft, + faChevronLeft } from '@fortawesome/free-solid-svg-icons' -library.add(faAngleDoubleDown, faAngleDoubleLeft, faChevronLeft) +library.add( + faAngleDoubleDown, + faAngleDoubleLeft, + faChevronLeft +) const sortById = (a, b) => { const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id @@ -41,25 +43,23 @@ const sortAndFilterConversation = (conversation, statusoid) => { if (statusoid.type === 'retweet') { conversation = filter( conversation, - (status) => - status.type === 'retweet' || - status.id !== statusoid.retweeted_status.id, + (status) => (status.type === 'retweet' || status.id !== statusoid.retweeted_status.id) ) } else { conversation = filter(conversation, (status) => status.type !== 'retweet') } - return conversation.filter((_) => _).sort(sortById) + return conversation.filter(_ => _).sort(sortById) } const conversation = { - data() { + data () { return { highlight: null, expanded: false, threadDisplayStatusObject: {}, // id => 'showing' | 'hidden' statusContentPropertiesObject: {}, inlineDivePosition: null, - loadStatusError: null, + loadStatusError: null } }, props: [ @@ -69,80 +69,76 @@ const conversation = { 'pinnedStatusIdsObject', 'inProfile', 'profileUserId', - 'virtualHidden', + 'virtualHidden' ], - created() { + created () { if (this.isPage) { this.fetchConversation() } }, computed: { - maxDepthToShowByDefault() { + maxDepthToShowByDefault () { // maxDepthInThread = max number of depths that is *visible* // since our depth starts with 0 and "showing" means "showing children" // there is a -2 here const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2 return maxDepth >= 1 ? maxDepth : 1 }, - streamingEnabled() { - return ( - this.mergedConfig.useStreamingApi && - this.mastoUserSocketStatus === WSConnectionStatus.JOINED - ) + streamingEnabled () { + return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED }, - displayStyle() { + displayStyle () { return this.$store.getters.mergedConfig.conversationDisplay }, - isTreeView() { + isTreeView () { return !this.isLinearView }, - treeViewIsSimple() { + treeViewIsSimple () { return !this.$store.getters.mergedConfig.conversationTreeAdvanced }, - isLinearView() { + isLinearView () { return this.displayStyle === 'linear' }, - shouldFadeAncestors() { + shouldFadeAncestors () { return this.$store.getters.mergedConfig.conversationTreeFadeAncestors }, - otherRepliesButtonPosition() { + otherRepliesButtonPosition () { return this.$store.getters.mergedConfig.conversationOtherRepliesButton }, - showOtherRepliesButtonBelowStatus() { + showOtherRepliesButtonBelowStatus () { return this.otherRepliesButtonPosition === 'below' }, - showOtherRepliesButtonInsideStatus() { + showOtherRepliesButtonInsideStatus () { return this.otherRepliesButtonPosition === 'inside' }, - suspendable() { + suspendable () { if (this.isTreeView) { - return Object.entries(this.statusContentProperties).every( - ([, prop]) => !prop.replying && prop.mediaPlaying.length === 0, - ) + return Object.entries(this.statusContentProperties) + .every(([, prop]) => !prop.replying && prop.mediaPlaying.length === 0) } if (this.$refs.statusComponent && this.$refs.statusComponent[0]) { - return this.$refs.statusComponent.every((s) => s.suspendable) + return this.$refs.statusComponent.every(s => s.suspendable) } else { return true } }, - hideStatus() { + hideStatus () { return this.virtualHidden && this.suspendable }, - status() { + status () { return this.$store.state.statuses.allStatusesObject[this.statusId] }, - originalStatusId() { + originalStatusId () { if (this.status.retweeted_status) { return this.status.retweeted_status.id } else { return this.statusId } }, - conversationId() { + conversationId () { return this.getConversationId(this.statusId) }, - conversation() { + conversation () { if (!this.status) { return [] } @@ -151,9 +147,7 @@ const conversation = { return [this.status] } - const conversation = clone( - this.$store.state.statuses.conversationsObject[this.conversationId], - ) + const conversation = clone(this.$store.state.statuses.conversationsObject[this.conversationId]) const statusIndex = findIndex(conversation, { id: this.originalStatusId }) if (statusIndex !== -1) { conversation[statusIndex] = this.status @@ -161,188 +155,144 @@ const conversation = { return sortAndFilterConversation(conversation, this.status) }, - statusMap() { + statusMap () { return this.conversation.reduce((res, s) => { res[s.id] = s return res }, {}) }, - threadTree() { - const reverseLookupTable = this.conversation.reduce( - (table, status, index) => { - table[status.id] = index - return table - }, - {}, - ) + threadTree () { + const reverseLookupTable = this.conversation.reduce((table, status, index) => { + table[status.id] = index + return table + }, {}) - const threads = this.conversation.reduce( - (a, cur) => { - const id = cur.id - a.forest[id] = this.getReplies(id).map((s) => s.id) + const threads = this.conversation.reduce((a, cur) => { + const id = cur.id + a.forest[id] = this.getReplies(id) + .map(s => s.id) - return a - }, - { - forest: {}, - }, - ) + return a + }, { + forest: {} + }) - const walk = (forest, topLevel, depth = 0, processed = {}) => - topLevel - .map((id) => { - if (processed[id]) { - return [] - } + const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => { + if (processed[id]) { + return [] + } - processed[id] = true - return [ - { - status: this.conversation[reverseLookupTable[id]], - id, - depth, - }, - walk(forest, forest[id], depth + 1, processed), - ].reduce((a, b) => a.concat(b), []) - }) - .reduce((a, b) => a.concat(b), []) + processed[id] = true + return [{ + status: this.conversation[reverseLookupTable[id]], + id, + depth + }, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), []) + }).reduce((a, b) => a.concat(b), []) - const linearized = walk( - threads.forest, - this.topLevel.map((k) => k.id), - ) + const linearized = walk(threads.forest, this.topLevel.map(k => k.id)) return linearized }, - replyIds() { - return this.conversation - .map((k) => k.id) + replyIds () { + return this.conversation.map(k => k.id) .reduce((res, id) => { - res[id] = (this.replies[id] || []).map((k) => k.id) + res[id] = (this.replies[id] || []).map(k => k.id) return res }, {}) }, - totalReplyCount() { + totalReplyCount () { const sizes = {} const subTreeSizeFor = (id) => { if (sizes[id]) { return sizes[id] } - sizes[id] = - 1 + - this.replyIds[id] - .map((cid) => subTreeSizeFor(cid)) - .reduce((a, b) => a + b, 0) + sizes[id] = 1 + this.replyIds[id].map(cid => subTreeSizeFor(cid)).reduce((a, b) => a + b, 0) return sizes[id] } - this.conversation.map((k) => k.id).map(subTreeSizeFor) + this.conversation.map(k => k.id).map(subTreeSizeFor) return Object.keys(sizes).reduce((res, id) => { res[id] = sizes[id] - 1 // exclude itself return res }, {}) }, - totalReplyDepth() { + totalReplyDepth () { const depths = {} const subTreeDepthFor = (id) => { if (depths[id]) { return depths[id] } - depths[id] = - 1 + - this.replyIds[id] - .map((cid) => subTreeDepthFor(cid)) - .reduce((a, b) => (a > b ? a : b), 0) + depths[id] = 1 + this.replyIds[id].map(cid => subTreeDepthFor(cid)).reduce((a, b) => a > b ? a : b, 0) return depths[id] } - this.conversation.map((k) => k.id).map(subTreeDepthFor) + this.conversation.map(k => k.id).map(subTreeDepthFor) return Object.keys(depths).reduce((res, id) => { res[id] = depths[id] - 1 // exclude itself return res }, {}) }, - depths() { + depths () { return this.threadTree.reduce((a, k) => { a[k.id] = k.depth return a }, {}) }, - topLevel() { - const topLevel = this.conversation.reduce( - (tl, cur) => - tl.filter( - (k) => - this.getReplies(cur.id) - .map((v) => v.id) - .indexOf(k.id) === -1, - ), - this.conversation, - ) + topLevel () { + const topLevel = this.conversation.reduce((tl, cur) => + tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation) return topLevel }, - otherTopLevelCount() { + otherTopLevelCount () { return this.topLevel.length - 1 }, - showingTopLevel() { + showingTopLevel () { if (this.canDive && this.diveRoot) { return [this.statusMap[this.diveRoot]] } return this.topLevel }, - diveRoot() { + diveRoot () { const statusId = this.inlineDivePosition || this.statusId const isTopLevel = !this.parentOf(statusId) return isTopLevel ? null : statusId }, - diveDepth() { + diveDepth () { return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0 }, - diveMode() { + diveMode () { return this.canDive && !!this.diveRoot }, - shouldShowAllConversationButton() { + shouldShowAllConversationButton () { // The "show all conversation" button tells the user that there exist // other toplevel statuses, so do not show it if there is only a single root - return ( - this.isTreeView && - this.isExpanded && - this.diveMode && - this.topLevel.length > 1 - ) + return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1 }, - shouldShowAncestors() { - return ( - this.isTreeView && - this.isExpanded && - this.ancestorsOf(this.diveRoot).length - ) + shouldShowAncestors () { + return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length }, - replies() { + replies () { let i = 1 - return reduce( - this.conversation, - (result, { id, in_reply_to_status_id: irid }) => { - if (irid) { - result[irid] = result[irid] || [] - result[irid].push({ - name: `#${i}`, - id, - }) - } - i++ - return result - }, - {}, - ) + return reduce(this.conversation, (result, { id, in_reply_to_status_id: irid }) => { + if (irid) { + result[irid] = result[irid] || [] + result[irid].push({ + name: `#${i}`, + id + }) + } + i++ + return result + }, {}) }, - isExpanded() { + isExpanded () { return !!(this.expanded || this.isPage) }, - hiddenStyle() { + hiddenStyle () { const height = (this.status && this.status.virtualHeight) || '120px' return this.virtualHidden ? { height } : {} }, - threadDisplayStatus() { + threadDisplayStatus () { return this.conversation.reduce((a, k) => { const id = k.id const depth = this.depths[id] @@ -350,7 +300,7 @@ const conversation = { if (this.threadDisplayStatusObject[id]) { return this.threadDisplayStatusObject[id] } - if (depth - this.diveDepth <= this.maxDepthToShowByDefault) { + if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) { return 'showing' } else { return 'hidden' @@ -361,7 +311,7 @@ const conversation = { return a }, {}) }, - statusContentProperties() { + statusContentProperties () { return this.conversation.reduce((a, k) => { const id = k.id const props = (() => { @@ -370,13 +320,13 @@ const conversation = { expandingSubject: false, showingLongSubject: false, isReplying: false, - mediaPlaying: [], + mediaPlaying: [] } if (this.statusContentPropertiesObject[id]) { return { ...def, - ...this.statusContentPropertiesObject[id], + ...this.statusContentPropertiesObject[id] } } return def @@ -386,59 +336,54 @@ const conversation = { return a }, {}) }, - canDive() { + canDive () { return this.isTreeView && this.isExpanded }, - maybeHighlight() { + maybeHighlight () { return this.isExpanded ? this.highlight : null }, ...mapGetters(['mergedConfig']), ...mapState({ - mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus, + mastoUserSocketStatus: state => state.api.mastoUserSocketStatus }), ...mapPiniaState(useInterfaceStore, { - mobileLayout: (store) => store.layoutType === 'mobile', - }), + mobileLayout: store => store.layoutType === 'mobile' + }) }, components: { Status, ThreadTree, QuickFilterSettings, - QuickViewSettings, + QuickViewSettings }, watch: { - statusId(newVal, oldVal) { + statusId (newVal, oldVal) { const newConversationId = this.getConversationId(newVal) const oldConversationId = this.getConversationId(oldVal) - if ( - newConversationId && - oldConversationId && - newConversationId === oldConversationId - ) { + if (newConversationId && oldConversationId && newConversationId === oldConversationId) { this.setHighlight(this.originalStatusId) } else { this.fetchConversation() } }, - expanded(value) { + expanded (value) { if (value) { this.fetchConversation() } else { this.resetDisplayState() } }, - virtualHidden() { - this.$store.dispatch('setVirtualHeight', { - statusId: this.statusId, - height: `${this.$el.clientHeight}px`, - }) - }, + virtualHidden () { + this.$store.dispatch( + 'setVirtualHeight', + { statusId: this.statusId, height: `${this.$el.clientHeight}px` } + ) + } }, methods: { - fetchConversation() { + fetchConversation () { if (this.status) { - this.$store.state.api.backendInteractor - .fetchConversation({ id: this.statusId }) + this.$store.state.api.backendInteractor.fetchConversation({ id: this.statusId }) .then(({ ancestors, descendants }) => { this.$store.dispatch('addNewStatuses', { statuses: ancestors }) this.$store.dispatch('addNewStatuses', { statuses: descendants }) @@ -446,8 +391,7 @@ const conversation = { }) } else { this.loadStatusError = null - this.$store.state.api.backendInteractor - .fetchStatus({ id: this.statusId }) + this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId }) .then((status) => { this.$store.dispatch('addNewStatuses', { statuses: [status] }) this.fetchConversation() @@ -457,16 +401,16 @@ const conversation = { }) } }, - isFocused(id) { - return this.isExpanded && id === this.highlight + isFocused (id) { + return (this.isExpanded) && id === this.highlight }, - getReplies(id) { + getReplies (id) { return this.replies[id] || [] }, - getHighlight() { + getHighlight () { return this.isExpanded ? this.highlight : null }, - setHighlight(id) { + setHighlight (id) { if (!id) return this.highlight = id @@ -477,54 +421,44 @@ const conversation = { this.$store.dispatch('fetchFavsAndRepeats', id) this.$store.dispatch('fetchEmojiReactionsBy', id) }, - toggleExpanded() { + toggleExpanded () { this.expanded = !this.expanded }, - getConversationId(statusId) { + getConversationId (statusId) { const status = this.$store.state.statuses.allStatusesObject[statusId] - return get( - status, - 'retweeted_status.statusnet_conversation_id', - get(status, 'statusnet_conversation_id'), - ) + return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id')) }, - setThreadDisplay(id, nextStatus) { + setThreadDisplay (id, nextStatus) { this.threadDisplayStatusObject = { ...this.threadDisplayStatusObject, - [id]: nextStatus, + [id]: nextStatus } }, - toggleThreadDisplay(id) { + toggleThreadDisplay (id) { const curStatus = this.threadDisplayStatus[id] const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing' this.setThreadDisplay(id, nextStatus) }, - setThreadDisplayRecursively(id, nextStatus) { + setThreadDisplayRecursively (id, nextStatus) { this.setThreadDisplay(id, nextStatus) - this.getReplies(id) - .map((k) => k.id) - .map((id) => this.setThreadDisplayRecursively(id, nextStatus)) + this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus)) }, - showThreadRecursively(id) { + showThreadRecursively (id) { this.setThreadDisplayRecursively(id, 'showing') }, - setStatusContentProperty(id, name, value) { + setStatusContentProperty (id, name, value) { this.statusContentPropertiesObject = { ...this.statusContentPropertiesObject, [id]: { ...this.statusContentPropertiesObject[id], - [name]: value, - }, + [name]: value + } } }, - toggleStatusContentProperty(id, name) { - this.setStatusContentProperty( - id, - name, - !this.statusContentProperties[id][name], - ) + toggleStatusContentProperty (id, name) { + this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name]) }, - leastVisibleAncestor(id) { + leastVisibleAncestor (id) { let cur = id let parent = this.parentOf(cur) while (cur) { @@ -538,20 +472,18 @@ const conversation = { // nothing found, fall back to toplevel return this.topLevel[0] ? this.topLevel[0].id : undefined }, - diveIntoStatus(id) { + diveIntoStatus (id) { this.tryScrollTo(id) }, - diveToTopLevel() { - this.tryScrollTo( - this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id, - ) + diveToTopLevel () { + this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id) }, // only used when we are not on a page - undive() { + undive () { this.inlineDivePosition = null this.setHighlight(this.statusId) }, - tryScrollTo(id) { + tryScrollTo (id) { if (!id) { return } @@ -580,13 +512,13 @@ const conversation = { this.setHighlight(id) }) }, - goToCurrent() { + goToCurrent () { this.tryScrollTo(this.diveRoot || this.topLevel[0].id) }, - statusById(id) { + statusById (id) { return this.statusMap[id] }, - parentOf(id) { + parentOf (id) { const status = this.statusById(id) if (!status) { return undefined @@ -597,11 +529,11 @@ const conversation = { } return parentId }, - parentOrSelf(id) { + parentOrSelf (id) { return this.parentOf(id) || id }, // Ancestors of some status, from top to bottom - ancestorsOf(id) { + ancestorsOf (id) { const ancestors = [] let cur = this.parentOf(id) while (cur) { @@ -610,7 +542,7 @@ const conversation = { } return ancestors }, - topLevelAncestorOrSelfId(id) { + topLevelAncestorOrSelfId (id) { let cur = id let parent = this.parentOf(id) while (parent) { @@ -619,11 +551,11 @@ const conversation = { } return cur }, - resetDisplayState() { + resetDisplayState () { this.undive() this.threadDisplayStatusObject = {} - }, - }, + } + } } export default conversation diff --git a/src/components/desktop_nav/desktop_nav.js b/src/components/desktop_nav/desktop_nav.js index 943c66d1e..98d408a7e 100644 --- a/src/components/desktop_nav/desktop_nav.js +++ b/src/components/desktop_nav/desktop_nav.js @@ -1,25 +1,20 @@ import SearchBar from 'components/search_bar/search_bar.vue' -import { mapActions, mapState } from 'pinia' - import ConfirmModal from '../confirm_modal/confirm_modal.vue' - -import { useInstanceStore } from 'src/stores/instance.js' -import { useInterfaceStore } from 'src/stores/interface' - import { library } from '@fortawesome/fontawesome-svg-core' import { - faBell, - faBullhorn, - faCog, - faComments, - faHome, - faInfoCircle, - faSearch, faSignInAlt, faSignOutAlt, - faTachometerAlt, + faHome, + faComments, + faBell, faUserPlus, + faBullhorn, + faSearch, + faTachometerAlt, + faCog, + faInfoCircle } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from 'src/stores/interface' library.add( faSignInAlt, @@ -32,98 +27,91 @@ library.add( faSearch, faTachometerAlt, faCog, - faInfoCircle, + faInfoCircle ) export default { components: { SearchBar, - ConfirmModal, + ConfirmModal }, data: () => ({ searchBarHidden: true, - supportsMask: - window.CSS && - window.CSS.supports && - (window.CSS.supports('mask-size', 'contain') || + supportsMask: window.CSS && window.CSS.supports && ( + window.CSS.supports('mask-size', 'contain') || window.CSS.supports('-webkit-mask-size', 'contain') || window.CSS.supports('-moz-mask-size', 'contain') || window.CSS.supports('-ms-mask-size', 'contain') || - window.CSS.supports('-o-mask-size', 'contain')), - showingConfirmLogout: false, + window.CSS.supports('-o-mask-size', 'contain') + ), + showingConfirmLogout: false }), computed: { - enableMask() { - return this.supportsMask && this.logoMask - }, - logoStyle() { + enableMask () { return this.supportsMask && this.$store.state.instance.logoMask }, + logoStyle () { return { - visibility: this.enableMask ? 'hidden' : 'visible', + visibility: this.enableMask ? 'hidden' : 'visible' } }, - logoMaskStyle() { + logoMaskStyle () { return this.enableMask ? { - 'mask-image': `url(${this.logo})`, + 'mask-image': `url(${this.$store.state.instance.logo})` } : { - 'background-color': this.enableMask ? '' : 'transparent', + 'background-color': this.enableMask ? '' : 'transparent' } }, - logoBgStyle() { - return Object.assign( - { - margin: `${this.logoMargin} 0`, - opacity: this.searchBarHidden ? 1 : 0, - }, - this.enableMask - ? {} - : { - 'background-color': this.enableMask ? '' : 'transparent', - }, - ) + logoBgStyle () { + return Object.assign({ + margin: `${this.$store.state.instance.logoMargin} 0`, + opacity: this.searchBarHidden ? 1 : 0 + }, this.enableMask + ? {} + : { + 'background-color': this.enableMask ? '' : 'transparent' + }) }, - ...mapState(useInstanceStore, ['privateMode']), - ...mapState(useInstanceStore, { - logoMask: (store) => store.instanceIdentity.logoMask, - logo: (store) => store.instanceIdentity.logo, - logoLeft: (store) => store.instanceIdentity.logoLeft, - logoMargin: (store) => store.instanceIdentity.logoMargin, - sitename: (store) => store.instanceIdentity.name, - hideSitename: (store) => store.instanceIdentity.hideSitename, - }), - currentUser() { - return this.$store.state.users.currentUser - }, - shouldConfirmLogout() { + logo () { return this.$store.state.instance.logo }, + sitename () { return this.$store.state.instance.name }, + hideSitename () { return this.$store.state.instance.hideSitename }, + logoLeft () { return this.$store.state.instance.logoLeft }, + currentUser () { return this.$store.state.users.currentUser }, + privateMode () { return this.$store.state.instance.private }, + shouldConfirmLogout () { return this.$store.getters.mergedConfig.modalOnLogout - }, + } }, methods: { - scrollToTop() { + scrollToTop () { window.scrollTo(0, 0) }, - showConfirmLogout() { + showConfirmLogout () { this.showingConfirmLogout = true }, - hideConfirmLogout() { + hideConfirmLogout () { this.showingConfirmLogout = false }, - logout() { + logout () { if (!this.shouldConfirmLogout) { this.doLogout() } else { this.showConfirmLogout() } }, - doLogout() { + doLogout () { this.$router.replace('/main/public') this.$store.dispatch('logout') this.hideConfirmLogout() }, - onSearchBarToggled(hidden) { + onSearchBarToggled (hidden) { this.searchBarHidden = hidden }, - ...mapActions(useInterfaceStore, ['openSettingsModal']), - }, + openSettingsModal () { + useInterfaceStore().openSettingsModal('user') + }, + openAdminModal () { + useInterfaceStore().openSettingsModal('admin') + } + } } diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue index da427f2a1..49382f8ee 100644 --- a/src/components/desktop_nav/desktop_nav.vue +++ b/src/components/desktop_nav/desktop_nav.vue @@ -40,7 +40,7 @@
{ +export const filterNavigation = (list = [], { + hasChats, + hasAnnouncements, + isFederating, + isPrivate, + currentUser, + supportsBookmarkFolders, + supportsBubbleTimeline +}) => { return list.filter(({ criteria, anon, anonRoute }) => { const set = new Set(criteria || []) if (!isFederating && set.has('federating')) return false if (!currentUser && isPrivate && set.has('!private')) return false if (!currentUser && !(anon || anonRoute)) return false - if ((!currentUser || !currentUser.locked) && set.has('lockedUser')) - return false + if ((!currentUser || !currentUser.locked) && set.has('lockedUser')) return false if (!hasChats && set.has('chats')) return false if (!hasAnnouncements && set.has('announcements')) return false - if (!supportsBubbleTimeline && set.has('supportsBubbleTimeline')) - return false - if (!supportsBookmarkFolders && set.has('supportsBookmarkFolders')) - return false - if (supportsBookmarkFolders && set.has('!supportsBookmarkFolders')) - return false + if (!supportsBubbleTimeline && set.has('supportsBubbleTimeline')) return false + if (!supportsBookmarkFolders && set.has('supportsBookmarkFolders')) return false + if (supportsBookmarkFolders && set.has('!supportsBookmarkFolders')) return false return true }) } -export const getListEntries = (store) => - store.allLists.map((list) => ({ - name: 'list-' + list.id, - routeObject: { name: 'lists-timeline', params: { id: list.id } }, - labelRaw: list.title, - iconLetter: list.title[0], - })) +export const getListEntries = store => store.allLists.map(list => ({ + name: 'list-' + list.id, + routeObject: { name: 'lists-timeline', params: { id: list.id } }, + labelRaw: list.title, + iconLetter: list.title[0] +})) -export const getBookmarkFolderEntries = (store) => - store.allFolders - ? store.allFolders.map((folder) => ({ - name: 'bookmark-folder-' + folder.id, - routeObject: { name: 'bookmark-folder', params: { id: folder.id } }, - labelRaw: folder.name, - iconEmoji: folder.emoji, - iconEmojiUrl: folder.emoji_url, - iconLetter: folder.name[0], - })) - : [] +export const getBookmarkFolderEntries = store => store.allFolders ? store.allFolders.map(folder => ({ + name: 'bookmark-folder-' + folder.id, + routeObject: { name: 'bookmark-folder', params: { id: folder.id } }, + labelRaw: folder.name, + iconEmoji: folder.emoji, + iconEmojiUrl: folder.emoji_url, + iconLetter: folder.name[0] +})) : [] diff --git a/src/components/navigation/navigation.js b/src/components/navigation/navigation.js index 66fb0d347..d1c2b6763 100644 --- a/src/components/navigation/navigation.js +++ b/src/components/navigation/navigation.js @@ -4,39 +4,42 @@ export const USERNAME_ROUTES = new Set([ 'interactions', 'notifications', 'chat', - 'chats', + 'chats' ]) // routes that take :name property -export const NAME_ROUTES = new Set(['user-profile', 'legacy-user-profile']) +export const NAME_ROUTES = new Set([ + 'user-profile', + 'legacy-user-profile' +]) export const TIMELINES = { home: { route: 'friends', icon: 'home', label: 'nav.home_timeline', - criteria: ['!private'], + criteria: ['!private'] }, public: { route: 'public-timeline', anon: true, icon: 'users', label: 'nav.public_tl', - criteria: ['!private'], + criteria: ['!private'] }, bubble: { route: 'bubble', anon: true, icon: 'city', label: 'nav.bubble', - criteria: ['!private', 'federating', 'supportsBubbleTimeline'], + criteria: ['!private', 'federating', 'supportsBubbleTimeline'] }, twkn: { route: 'public-external-timeline', anon: true, icon: 'globe', label: 'nav.twkn', - criteria: ['!private', 'federating'], + criteria: ['!private', 'federating'] }, // bookmarks are still technically a timeline so we should show it in the dropdown bookmarks: { @@ -47,13 +50,13 @@ export const TIMELINES = { favorites: { routeObject: { name: 'user-profile', query: { tab: 'favorites' } }, icon: 'star', - label: 'user_card.favorites', + label: 'user_card.favorites' }, dms: { route: 'dms', icon: 'envelope', - label: 'nav.dms', - }, + label: 'nav.dms' + } } export const ROOT_ITEMS = { @@ -64,12 +67,12 @@ export const ROOT_ITEMS = { // shows bookmarks entry in a better suited location // hides it when bookmark folders are supported since // we show custom component instead of it - criteria: ['!supportsBookmarkFolders'], + criteria: ['!supportsBookmarkFolders'] }, interactions: { route: 'interactions', icon: 'bell', - label: 'nav.interactions', + label: 'nav.interactions' }, chats: { route: 'chats', @@ -77,7 +80,7 @@ export const ROOT_ITEMS = { label: 'nav.chats', badgeStyle: 'notification', badgeGetter: 'unreadChatCount', - criteria: ['chats'], + criteria: ['chats'] }, friendRequests: { route: 'friend-requests', @@ -85,13 +88,13 @@ export const ROOT_ITEMS = { label: 'nav.friend_requests', badgeStyle: 'notification', criteria: ['lockedUser'], - badgeGetter: 'followRequestCount', + badgeGetter: 'followRequestCount' }, about: { route: 'about', anon: true, icon: 'info-circle', - label: 'nav.about', + label: 'nav.about' }, announcements: { route: 'announcements', @@ -100,18 +103,18 @@ export const ROOT_ITEMS = { store: 'announcements', badgeStyle: 'notification', badgeGetter: 'unreadAnnouncementCount', - criteria: ['announcements'], + criteria: ['announcements'] }, drafts: { route: 'drafts', icon: 'file-pen', label: 'nav.drafts', badgeStyle: 'neutral', - badgeGetter: 'draftCount', - }, + badgeGetter: 'draftCount' + } } -export function routeTo(item, currentUser) { +export function routeTo (item, currentUser) { if (!item.route && !item.routeObject) return null let route @@ -119,7 +122,7 @@ export function routeTo(item, currentUser) { if (item.routeObject) { route = item.routeObject } else { - route = { name: item.anon || currentUser ? item.route : item.anonRoute } + route = { name: (item.anon || currentUser) ? item.route : item.anonRoute } } if (USERNAME_ROUTES.has(route.name)) { diff --git a/src/components/navigation/navigation_entry.js b/src/components/navigation/navigation_entry.js index 3384534be..11db1c9e3 100644 --- a/src/components/navigation/navigation_entry.js +++ b/src/components/navigation/navigation_entry.js @@ -1,57 +1,48 @@ -import { mapState as mapPiniaState, mapStores } from 'pinia' import { mapState } from 'vuex' - import { routeTo } from 'src/components/navigation/navigation.js' import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue' - -import { useAnnouncementsStore } from 'src/stores/announcements.js' -import { useServerSideStorageStore } from 'src/stores/serverSideStorage.js' - import { library } from '@fortawesome/fontawesome-svg-core' import { faThumbtack } from '@fortawesome/free-solid-svg-icons' +import { mapStores, mapState as mapPiniaState } from 'pinia' + +import { useAnnouncementsStore } from 'src/stores/announcements' +import { useServerSideStorageStore } from 'src/stores/serverSideStorage' library.add(faThumbtack) const NavigationEntry = { props: ['item', 'showPin'], components: { - OptionalRouterLink, + OptionalRouterLink }, methods: { - isPinned(value) { + isPinned (value) { return this.pinnedItems.has(value) }, - togglePin(value) { + togglePin (value) { if (this.isPinned(value)) { - useServerSideStorageStore().removeCollectionPreference({ - path: 'collections.pinnedNavItems', - value, - }) + useServerSideStorageStore().removeCollectionPreference({ path: 'collections.pinnedNavItems', value }) } else { - useServerSideStorageStore().addCollectionPreference({ - path: 'collections.pinnedNavItems', - value, - }) + useServerSideStorageStore().addCollectionPreference({ path: 'collections.pinnedNavItems', value }) } useServerSideStorageStore().pushServerSideStorage() - }, + } }, computed: { - routeTo() { + routeTo () { return routeTo(this.item, this.currentUser) }, - getters() { + getters () { return this.$store.getters }, ...mapStores(useAnnouncementsStore), ...mapState({ - currentUser: (state) => state.users.currentUser, + currentUser: state => state.users.currentUser }), ...mapPiniaState(useServerSideStorageStore, { - pinnedItems: (store) => - new Set(store.prefsStorage.collections.pinnedNavItems), + pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems) }), - }, + } } export default NavigationEntry diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js index 698bf5d59..f9a900fc6 100644 --- a/src/components/navigation/navigation_pins.js +++ b/src/components/navigation/navigation_pins.js @@ -1,38 +1,27 @@ -import { mapState as mapPiniaState } from 'pinia' import { mapState } from 'vuex' +import { mapState as mapPiniaState } from 'pinia' +import { TIMELINES, ROOT_ITEMS, routeTo } from 'src/components/navigation/navigation.js' +import { getBookmarkFolderEntries, getListEntries, filterNavigation } from 'src/components/navigation/filter.js' -import { - filterNavigation, - getBookmarkFolderEntries, - getListEntries, -} from 'src/components/navigation/filter.js' -import { - ROOT_ITEMS, - routeTo, - TIMELINES, -} from 'src/components/navigation/navigation.js' import StillImage from 'src/components/still-image/still-image.vue' -import { useAnnouncementsStore } from 'src/stores/announcements' -import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders' -import { useInstanceStore } from 'src/stores/instance.js' -import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' -import { useListsStore } from 'src/stores/lists' -import { useServerSideStorageStore } from 'src/stores/serverSideStorage' - import { library } from '@fortawesome/fontawesome-svg-core' import { - faBell, - faBookmark, - faCity, - faComments, - faEnvelope, - faGlobe, - faInfoCircle, - faList, - faStream, faUsers, + faGlobe, + faCity, + faBookmark, + faEnvelope, + faComments, + faBell, + faInfoCircle, + faStream, + faList } from '@fortawesome/free-solid-svg-icons' +import { useListsStore } from 'src/stores/lists' +import { useAnnouncementsStore } from 'src/stores/announcements' +import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders' +import { useServerSideStorageStore } from 'src/stores/serverSideStorage' library.add( faUsers, @@ -44,87 +33,85 @@ library.add( faBell, faInfoCircle, faStream, - faList, + faList ) const NavPanel = { props: ['limit'], methods: { - getRouteTo(item) { + getRouteTo (item) { return routeTo(item, this.currentUser) - }, + } }, components: { - StillImage, + StillImage }, computed: { - getters() { + getters () { return this.$store.getters }, ...mapPiniaState(useListsStore, { - lists: getListEntries, + lists: getListEntries }), ...mapPiniaState(useAnnouncementsStore, { - supportsAnnouncements: (store) => store.supportsAnnouncements, + supportsAnnouncements: store => store.supportsAnnouncements }), ...mapPiniaState(useBookmarkFoldersStore, { bookmarks: getBookmarkFolderEntries, }), ...mapPiniaState(useServerSideStorageStore, { - pinnedItems: (store) => - new Set(store.prefsStorage.collections.pinnedNavItems), + pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems) }), - ...mapPiniaState(useInstanceStore, ['privateMode', 'federating']), - ...mapPiniaState(useInstanceCapabilitiesStore, [ - 'pleromaChatMessagesAvailable', - 'localBubble', - ]), ...mapState({ - currentUser: (state) => state.users.currentUser, - followRequestCount: (state) => state.api.followRequests.length, + currentUser: state => state.users.currentUser, + followRequestCount: state => state.api.followRequests.length, + privateMode: state => state.instance.private, + federating: state => state.instance.federating, + pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable, + bubbleTimeline: state => state.instance.localBubbleInstances.length > 0 }), - pinnedList() { + pinnedList () { if (!this.currentUser) { - return filterNavigation( - [ - { ...TIMELINES.public, name: 'public' }, - { ...TIMELINES.twkn, name: 'twkn' }, - { ...ROOT_ITEMS.about, name: 'about' }, - ], - { - hasChats: this.pleromaChatMessagesAvailable, - hasAnnouncements: this.supportsAnnouncements, - isFederating: this.federating, - isPrivate: this.privateMode, - currentUser: this.currentUser, - supportsBubbleTimeline: this.localBubble, - supportsBookmarkFolders: this.bookmarks, - }, - ) - } - return filterNavigation( - [ - ...Object.entries({ ...TIMELINES }) - .filter(([k]) => this.pinnedItems.has(k)) - .map(([k, v]) => ({ ...v, name: k })), - ...this.lists.filter((k) => this.pinnedItems.has(k.name)), - ...this.bookmarks.filter((k) => this.pinnedItems.has(k.name)), - ...Object.entries({ ...ROOT_ITEMS }) - .filter(([k]) => this.pinnedItems.has(k)) - .map(([k, v]) => ({ ...v, name: k })), + return filterNavigation([ + { ...TIMELINES.public, name: 'public' }, + { ...TIMELINES.twkn, name: 'twkn' }, + { ...ROOT_ITEMS.about, name: 'about' } ], { hasChats: this.pleromaChatMessagesAvailable, hasAnnouncements: this.supportsAnnouncements, - supportsBubbleTimeline: this.localBubble, - supportsBookmarkFolders: this.bookmarks, isFederating: this.federating, isPrivate: this.privateMode, currentUser: this.currentUser, - }, + supportsBubbleTimeline: this.bubbleTimeline, + supportsBookmarkFolders: this.bookmarks + }) + } + return filterNavigation( + [ + ...Object + .entries({ ...TIMELINES }) + .filter(([k]) => this.pinnedItems.has(k)) + .map(([k, v]) => ({ ...v, name: k })), + ...this.lists.filter((k) => this.pinnedItems.has(k.name)), + ...this.bookmarks.filter((k) => this.pinnedItems.has(k.name)), + ...Object + .entries({ ...ROOT_ITEMS }) + .filter(([k]) => this.pinnedItems.has(k)) + .map(([k, v]) => ({ ...v, name: k })) + ], + { + hasChats: this.pleromaChatMessagesAvailable, + hasAnnouncements: this.supportsAnnouncements, + supportsBubbleTimeline: this.bubbleTimeline, + supportsBookmarkFolders: this.bookmarks, + isFederating: this.federating, + isPrivate: this.privateMode, + currentUser: this.currentUser + } ).slice(0, this.limit) - }, - }, + } + } } export default NavPanel diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index 16084dee4..043be1b1a 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -1,37 +1,29 @@ -import { mapState } from 'vuex' - -import RichContent from 'src/components/rich_content/rich_content.jsx' -import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' -import { - highlightClass, - highlightStyle, -} from '../../services/user_highlighter/user_highlighter.js' -import ConfirmModal from '../confirm_modal/confirm_modal.vue' -import Report from '../report/report.vue' -import Status from '../status/status.vue' import StatusContent from '../status_content/status_content.vue' -import Timeago from '../timeago/timeago.vue' +import { mapState } from 'vuex' +import Status from '../status/status.vue' import UserAvatar from '../user_avatar/user_avatar.vue' import UserCard from '../user_card/user_card.vue' +import Timeago from '../timeago/timeago.vue' +import Report from '../report/report.vue' import UserLink from '../user_link/user_link.vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' import UserPopover from '../user_popover/user_popover.vue' - -import { useInstanceStore } from 'src/stores/instance.js' - +import ConfirmModal from '../confirm_modal/confirm_modal.vue' +import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' +import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' - import { library } from '@fortawesome/fontawesome-svg-core' import { faCheck, - faCompressAlt, - faExpandAlt, - faEyeSlash, - faRetweet, - faStar, - faSuitcaseRolling, faTimes, - faUser, + faStar, + faRetweet, faUserPlus, + faEyeSlash, + faUser, + faSuitcaseRolling, + faExpandAlt, + faCompressAlt } from '@fortawesome/free-solid-svg-icons' library.add( @@ -44,17 +36,16 @@ library.add( faEyeSlash, faSuitcaseRolling, faExpandAlt, - faCompressAlt, + faCompressAlt ) const Notification = { - data() { + data () { return { - selecting: false, statusExpanded: false, unmuted: false, showingApproveConfirmDialog: false, - showingDenyConfirmDialog: false, + showingDenyConfirmDialog: false } }, props: ['notification'], @@ -69,158 +60,113 @@ const Notification = { RichContent, UserPopover, UserLink, - ConfirmModal, - }, - mounted() { - document.addEventListener('selectionchange', this.onContentSelect) - }, - unmounted() { - document.removeEventListener('selectionchange', this.onContentSelect) + ConfirmModal }, methods: { - toggleStatusExpanded() { - if (!this.expandable) return + toggleStatusExpanded () { this.statusExpanded = !this.statusExpanded }, - onContentSelect() { - const { isCollapsed, anchorNode, offsetNode } = document.getSelection() - if (isCollapsed) { - this.selecting = false - return - } - const within = - this.$refs.root.contains(anchorNode) || - this.$refs.root.contains(offsetNode) - if (within) { - this.selecting = true - } else { - this.selecting = false - } + generateUserProfileLink (user) { + return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) }, - onContentClick(e) { - if ( - !this.selecting && - !e.target.closest('a') && - !e.target.closest('button') - ) { - this.toggleStatusExpanded() - } - }, - generateUserProfileLink(user) { - return generateProfileLink( - user.id, - user.screen_name, - useInstanceStore().restrictedNicknames, - ) - }, - getUser(notification) { + getUser (notification) { return this.$store.state.users.usersObject[notification.from_profile.id] }, - interacted() { + interacted () { this.$emit('interacted') }, - toggleMute() { + toggleMute () { this.unmuted = !this.unmuted }, - showApproveConfirmDialog() { + showApproveConfirmDialog () { this.showingApproveConfirmDialog = true }, - hideApproveConfirmDialog() { + hideApproveConfirmDialog () { this.showingApproveConfirmDialog = false }, - showDenyConfirmDialog() { + showDenyConfirmDialog () { this.showingDenyConfirmDialog = true }, - hideDenyConfirmDialog() { + hideDenyConfirmDialog () { this.showingDenyConfirmDialog = false }, - approveUser() { + approveUser () { if (this.shouldConfirmApprove) { this.showApproveConfirmDialog() } else { this.doApprove() } }, - doApprove() { + doApprove () { this.$emit('interacted') this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) this.$store.dispatch('removeFollowRequest', this.user) - this.$store.dispatch('markSingleNotificationAsSeen', { - id: this.notification.id, - }) + this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id }) this.$store.dispatch('updateNotification', { id: this.notification.id, - updater: (notification) => { + updater: notification => { notification.type = 'follow' - }, + } }) this.hideApproveConfirmDialog() }, - denyUser() { + denyUser () { if (this.shouldConfirmDeny) { this.showDenyConfirmDialog() } else { this.doDeny() } }, - doDeny() { + doDeny () { this.$emit('interacted') - this.$store.state.api.backendInteractor - .denyUser({ id: this.user.id }) + this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) .then(() => { - this.$store.dispatch('dismissNotificationLocal', { - id: this.notification.id, - }) + this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id }) this.$store.dispatch('removeFollowRequest', this.user) }) this.hideDenyConfirmDialog() - }, + } }, computed: { - userClass() { + userClass () { return highlightClass(this.notification.from_profile) }, - userStyle() { + userStyle () { const highlight = this.$store.getters.mergedConfig.highlight const user = this.notification.from_profile return highlightStyle(highlight[user.screen_name]) }, - expandable() { - return new Set(['like', 'pleroma:emoji_reaction', 'repeat', 'poll']).has( - this.notification.type, - ) - }, - user() { + user () { return this.$store.getters.findUser(this.notification.from_profile.id) }, - userProfileLink() { + userProfileLink () { return this.generateUserProfileLink(this.user) }, - targetUser() { + targetUser () { return this.$store.getters.findUser(this.notification.target.id) }, - targetUserProfileLink() { + targetUserProfileLink () { return this.generateUserProfileLink(this.targetUser) }, - needMute() { + needMute () { return this.$store.getters.relationship(this.user.id).muting }, - isStatusNotification() { + isStatusNotification () { return isStatusNotification(this.notification.type) }, - mergedConfig() { + mergedConfig () { return this.$store.getters.mergedConfig }, - shouldConfirmApprove() { + shouldConfirmApprove () { return this.mergedConfig.modalOnApproveFollow }, - shouldConfirmDeny() { + shouldConfirmDeny () { return this.mergedConfig.modalOnDenyFollow }, ...mapState({ - currentUser: (state) => state.users.currentUser, - }), - }, + currentUser: state => state.users.currentUser + }) + } } export default Notification diff --git a/src/components/notification/notification.scss b/src/components/notification/notification.scss index 934d3e58d..e8895ce59 100644 --- a/src/components/notification/notification.scss +++ b/src/components/notification/notification.scss @@ -1,15 +1,10 @@ // TODO Copypaste from Status, should unify it somehow - .Notification { border-bottom: 1px solid; border-color: var(--border); overflow-wrap: break-word; text-wrap: pretty; - .status-content { - cursor: pointer; - } - &.Status { /* stylelint-disable-next-line declaration-no-important */ background-color: transparent !important; diff --git a/src/components/notification/notification.style.js b/src/components/notification/notification.style.js index 49e28cf2e..c6d317d1c 100644 --- a/src/components/notification/notification.style.js +++ b/src/components/notification/notification.style.js @@ -6,8 +6,13 @@ export default { 'Link', 'Icon', 'Border', + 'Button', + 'ButtonUnstyled', + 'RichContent', + 'Input', 'Avatar', - 'PollGraph', + 'Attachment', + 'PollGraph' ], - defaultRules: [], + defaultRules: [] } diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 7165289dc..0930e0990 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -1,7 +1,6 @@