Merge branch 'biome' into 'develop'

Migration to Biome

See merge request pleroma/pleroma-fe!2193
This commit is contained in:
HJ 2026-01-08 16:10:23 +00:00
commit aa25cd04b1
426 changed files with 55607 additions and 18546 deletions

View file

@ -34,12 +34,23 @@ check-changelog:
- apk add git - apk add git
- sh ./tools/check-changelog - sh ./tools/check-changelog
lint: lint-eslint:
stage: lint stage: lint
script: script:
- yarn - yarn
- yarn lint - yarn ci-eslint
- yarn stylelint
lint-biome:
stage: lint
script:
- yarn
- yarn ci-biome
lint-stylelint:
stage: lint
script:
- yarn
- yarn ci-stylelint
test: test:
stage: test stage: test

145
biome.json Normal file
View file

@ -0,0 +1,145 @@
{
"$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/**"],
":BLANK_LINE:",
"@fortawesome/fontawesome-svg-core",
"@fortawesome/*"
]
}
}
}
}
}
}

View file

@ -1,5 +1,5 @@
import semver from 'semver'
import chalk from 'chalk' import chalk from 'chalk'
import semver from 'semver'
import packageConfig from '../package.json' with { type: 'json' } import packageConfig from '../package.json' with { type: 'json' }
@ -7,8 +7,8 @@ var versionRequirements = [
{ {
name: 'node', name: 'node',
currentVersion: semver.clean(process.version), currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node versionRequirement: packageConfig.engines.node,
} },
] ]
export default function () { export default function () {
@ -16,15 +16,22 @@ export default function () {
for (let i = 0; i < versionRequirements.length; i++) { for (let i = 0; i < versionRequirements.length; i++) {
const mod = versionRequirements[i] const mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' + warnings.push(
chalk.red(mod.currentVersion) + ' should be ' + mod.name +
chalk.green(mod.versionRequirement) ': ' +
chalk.red(mod.currentVersion) +
' should be ' +
chalk.green(mod.versionRequirement),
) )
} }
} }
if (warnings.length) { 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++) { for (let i = 0; i < warnings.length; i++) {
const warning = warnings[i] const warning = warnings[i]
console.warn(' ' + warning) console.warn(' ' + warning)

View file

@ -1,7 +1,7 @@
import childProcess from 'child_process' import childProcess from 'child_process'
export const getCommitHash = (() => { export const getCommitHash = () => {
const subst = "$Format:%h$" const subst = '$Format:%h$'
if (!subst.match(/Format:/)) { if (!subst.match(/Format:/)) {
return subst return subst
} else { } else {
@ -15,4 +15,4 @@ export const getCommitHash = (() => {
return 'UNKNOWN' return 'UNKNOWN'
} }
} }
}) }

View file

@ -1,8 +1,8 @@
import serveStatic from 'serve-static'
import { resolve } from 'node:path'
import { cp } from 'node:fs/promises' import { cp } from 'node:fs/promises'
import { resolve } from 'node:path'
import serveStatic from 'serve-static'
const getPrefix = s => { const getPrefix = (s) => {
const padEnd = s.endsWith('/') ? s : s + '/' const padEnd = s.endsWith('/') ? s : s + '/'
return padEnd.startsWith('/') ? padEnd : '/' + padEnd return padEnd.startsWith('/') ? padEnd : '/' + padEnd
} }
@ -13,13 +13,15 @@ const copyPlugin = ({ inUrl, inFs }) => {
let copyTarget let copyTarget
const handler = serveStatic(inFs) const handler = serveStatic(inFs)
return [{ return [
{
name: 'copy-plugin-serve', name: 'copy-plugin-serve',
apply: 'serve', apply: 'serve',
configureServer(server) { configureServer(server) {
server.middlewares.use(prefix, handler) server.middlewares.use(prefix, handler)
} },
}, { },
{
name: 'copy-plugin-build', name: 'copy-plugin-build',
apply: 'build', apply: 'build',
configResolved(config) { configResolved(config) {
@ -32,9 +34,10 @@ const copyPlugin = ({ inUrl, inFs }) => {
console.info(`Copying '${inFs}' to ${copyTarget}...`) console.info(`Copying '${inFs}' to ${copyTarget}...`)
await cp(inFs, copyTarget, { recursive: true }) await cp(inFs, copyTarget, { recursive: true })
console.info('Done.') console.info('Done.')
} },
} },
}] },
]
} }
export default copyPlugin export default copyPlugin

View file

@ -1,21 +1,23 @@
import { resolve } from 'node:path'
import { access } from 'node:fs/promises' import { access } from 'node:fs/promises'
import { languages, langCodeToCldrName } from '../src/i18n/languages.js' import { resolve } from 'node:path'
import { languages } from '../src/i18n/languages.js'
const annotationsImportPrefix = '@kazvmoe-infra/unicode-emoji-json/annotations/' const annotationsImportPrefix = '@kazvmoe-infra/unicode-emoji-json/annotations/'
const specialAnnotationsLocale = { 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 // This gets all the annotations that are accessible (whose language
// can be chosen in the settings). Data for other languages are // can be chosen in the settings). Data for other languages are
// discarded because there is no way for it to be fetched. // discarded because there is no way for it to be fetched.
const getAllAccessibleAnnotations = async (projectRoot) => { const getAllAccessibleAnnotations = async (projectRoot) => {
const imports = (await Promise.all( const imports = (
languages await Promise.all(
.map(async lang => { languages.map(async (lang) => {
const destLang = internalToAnnotationsLocale(lang) const destLang = internalToAnnotationsLocale(lang)
const importModule = `${annotationsImportPrefix}${destLang}.json` const importModule = `${annotationsImportPrefix}${destLang}.json`
const importFile = resolve(projectRoot, 'node_modules', importModule) const importFile = resolve(projectRoot, 'node_modules', importModule)
@ -23,10 +25,13 @@ const getAllAccessibleAnnotations = async (projectRoot) => {
await access(importFile) await access(importFile)
return `'${lang}': () => import('${importModule}')` return `'${lang}': () => import('${importModule}')`
} catch (e) { } catch (e) {
console.error(e)
return return
} }
}))) }),
.filter(k => k) )
)
.filter((k) => k)
.join(',\n') .join(',\n')
return ` return `
@ -57,7 +62,7 @@ const emojisPlugin = () => {
return await getAllAccessibleAnnotations(projectRoot) return await getAllAccessibleAnnotations(projectRoot)
} }
return null return null
} },
} }
} }

View file

@ -1,5 +1,5 @@
import { resolve } from 'node:path'
import { readFile } from 'node:fs/promises' import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const target = 'node_modules/msw/lib/mockServiceWorker.js' const target = 'node_modules/msw/lib/mockServiceWorker.js'
@ -21,7 +21,7 @@ const mswPlugin = () => {
next() next()
} }
}) })
} },
} }
} }

View file

@ -1,11 +1,12 @@
import { languages, langCodeToJsonName } from '../src/i18n/languages.js'
import { readFile } from 'node:fs/promises' import { readFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path' import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { langCodeToJsonName, languages } from '../src/i18n/languages.js'
const i18nDir = resolve( const i18nDir = resolve(
dirname(dirname(fileURLToPath(import.meta.url))), dirname(dirname(fileURLToPath(import.meta.url))),
'src/i18n' 'src/i18n',
) )
export const i18nFiles = languages.reduce((acc, lang) => { export const i18nFiles = languages.reduce((acc, lang) => {
@ -16,13 +17,15 @@ export const i18nFiles = languages.reduce((acc, lang) => {
}, {}) }, {})
export const generateServiceWorkerMessages = async () => { export const generateServiceWorkerMessages = async () => {
const msgArray = await Promise.all(Object.entries(i18nFiles).map(async ([lang, file]) => { const msgArray = await Promise.all(
Object.entries(i18nFiles).map(async ([lang, file]) => {
const fileContent = await readFile(file, 'utf-8') const fileContent = await readFile(file, 'utf-8')
const msg = { const msg = {
notifications: JSON.parse(fileContent).notifications || {} notifications: JSON.parse(fileContent).notifications || {},
} }
return [lang, msg] return [lang, msg]
})) }),
)
return msgArray.reduce((acc, [lang, msg]) => { return msgArray.reduce((acc, [lang, msg]) => {
acc[lang] = msg acc[lang] = msg
return acc return acc

View file

@ -1,9 +1,13 @@
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
import { readFile } from 'node:fs/promises' import { readFile } from 'node:fs/promises'
import { build } from 'vite' import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import * as esbuild from 'esbuild' import * as esbuild from 'esbuild'
import { generateServiceWorkerMessages, i18nFiles } from './service_worker_messages.js' import { build } from 'vite'
import {
generateServiceWorkerMessages,
i18nFiles,
} from './service_worker_messages.js'
const getSWMessagesAsText = async () => { const getSWMessagesAsText = async () => {
const messages = await generateServiceWorkerMessages() const messages = await generateServiceWorkerMessages()
@ -14,14 +18,10 @@ const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)))
const swEnvName = 'virtual:pleroma-fe/service_worker_env' const swEnvName = 'virtual:pleroma-fe/service_worker_env'
const swEnvNameResolved = '\0' + swEnvName const swEnvNameResolved = '\0' + swEnvName
const getDevSwEnv = () => `self.serviceWorkerOption = { assets: [] };` 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 = ({ export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => {
swSrc,
swDest,
transformSW,
alias
}) => {
const swFullSrc = resolve(projectRoot, swSrc) const swFullSrc = resolve(projectRoot, swSrc)
const esbuildAlias = {} const esbuildAlias = {}
Object.entries(alias).forEach(([source, dest]) => { Object.entries(alias).forEach(([source, dest]) => {
@ -31,7 +31,8 @@ export const devSwPlugin = ({
return { return {
name: 'dev-sw-plugin', name: 'dev-sw-plugin',
apply: 'serve', apply: 'serve',
configResolved (conf) { configResolved() {
/* no-op */
}, },
resolveId(id) { resolveId(id) {
const name = id.startsWith('/') ? id.slice(1) : id const name = id.startsWith('/') ? id.slice(1) : id
@ -63,52 +64,54 @@ export const devSwPlugin = ({
write: false, write: false,
outfile: 'sw-pleroma.js', outfile: 'sw-pleroma.js',
alias: esbuildAlias, alias: esbuildAlias,
plugins: [{ plugins: [
{
name: 'vite-like-root-resolve', name: 'vite-like-root-resolve',
setup(b) { setup(b) {
b.onResolve( b.onResolve({ filter: new RegExp(/^\//) }, (args) => ({
{ filter: new RegExp(/^\//) }, path: resolve(projectRoot, args.path.slice(1)),
args => ({ }))
path: resolve(projectRoot, args.path.slice(1)) },
}) },
) {
}
}, {
name: 'sw-messages', name: 'sw-messages',
setup(b) { setup(b) {
b.onResolve( b.onResolve(
{ filter: new RegExp('^' + swMessagesName + '$') }, { filter: new RegExp('^' + swMessagesName + '$') },
args => ({ (args) => ({
path: args.path, path: args.path,
namespace: 'sw-messages' namespace: 'sw-messages',
})) }),
)
b.onLoad( b.onLoad(
{ filter: /.*/, namespace: 'sw-messages' }, { filter: /.*/, namespace: 'sw-messages' },
async () => ({ async () => ({
contents: await getSWMessagesAsText() contents: await getSWMessagesAsText(),
})) }),
} )
}, { },
},
{
name: 'sw-env', name: 'sw-env',
setup(b) { setup(b) {
b.onResolve( b.onResolve(
{ filter: new RegExp('^' + swEnvName + '$') }, { filter: new RegExp('^' + swEnvName + '$') },
args => ({ (args) => ({
path: args.path, path: args.path,
namespace: 'sw-env' namespace: 'sw-env',
}),
)
b.onLoad({ filter: /.*/, namespace: 'sw-env' }, () => ({
contents: getDevSwEnv(),
})) }))
b.onLoad( },
{ filter: /.*/, namespace: 'sw-env' }, },
() => ({ ],
contents: getDevSwEnv()
}))
}
}]
}) })
const text = res.outputFiles[0].text const text = res.outputFiles[0].text
return text return text
} }
} },
} }
} }
@ -118,10 +121,7 @@ export const devSwPlugin = ({
// however, we must compile the service worker to iife because of browser support. // 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 // Run another vite build just for the service worker targeting iife at
// the end of the build. // the end of the build.
export const buildSwPlugin = ({ export const buildSwPlugin = ({ swSrc, swDest }) => {
swSrc,
swDest,
}) => {
let config let config
return { return {
name: 'build-sw-plugin', name: 'build-sw-plugin',
@ -138,16 +138,16 @@ export const buildSwPlugin = ({
lib: { lib: {
entry: swSrc, entry: swSrc,
formats: ['iife'], formats: ['iife'],
name: 'sw_pleroma' name: 'sw_pleroma',
}, },
emptyOutDir: false, emptyOutDir: false,
rollupOptions: { rollupOptions: {
output: { output: {
entryFileNames: swDest entryFileNames: swDest,
}
}
}, },
configFile: false },
},
configFile: false,
} }
}, },
generateBundle: { generateBundle: {
@ -155,8 +155,8 @@ export const buildSwPlugin = ({
sequential: true, sequential: true,
async handler(_, bundle) { async handler(_, bundle) {
const assets = Object.keys(bundle) const assets = Object.keys(bundle)
.filter(name => !/\.map$/.test(name)) .filter((name) => !/\.map$/.test(name))
.map(name => '/' + name) .map((name) => '/' + name)
config.plugins.push({ config.plugins.push({
name: 'build-sw-env-plugin', name: 'build-sw-env-plugin',
resolveId(id) { resolveId(id) {
@ -170,9 +170,9 @@ export const buildSwPlugin = ({
return getProdSwEnv({ assets }) return getProdSwEnv({ assets })
} }
return null return null
} },
}) })
} },
}, },
closeBundle: { closeBundle: {
order: 'post', order: 'post',
@ -180,8 +180,8 @@ export const buildSwPlugin = ({
async handler() { async handler() {
console.info('Building service worker for production') console.info('Building service worker for production')
await build(config) await build(config)
} },
} },
} }
} }
@ -193,7 +193,7 @@ export const swMessagesPlugin = () => {
name: 'sw-messages-plugin', name: 'sw-messages-plugin',
resolveId(id) { resolveId(id) {
if (id === swMessagesName) { if (id === swMessagesName) {
Object.values(i18nFiles).forEach(f => { Object.values(i18nFiles).forEach((f) => {
this.addWatchFile(f) this.addWatchFile(f)
}) })
return swMessagesNameResolved return swMessagesNameResolved
@ -206,6 +206,6 @@ export const swMessagesPlugin = () => {
return await getSWMessagesAsText() return await getSWMessagesAsText()
} }
return null return null
} },
} }
} }

View file

@ -1,10 +1,10 @@
import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with {
import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with { type: 'json' } type: 'json',
}
import fs from 'fs' import fs from 'fs'
Object.keys(emojis) Object.keys(emojis).map((k) => {
.map(k => { emojis[k].map((e) => {
emojis[k].map(e => {
delete e.unicode_version delete e.unicode_version
delete e.emoji_version delete e.emoji_version
delete e.skin_tone_support_unicode_version delete e.skin_tone_support_unicode_version
@ -12,8 +12,7 @@ Object.keys(emojis)
}) })
const res = {} const res = {}
Object.keys(emojis) Object.keys(emojis).map((k) => {
.map(k => {
const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase() const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase()
res[groupId] = emojis[k] res[groupId] = emojis[k]
}) })

0
changelog.d/biome.skip Normal file
View file

View file

@ -1,37 +1,34 @@
import vue from "eslint-plugin-vue"; import js from '@eslint/js'
import js from "@eslint/js"; import { defineConfig, globalIgnores } from 'eslint/config'
import globals from "globals"; import vue from 'eslint-plugin-vue'
import globals from 'globals'
export default defineConfig([
export default [
...vue.configs['flat/recommended'], ...vue.configs['flat/recommended'],
js.configs.recommended, globalIgnores(['**/*.js', 'build/', 'dist/', 'config/']),
{ {
files: ["**/*.js", "**/*.mjs", "**/*.vue"], files: ['src/**/*.vue'],
ignores: ["build/*.js", "config/*.js"], plugins: { js },
extends: ['js/recommended'],
languageOptions: { languageOptions: {
ecmaVersion: 2024, ecmaVersion: 2024,
sourceType: "module", sourceType: 'module',
parserOptions: { parserOptions: {
parser: "@babel/eslint-parser", parser: '@babel/eslint-parser',
}, },
globals: { globals: {
...globals.browser, ...globals.browser,
...globals.vitest, ...globals.vitest,
...globals.chai, ...globals.chai,
...globals.commonjs, ...globals.commonjs,
...globals.serviceworker ...globals.serviceworker,
} },
}, },
rules: { rules: {
'arrow-parens': 0,
'generator-star-spacing': 0,
'no-debugger': 0,
'vue/require-prop-types': 0, 'vue/require-prop-types': 0,
'vue/multi-word-component-names': 0, 'vue/multi-word-component-names': 0,
} },
} },
] ])

View file

@ -1,6 +1,6 @@
{ {
"name": "pleroma_fe", "name": "pleroma_fe",
"version": "2.9.3", "version": "2.10.0",
"description": "Pleroma frontend, the default frontend of Pleroma social network server", "description": "Pleroma frontend, the default frontend of Pleroma social network server",
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>", "author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>",
"private": false, "private": false,
@ -13,9 +13,11 @@
"e2e:pw": "playwright test --config test/e2e-playwright/playwright.config.mjs", "e2e:pw": "playwright test --config test/e2e-playwright/playwright.config.mjs",
"e2e": "sh ./tools/e2e/run.sh", "e2e": "sh ./tools/e2e/run.sh",
"test": "yarn run unit && yarn run e2e", "test": "yarn run unit && yarn run e2e",
"stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'", "ci-biome": "yarn exec biome check",
"lint": "eslint src test/unit/specs test/e2e/specs test/e2e-playwright/specs test/e2e-playwright/playwright.config.mjs", "ci-eslint": "yarn exec eslint",
"lint-fix": "eslint --fix src test/unit/specs test/e2e/specs test/e2e-playwright/specs test/e2e-playwright/playwright.config.mjs" "ci-stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'",
"lint": "yarn ci-biome; yarn ci-eslint; yarn ci-stylelint",
"lint-fix": "yarn exec eslint --fix; yarn exec stylelint '**/*.scss' '**/*.vue' --fix; biome check --write"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.28.4", "@babel/runtime": "7.28.4",
@ -60,6 +62,7 @@
"@babel/plugin-transform-runtime": "7.28.5", "@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.5", "@babel/preset-env": "7.28.5",
"@babel/register": "7.28.3", "@babel/register": "7.28.3",
"@biomejs/biome": "2.3.11",
"@ungap/event-target": "0.2.4", "@ungap/event-target": "0.2.4",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1", "@vitejs/plugin-vue-jsx": "^4.1.1",
@ -78,7 +81,6 @@
"cross-spawn": "7.0.6", "cross-spawn": "7.0.6",
"custom-event-polyfill": "1.0.7", "custom-event-polyfill": "1.0.7",
"eslint": "9.39.2", "eslint": "9.39.2",
"vue-eslint-parser": "10.2.0",
"eslint-config-standard": "17.1.0", "eslint-config-standard": "17.1.0",
"eslint-formatter-friendly": "7.0.0", "eslint-formatter-friendly": "7.0.0",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",
@ -113,7 +115,8 @@
"vite": "^6.1.0", "vite": "^6.1.0",
"vite-plugin-eslint2": "^5.0.3", "vite-plugin-eslint2": "^5.0.3",
"vite-plugin-stylelint": "^6.0.0", "vite-plugin-stylelint": "^6.0.0",
"vitest": "^3.0.7" "vitest": "^3.0.7",
"vue-eslint-parser": "10.2.0"
}, },
"type": "module", "type": "module",
"engines": { "engines": {

View file

@ -1,7 +1,5 @@
import autoprefixer from 'autoprefixer' import autoprefixer from 'autoprefixer'
export default { export default {
plugins: [ plugins: [autoprefixer],
autoprefixer
]
} }

View file

@ -1,6 +1,26 @@
{ {
"pleroma-dark": [ "Pleroma Dark", "#121a24", "#182230", "#b9b9ba", "#d8a070", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ], "pleroma-dark": [
"pleroma-light": [ "Pleroma Light", "#f2f4f6", "#dbe0e8", "#304055", "#f86f0f", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ], "Pleroma Dark",
"#121a24",
"#182230",
"#b9b9ba",
"#d8a070",
"#d31014",
"#0fa00f",
"#0095ff",
"#ffa500"
],
"pleroma-light": [
"Pleroma Light",
"#f2f4f6",
"#dbe0e8",
"#304055",
"#f86f0f",
"#d31014",
"#0fa00f",
"#0095ff",
"#ffa500"
],
"classic-dark": { "classic-dark": {
"name": "Classic Dark", "name": "Classic Dark",
"bg": "#161c20", "bg": "#161c20",
@ -12,8 +32,28 @@
"cBlue": "#0095ff", "cBlue": "#0095ff",
"cOrange": "#ffa500" "cOrange": "#ffa500"
}, },
"bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"], "bird": [
"pleroma-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"], "Bird",
"#f8fafd",
"#e6ecf0",
"#14171a",
"#0084b8",
"#e0245e",
"#17bf63",
"#1b95e0",
"#fab81e"
],
"pleroma-amoled": [
"Pleroma Dark AMOLED",
"#000000",
"#111111",
"#b0b0b1",
"#d8a070",
"#aa0000",
"#0fa00f",
"#0095ff",
"#d59500"
],
"tomorrow-night": { "tomorrow-night": {
"name": "Tomorrow Night", "name": "Tomorrow Night",
"bg": "#1d1f21", "bg": "#1d1f21",
@ -36,8 +76,28 @@
"cGreen": "#50FA7B", "cGreen": "#50FA7B",
"cOrange": "#FFB86C" "cOrange": "#FFB86C"
}, },
"ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ], "ir-black": [
"monokai": [ "Monokai", "#272822", "#383830", "#f8f8f2", "#f92672", "#F92672", "#a6e22e", "#66d9ef", "#f4bf75" ], "Ir Black",
"#000000",
"#242422",
"#b5b3aa",
"#ff6c60",
"#FF6C60",
"#A8FF60",
"#96CBFE",
"#FFFFB6"
],
"monokai": [
"Monokai",
"#272822",
"#383830",
"#f8f8f2",
"#f92672",
"#F92672",
"#a6e22e",
"#66d9ef",
"#f4bf75"
],
"purple-stream": { "purple-stream": {
"name": "Purple stream", "name": "Purple stream",
"bg": "#17171A", "bg": "#17171A",

View file

@ -65,7 +65,8 @@ body {
z-index: 2; z-index: 2;
grid-template-rows: repeat(8, 1fr); grid-template-rows: repeat(8, 1fr);
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, 1fr);
grid-template-areas: "P P . L L" grid-template-areas:
"P P . L L"
"P P . L L" "P P . L L"
"P P . L L" "P P . L L"
"P P . L L" "P P . L L"
@ -74,7 +75,7 @@ body {
"P P . E E" "P P . E E"
"P P . E E"; "P P . E E";
--logoChunkSize: calc(2em * 0.5 * var(--scale)) --logoChunkSize: calc(2em * 0.5 * var(--scale));
} }
.chunk { .chunk {

View file

@ -1,6 +1,4 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": ["config:base"]
"config:base"
]
} }

View file

@ -1,34 +1,36 @@
import UserPanel from './components/user_panel/user_panel.vue'
import NavPanel from './components/nav_panel/nav_panel.vue'
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
import FeaturesPanel from './components/features_panel/features_panel.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
import ShoutPanel from './components/shout_panel/shout_panel.vue'
import MediaModal from './components/media_modal/media_modal.vue'
import SideDrawer from './components/side_drawer/side_drawer.vue'
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
import MobileNav from './components/mobile_nav/mobile_nav.vue'
import DesktopNav from './components/desktop_nav/desktop_nav.vue'
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
import { getOrCreateServiceWorker } from './services/sw/sw'
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
import { mapGetters } from 'vuex'
import { defineAsyncComponent } from 'vue'
import { useShoutStore } from './stores/shout'
import { useInterfaceStore } from './stores/interface'
import { throttle } from 'lodash' import { throttle } from 'lodash'
import { defineAsyncComponent } from 'vue'
import { mapGetters } from 'vuex'
import DesktopNav from './components/desktop_nav/desktop_nav.vue'
import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
import FeaturesPanel from './components/features_panel/features_panel.vue'
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
import MediaModal from './components/media_modal/media_modal.vue'
import MobileNav from './components/mobile_nav/mobile_nav.vue'
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
import NavPanel from './components/nav_panel/nav_panel.vue'
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
import ShoutPanel from './components/shout_panel/shout_panel.vue'
import SideDrawer from './components/side_drawer/side_drawer.vue'
import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
import UserPanel from './components/user_panel/user_panel.vue'
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
import { getOrCreateServiceWorker } from './services/sw/sw'
import { windowHeight, windowWidth } from './services/window_utils/window_utils'
import { useInterfaceStore } from './stores/interface'
import { useShoutStore } from './stores/shout'
export default { export default {
name: 'app', name: 'app',
components: { components: {
UserPanel, UserPanel,
NavPanel, NavPanel,
Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')), Notifications: defineAsyncComponent(
() => import('./components/notifications/notifications.vue'),
),
InstanceSpecificPanel, InstanceSpecificPanel,
FeaturesPanel, FeaturesPanel,
WhoToFollowPanel, WhoToFollowPanel,
@ -38,16 +40,20 @@ export default {
MobilePostStatusButton, MobilePostStatusButton,
MobileNav, MobileNav,
DesktopNav, DesktopNav,
SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')), SettingsModal: defineAsyncComponent(
UpdateNotification: defineAsyncComponent(() => import('./components/update_notification/update_notification.vue')), () => import('./components/settings_modal/settings_modal.vue'),
),
UpdateNotification: defineAsyncComponent(
() => import('./components/update_notification/update_notification.vue'),
),
UserReportingModal, UserReportingModal,
PostStatusModal, PostStatusModal,
EditStatusModal, EditStatusModal,
StatusHistoryModal, StatusHistoryModal,
GlobalNoticeList GlobalNoticeList,
}, },
data: () => ({ data: () => ({
mobileActivePanel: 'timeline' mobileActivePanel: 'timeline',
}), }),
watch: { watch: {
themeApplied() { themeApplied() {
@ -58,7 +64,7 @@ export default {
}, },
layoutType() { layoutType() {
document.getElementById('modal').classList = ['-' + this.layoutType] document.getElementById('modal').classList = ['-' + this.layoutType]
} },
}, },
created() { created() {
// Load the locale from the storage // Load the locale from the storage
@ -90,10 +96,12 @@ export default {
}, },
currentTheme() { currentTheme() {
if (useInterfaceStore().styleDataUsed) { if (useInterfaceStore().styleDataUsed) {
const styleMeta = useInterfaceStore().styleDataUsed.find(x => x.component === '@meta') const styleMeta = useInterfaceStore().styleDataUsed.find(
(x) => x.component === '@meta',
)
if (styleMeta !== undefined) { if (styleMeta !== undefined) {
return styleMeta.directives.name.replaceAll(" ", "-").toLowerCase() return styleMeta.directives.name.replaceAll(' ', '-').toLowerCase()
} }
} }
@ -107,39 +115,51 @@ export default {
{ {
'-reverse': this.reverseLayout, '-reverse': this.reverseLayout,
'-no-sticky-headers': this.noSticky, '-no-sticky-headers': this.noSticky,
'-has-new-post-button': this.newPostButtonShown '-has-new-post-button': this.newPostButtonShown,
}, },
'-' + this.layoutType '-' + this.layoutType,
] ]
}, },
navClasses() { navClasses() {
const { navbarColumnStretch } = this.$store.getters.mergedConfig const { navbarColumnStretch } = this.$store.getters.mergedConfig
return [ return [
'-' + this.layoutType, '-' + this.layoutType,
...(navbarColumnStretch ? ['-column-stretch'] : []) ...(navbarColumnStretch ? ['-column-stretch'] : []),
] ]
}, },
currentUser () { return this.$store.state.users.currentUser }, currentUser() {
userBackground () { return this.currentUser.background_image }, return this.$store.state.users.currentUser
},
userBackground() {
return this.currentUser.background_image
},
instanceBackground() { instanceBackground() {
return this.mergedConfig.hideInstanceWallpaper return this.mergedConfig.hideInstanceWallpaper
? null ? null
: this.$store.state.instance.background : this.$store.state.instance.background
}, },
background () { return this.userBackground || this.instanceBackground }, background() {
return this.userBackground || this.instanceBackground
},
bgStyle() { bgStyle() {
if (this.background) { if (this.background) {
return { return {
'--body-background-image': `url(${this.background})` '--body-background-image': `url(${this.background})`,
} }
} }
}, },
shout () { return useShoutStore().joined }, shout() {
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, return useShoutStore().joined
},
suggestionsEnabled() {
return this.$store.state.instance.suggestionsEnabled
},
showInstanceSpecificPanel() { showInstanceSpecificPanel() {
return this.$store.state.instance.showInstanceSpecificPanel && return (
this.$store.state.instance.showInstanceSpecificPanel &&
!this.$store.getters.mergedConfig.hideISP && !this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent this.$store.state.instance.instanceSpecificPanelContent
)
}, },
isChats() { isChats() {
return this.$route.name === 'chat' || this.$route.name === 'chats' return this.$route.name === 'chat' || this.$route.name === 'chats'
@ -150,30 +170,50 @@ export default {
newPostButtonShown() { newPostButtonShown() {
if (this.isChats) return false if (this.isChats) return false
if (this.isListEdit) return false if (this.isListEdit) return false
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile' return (
this.$store.getters.mergedConfig.alwaysShowNewPostButton ||
this.layoutType === 'mobile'
)
},
showFeaturesPanel() {
return this.$store.state.instance.showFeaturesPanel
},
editingAvailable() {
return this.$store.state.instance.editingAvailable
}, },
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
editingAvailable () { return this.$store.state.instance.editingAvailable },
shoutboxPosition() { shoutboxPosition() {
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
}, },
hideShoutbox() { hideShoutbox() {
return this.$store.getters.mergedConfig.hideShoutbox return this.$store.getters.mergedConfig.hideShoutbox
}, },
layoutType () { return useInterfaceStore().layoutType }, layoutType() {
privateMode () { return this.$store.state.instance.private }, return useInterfaceStore().layoutType
},
privateMode() {
return this.$store.state.instance.private
},
reverseLayout() { reverseLayout() {
const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig const { thirdColumnMode, sidebarRight: reverseSetting } =
this.$store.getters.mergedConfig
if (this.layoutType !== 'wide') { if (this.layoutType !== 'wide') {
return reverseSetting return reverseSetting
} else { } else {
return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting return thirdColumnMode === 'notifications'
? reverseSetting
: !reverseSetting
} }
}, },
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders }, noSticky() {
showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars }, return this.$store.getters.mergedConfig.disableStickyHeaders
scrollParent () { return window; /* this.$refs.appContentRef */ }, },
...mapGetters(['mergedConfig']) showScrollbars() {
return this.$store.getters.mergedConfig.showScrollbars
},
scrollParent() {
return window /* this.$refs.appContentRef */
},
...mapGetters(['mergedConfig']),
}, },
methods: { methods: {
resizeHandler() { resizeHandler() {
@ -181,7 +221,10 @@ export default {
useInterfaceStore().setLayoutHeight(windowHeight()) useInterfaceStore().setLayoutHeight(windowHeight())
}, },
scrollHandler() { scrollHandler() {
const scrollPosition = this.scrollParent === window ? window.scrollY : this.scrollParent.scrollTop const scrollPosition =
this.scrollParent === window
? window.scrollY
: this.scrollParent.scrollTop
if (scrollPosition != 0) { if (scrollPosition != 0) {
this.$refs.appContentRef.classList.add(['-scrolled']) this.$refs.appContentRef.classList.add(['-scrolled'])
@ -192,7 +235,7 @@ export default {
setThemeBodyClass() { setThemeBodyClass() {
const themeName = this.currentTheme const themeName = this.currentTheme
const classList = Array.from(document.body.classList) const classList = Array.from(document.body.classList)
const oldTheme = classList.filter(c => c.startsWith('theme-')) const oldTheme = classList.filter((c) => c.startsWith('theme-'))
if (themeName !== null && themeName !== '') { if (themeName !== null && themeName !== '') {
const newTheme = `theme-${themeName.toLowerCase()}` const newTheme = `theme-${themeName.toLowerCase()}`
@ -209,7 +252,9 @@ export default {
} }
}, },
removeSplash() { removeSplash() {
document.querySelector('#status').textContent = this.$t('splash.fun_' + Math.ceil(Math.random() * 4)) document.querySelector('#status').textContent = this.$t(
'splash.fun_' + Math.ceil(Math.random() * 4),
)
const splashscreenRoot = document.querySelector('#splash') const splashscreenRoot = document.querySelector('#splash')
splashscreenRoot.addEventListener('transitionend', () => { splashscreenRoot.addEventListener('transitionend', () => {
splashscreenRoot.remove() splashscreenRoot.remove()
@ -219,6 +264,6 @@ export default {
}, 600) }, 600)
splashscreenRoot.classList.add('hidden') splashscreenRoot.classList.add('hidden')
document.querySelector('#app').classList.remove('hidden') document.querySelector('#app').classList.remove('hidden')
} },
} },
} }

View file

@ -1,30 +1,39 @@
/* global process */ /* global process */
import vClickOutside from 'click-outside-vue3'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import vClickOutside from 'click-outside-vue3'
import VueVirtualScroller from 'vue-virtual-scroller' import VueVirtualScroller from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome' import { config } from '@fortawesome/fontawesome-svg-core'
import { config } from '@fortawesome/fontawesome-svg-core'; import {
FontAwesomeIcon,
FontAwesomeLayers,
} from '@fortawesome/vue-fontawesome'
config.autoAddCss = false config.autoAddCss = false
import App from '../App.vue'
import routes from './routes'
import VBodyScrollLock from 'src/directives/body_scroll_lock' import VBodyScrollLock from 'src/directives/body_scroll_lock'
import {
import { windowWidth, windowHeight } from '../services/window_utils/window_utils' instanceDefaultConfig,
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' staticOrApiConfigDefault,
import { applyConfig } from '../services/style_setter/style_setter.js' } from 'src/modules/default_config_state.js'
import FaviconService from '../services/favicon_service/favicon_service.js'
import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
import { useOAuthStore } from 'src/stores/oauth'
import { useI18nStore } from 'src/stores/i18n'
import { useInterfaceStore } from 'src/stores/interface'
import { useAnnouncementsStore } from 'src/stores/announcements' import { useAnnouncementsStore } from 'src/stores/announcements'
import { useAuthFlowStore } from 'src/stores/auth_flow' import { useAuthFlowStore } from 'src/stores/auth_flow'
import { staticOrApiConfigDefault, instanceDefaultConfig } from 'src/modules/default_config_state.js' import { useI18nStore } from 'src/stores/i18n'
import { useInterfaceStore } from 'src/stores/interface'
import { useOAuthStore } from 'src/stores/oauth'
import App from '../App.vue'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import FaviconService from '../services/favicon_service/favicon_service.js'
import { applyConfig } from '../services/style_setter/style_setter.js'
import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
import {
windowHeight,
windowWidth,
} from '../services/window_utils/window_utils'
import routes from './routes'
let staticInitialResults = null let staticInitialResults = null
@ -33,7 +42,9 @@ const parsedInitialResults = () => {
return null return null
} }
if (!staticInitialResults) { if (!staticInitialResults) {
staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent) staticInitialResults = JSON.parse(
document.getElementById('initial-results').textContent,
)
} }
return staticInitialResults return staticInitialResults
} }
@ -55,7 +66,7 @@ const preloadFetch = async (request) => {
return { return {
ok: true, ok: true,
json: () => requestData, json: () => requestData,
text: () => requestData text: () => requestData,
} }
} }
@ -67,17 +78,35 @@ const getInstanceConfig = async ({ store }) => {
const textlimit = data.max_toot_chars const textlimit = data.max_toot_chars
const vapidPublicKey = data.pleroma.vapid_public_key const vapidPublicKey = data.pleroma.vapid_public_key
store.dispatch('setInstanceOption', { name: 'pleromaExtensionsAvailable', value: data.pleroma }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit }) name: 'pleromaExtensionsAvailable',
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required }) value: data.pleroma,
store.dispatch('setInstanceOption', { name: 'birthdayRequired', value: !!data.pleroma?.metadata.birthday_required }) })
store.dispatch('setInstanceOption', { name: 'birthdayMinAge', value: data.pleroma?.metadata.birthday_min_age || 0 }) store.dispatch('setInstanceOption', {
name: 'textlimit',
value: textlimit,
})
store.dispatch('setInstanceOption', {
name: 'accountApprovalRequired',
value: data.approval_required,
})
store.dispatch('setInstanceOption', {
name: 'birthdayRequired',
value: !!data.pleroma?.metadata.birthday_required,
})
store.dispatch('setInstanceOption', {
name: 'birthdayMinAge',
value: data.pleroma?.metadata.birthday_min_age || 0,
})
if (vapidPublicKey) { if (vapidPublicKey) {
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) store.dispatch('setInstanceOption', {
name: 'vapidPublicKey',
value: vapidPublicKey,
})
} }
} else { } else {
throw (res) throw res
} }
} catch (error) { } catch (error) {
console.error('Could not load instance config, potentially fatal') console.error('Could not load instance config, potentially fatal')
@ -94,10 +123,12 @@ const getBackendProvidedConfig = async () => {
const data = await res.json() const data = await res.json()
return data.pleroma_fe return data.pleroma_fe
} else { } else {
throw (res) throw res
} }
} catch (error) { } catch (error) {
console.error('Could not load backend-provided frontend config, potentially fatal') console.error(
'Could not load backend-provided frontend config, potentially fatal',
)
console.error(error) console.error(error)
} }
} }
@ -108,7 +139,7 @@ const getStaticConfig = async () => {
if (res.ok) { if (res.ok) {
return res.json() return res.json()
} else { } else {
throw (res) throw res
} }
} catch (error) { } catch (error) {
console.warn('Failed to load static/config.json, continuing without it.') console.warn('Failed to load static/config.json, continuing without it.')
@ -149,7 +180,7 @@ const getTOS = async ({ store }) => {
const html = await res.text() const html = await res.text()
store.dispatch('setInstanceOption', { name: 'tos', value: html }) store.dispatch('setInstanceOption', { name: 'tos', value: html })
} else { } else {
throw (res) throw res
} }
} catch (e) { } catch (e) {
console.warn("Can't load TOS\n", e) console.warn("Can't load TOS\n", e)
@ -161,9 +192,12 @@ const getInstancePanel = async ({ store }) => {
const res = await preloadFetch('/instance/panel.html') const res = await preloadFetch('/instance/panel.html')
if (res.ok) { if (res.ok) {
const html = await res.text() const html = await res.text()
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html }) store.dispatch('setInstanceOption', {
name: 'instanceSpecificPanelContent',
value: html,
})
} else { } else {
throw (res) throw res
} }
} catch (e) { } catch (e) {
console.warn("Can't load instance panel\n", e) console.warn("Can't load instance panel\n", e)
@ -175,7 +209,8 @@ const getStickers = async ({ store }) => {
const res = await window.fetch('/static/stickers.json') const res = await window.fetch('/static/stickers.json')
if (res.ok) { if (res.ok) {
const values = await res.json() const values = await res.json()
const stickers = (await Promise.all( const stickers = (
await Promise.all(
Object.entries(values).map(async ([name, path]) => { Object.entries(values).map(async ([name, path]) => {
const resPack = await window.fetch(path + 'pack.json') const resPack = await window.fetch(path + 'pack.json')
let meta = {} let meta = {}
@ -185,15 +220,16 @@ const getStickers = async ({ store }) => {
return { return {
pack: name, pack: name,
path, path,
meta meta,
} }
}) }),
)).sort((a, b) => { )
).sort((a, b) => {
return a.meta.title.localeCompare(b.meta.title) return a.meta.title.localeCompare(b.meta.title)
}) })
store.dispatch('setInstanceOption', { name: 'stickers', value: stickers }) store.dispatch('setInstanceOption', { name: 'stickers', value: stickers })
} else { } else {
throw (res) throw res
} }
} catch (e) { } catch (e) {
console.warn("Can't load stickers\n", e) console.warn("Can't load stickers\n", e)
@ -203,13 +239,19 @@ const getStickers = async ({ store }) => {
const getAppSecret = async ({ store }) => { const getAppSecret = async ({ store }) => {
const oauth = useOAuthStore() const oauth = useOAuthStore()
if (oauth.userToken) { if (oauth.userToken) {
store.commit('setBackendInteractor', backendInteractorService(oauth.getToken)) store.commit(
'setBackendInteractor',
backendInteractorService(oauth.getToken),
)
} }
} }
const resolveStaffAccounts = ({ store, accounts }) => { const resolveStaffAccounts = ({ store, accounts }) => {
const nicknames = accounts.map(uri => uri.split('/').pop()) const nicknames = accounts.map((uri) => uri.split('/').pop())
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames }) store.dispatch('setInstanceOption', {
name: 'staffAccounts',
value: nicknames,
})
} }
const getNodeInfo = async ({ store }) => { const getNodeInfo = async ({ store }) => {
@ -220,77 +262,167 @@ const getNodeInfo = async ({ store }) => {
const data = await res.json() const data = await res.json()
const metadata = data.metadata const metadata = data.metadata
const features = metadata.features const features = metadata.features
store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations }) name: 'name',
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') }) value: metadata.nodeName,
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') }) })
store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') }) name: 'registrationOpen',
value: data.openRegistrations,
})
store.dispatch('setInstanceOption', {
name: 'mediaProxyAvailable',
value: features.includes('media_proxy'),
})
store.dispatch('setInstanceOption', {
name: 'safeDM',
value: features.includes('safe_dm_mentions'),
})
store.dispatch('setInstanceOption', {
name: 'shoutAvailable',
value: features.includes('chat'),
})
store.dispatch('setInstanceOption', {
name: 'pleromaChatMessagesAvailable',
value: features.includes('pleroma_chat_messages'),
})
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', {
name: 'pleromaCustomEmojiReactionsAvailable', name: 'pleromaCustomEmojiReactionsAvailable',
value: value:
features.includes('pleroma_custom_emoji_reactions') || features.includes('pleroma_custom_emoji_reactions') ||
features.includes('custom_emoji_reactions') features.includes('custom_emoji_reactions'),
})
store.dispatch('setInstanceOption', {
name: 'pleromaBookmarkFoldersAvailable',
value: features.includes('pleroma:bookmark_folders'),
})
store.dispatch('setInstanceOption', {
name: 'gopherAvailable',
value: features.includes('gopher'),
})
store.dispatch('setInstanceOption', {
name: 'pollsAvailable',
value: features.includes('polls'),
})
store.dispatch('setInstanceOption', {
name: 'editingAvailable',
value: features.includes('editing'),
})
store.dispatch('setInstanceOption', {
name: 'pollLimits',
value: metadata.pollLimits,
})
store.dispatch('setInstanceOption', {
name: 'mailerEnabled',
value: metadata.mailerEnabled,
})
store.dispatch('setInstanceOption', {
name: 'quotingAvailable',
value: features.includes('quote_posting'),
})
store.dispatch('setInstanceOption', {
name: 'groupActorAvailable',
value: features.includes('pleroma:group_actors'),
})
store.dispatch('setInstanceOption', {
name: 'blockExpiration',
value: features.includes('pleroma:block_expiration'),
})
store.dispatch('setInstanceOption', {
name: 'localBubbleInstances',
value: metadata.localBubbleInstances ?? [],
}) })
store.dispatch('setInstanceOption', { name: 'pleromaBookmarkFoldersAvailable', value: features.includes('pleroma:bookmark_folders') })
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') })
store.dispatch('setInstanceOption', { name: 'groupActorAvailable', value: features.includes('pleroma:group_actors') })
store.dispatch('setInstanceOption', { name: 'blockExpiration', value: features.includes('pleroma:block_expiration') })
store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances ?? [] })
const uploadLimits = metadata.uploadLimits const uploadLimits = metadata.uploadLimits
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadLimits.avatar) }) name: 'uploadlimit',
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadLimits.background) }) value: parseInt(uploadLimits.general),
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadLimits.banner) }) })
store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits }) store.dispatch('setInstanceOption', {
name: 'avatarlimit',
value: parseInt(uploadLimits.avatar),
})
store.dispatch('setInstanceOption', {
name: 'backgroundlimit',
value: parseInt(uploadLimits.background),
})
store.dispatch('setInstanceOption', {
name: 'bannerlimit',
value: parseInt(uploadLimits.banner),
})
store.dispatch('setInstanceOption', {
name: 'fieldsLimits',
value: metadata.fieldsLimits,
})
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats }) name: 'restrictedNicknames',
value: metadata.restrictedNicknames,
})
store.dispatch('setInstanceOption', {
name: 'postFormats',
value: metadata.postFormats,
})
const suggestions = metadata.suggestions const suggestions = metadata.suggestions
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web }) name: 'suggestionsEnabled',
value: suggestions.enabled,
})
store.dispatch('setInstanceOption', {
name: 'suggestionsWeb',
value: suggestions.web,
})
const software = data.software const software = data.software
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'backendRepository', value: software.repository }) name: 'backendVersion',
value: software.version,
})
store.dispatch('setInstanceOption', {
name: 'backendRepository',
value: software.repository,
})
const priv = metadata.private const priv = metadata.private
store.dispatch('setInstanceOption', { name: 'private', value: priv }) store.dispatch('setInstanceOption', { name: 'private', value: priv })
const frontendVersion = window.___pleromafe_commit_hash const frontendVersion = window.___pleromafe_commit_hash
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion }) store.dispatch('setInstanceOption', {
name: 'frontendVersion',
value: frontendVersion,
})
const federation = metadata.federation const federation = metadata.federation
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', {
name: 'tagPolicyAvailable', name: 'tagPolicyAvailable',
value: typeof federation.mrf_policies === 'undefined' value:
typeof federation.mrf_policies === 'undefined'
? false ? false
: metadata.federation.mrf_policies.includes('TagPolicy') : metadata.federation.mrf_policies.includes('TagPolicy'),
}) })
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation }) store.dispatch('setInstanceOption', {
name: 'federationPolicy',
value: federation,
})
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', {
name: 'federating', name: 'federating',
value: typeof federation.enabled === 'undefined' value:
? true typeof federation.enabled === 'undefined' ? true : federation.enabled,
: federation.enabled
}) })
const accountActivationRequired = metadata.accountActivationRequired const accountActivationRequired = metadata.accountActivationRequired
store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired }) store.dispatch('setInstanceOption', {
name: 'accountActivationRequired',
value: accountActivationRequired,
})
const accounts = metadata.staffAccounts const accounts = metadata.staffAccounts
resolveStaffAccounts({ store, accounts }) resolveStaffAccounts({ store, accounts })
} else { } else {
throw (res) throw res
} }
} catch (e) { } catch (e) {
console.warn('Could not load nodeinfo') console.warn('Could not load nodeinfo')
@ -300,7 +432,10 @@ const getNodeInfo = async ({ store }) => {
const setConfig = async ({ store }) => { const setConfig = async ({ store }) => {
// apiConfig, staticConfig // apiConfig, staticConfig
const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()]) const configInfos = await Promise.all([
getBackendProvidedConfig({ store }),
getStaticConfig(),
])
const apiConfig = configInfos[0] const apiConfig = configInfos[0]
const staticConfig = configInfos[1] const staticConfig = configInfos[1]
@ -331,29 +466,37 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
// do some checks to avoid common errors // do some checks to avoid common errors
if (!Object.keys(allStores).length) { if (!Object.keys(allStores).length) {
throw new Error('No stores are available. Check the code in src/boot/after_store.js') throw new Error(
'No stores are available. Check the code in src/boot/after_store.js',
)
} }
} }
await Promise.all( await Promise.all(
Object.entries(allStores) Object.entries(allStores).map(async ([name, mod]) => {
.map(async ([name, mod]) => { const isStoreName = (name) => name.startsWith('use')
const isStoreName = name => name.startsWith('use')
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
if (Object.keys(mod).filter(isStoreName).length !== 1) { if (Object.keys(mod).filter(isStoreName).length !== 1) {
throw new Error('Each store file must export exactly one store as a named export. Check your code in src/stores/') throw new Error(
'Each store file must export exactly one store as a named export. Check your code in src/stores/',
)
} }
} }
const storeFuncName = Object.keys(mod).find(isStoreName) const storeFuncName = Object.keys(mod).find(isStoreName)
if (storeFuncName && typeof mod[storeFuncName] === 'function') { if (storeFuncName && typeof mod[storeFuncName] === 'function') {
const p = mod[storeFuncName]().$persistLoaded const p = mod[storeFuncName]().$persistLoaded
if (!(p instanceof Promise)) { if (!(p instanceof Promise)) {
throw new Error(`${name} store's $persistLoaded is not a Promise. The persist plugin is not applied.`) throw new Error(
`${name} store's $persistLoaded is not a Promise. The persist plugin is not applied.`,
)
} }
await p await p
} else { } else {
throw new Error(`Store module ${name} does not export a 'use...' function`) throw new Error(
`Store module ${name} does not export a 'use...' function`,
)
} }
})) }),
)
} }
try { try {
@ -364,7 +507,10 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
} }
if (storageError) { if (storageError) {
useInterfaceStore().pushGlobalNotice({ messageKey: 'errors.storage_unavailable', level: 'error' }) useInterfaceStore().pushGlobalNotice({
messageKey: 'errors.storage_unavailable',
level: 'error',
})
} }
useInterfaceStore().setLayoutWidth(windowWidth()) useInterfaceStore().setLayoutWidth(windowWidth())
@ -376,12 +522,19 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
window.addEventListener('focus', () => updateFocus()) window.addEventListener('focus', () => updateFocus())
const overrides = window.___pleromafe_dev_overrides || {} const overrides = window.___pleromafe_dev_overrides || {}
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin const server =
typeof overrides.target !== 'undefined'
? overrides.target
: window.location.origin
store.dispatch('setInstanceOption', { name: 'server', value: server }) store.dispatch('setInstanceOption', { name: 'server', value: server })
await setConfig({ store }) await setConfig({ store })
try { try {
await useInterfaceStore().applyTheme().catch((e) => { console.error('Error setting theme', e) }) await useInterfaceStore()
.applyTheme()
.catch((e) => {
console.error('Error setting theme', e)
})
} catch (e) { } catch (e) {
window.splashError(e) window.splashError(e)
return Promise.reject(e) return Promise.reject(e)
@ -395,8 +548,8 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
checkOAuthToken({ store }), checkOAuthToken({ store }),
getInstancePanel({ store }), getInstancePanel({ store }),
getNodeInfo({ store }), getNodeInfo({ store }),
getInstanceConfig({ store }) getInstanceConfig({ store }),
]).catch(e => Promise.reject(e)) ]).catch((e) => Promise.reject(e))
// Start fetching things that don't need to block the UI // Start fetching things that don't need to block the UI
store.dispatch('fetchMutes') store.dispatch('fetchMutes')
@ -409,11 +562,11 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
history: createWebHistory(), history: createWebHistory(),
routes: routes(store), routes: routes(store),
scrollBehavior: (to, _from, savedPosition) => { scrollBehavior: (to, _from, savedPosition) => {
if (to.matched.some(m => m.meta.dontScroll)) { if (to.matched.some((m) => m.meta.dontScroll)) {
return false return false
} }
return savedPosition || { left: 0, top: 0 } return savedPosition || { left: 0, top: 0 }
} },
}) })
useI18nStore().setI18n(i18n) useI18nStore().setI18n(i18n)

View file

@ -1,35 +1,36 @@
import PublicTimeline from 'components/public_timeline/public_timeline.vue'
import BubbleTimeline from 'components/bubble_timeline/bubble_timeline.vue'
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue'
import ConversationPage from 'components/conversation-page/conversation-page.vue'
import Interactions from 'components/interactions/interactions.vue'
import DMs from 'components/dm_timeline/dm_timeline.vue'
import ChatList from 'components/chat_list/chat_list.vue'
import Chat from 'components/chat/chat.vue'
import UserProfile from 'components/user_profile/user_profile.vue'
import Search from 'components/search/search.vue'
import Registration from 'components/registration/registration.vue'
import PasswordReset from 'components/password_reset/password_reset.vue'
import FollowRequests from 'components/follow_requests/follow_requests.vue'
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
import Notifications from 'components/notifications/notifications.vue'
import AuthForm from 'components/auth_form/auth_form.js'
import ShoutPanel from 'components/shout_panel/shout_panel.vue'
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
import About from 'components/about/about.vue' import About from 'components/about/about.vue'
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
import Lists from 'components/lists/lists.vue'
import ListsTimeline from 'components/lists_timeline/lists_timeline.vue'
import ListsEdit from 'components/lists_edit/lists_edit.vue'
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
import AnnouncementsPage from 'components/announcements_page/announcements_page.vue' import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue' import AuthForm from 'components/auth_form/auth_form.js'
import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue'
import BubbleTimeline from 'components/bubble_timeline/bubble_timeline.vue'
import Chat from 'components/chat/chat.vue'
import ChatList from 'components/chat_list/chat_list.vue'
import ConversationPage from 'components/conversation-page/conversation-page.vue'
import DMs from 'components/dm_timeline/dm_timeline.vue'
import Drafts from 'components/drafts/drafts.vue' import Drafts from 'components/drafts/drafts.vue'
import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue' import FollowRequests from 'components/follow_requests/follow_requests.vue'
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
import Interactions from 'components/interactions/interactions.vue'
import Lists from 'components/lists/lists.vue'
import ListsEdit from 'components/lists_edit/lists_edit.vue'
import ListsTimeline from 'components/lists_timeline/lists_timeline.vue'
import Notifications from 'components/notifications/notifications.vue'
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
import PasswordReset from 'components/password_reset/password_reset.vue'
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
import PublicTimeline from 'components/public_timeline/public_timeline.vue'
import Registration from 'components/registration/registration.vue'
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
import Search from 'components/search/search.vue'
import ShoutPanel from 'components/shout_panel/shout_panel.vue'
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
import UserProfile from 'components/user_profile/user_profile.vue'
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
import BookmarkFolderEdit from '../components/bookmark_folder_edit/bookmark_folder_edit.vue' import BookmarkFolderEdit from '../components/bookmark_folder_edit/bookmark_folder_edit.vue'
import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue'
import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue'
export default (store) => { export default (store) => {
const validateAuthenticatedRoute = (to, from, next) => { const validateAuthenticatedRoute = (to, from, next) => {
@ -45,46 +46,124 @@ export default (store) => {
name: 'root', name: 'root',
path: '/', path: '/',
redirect: () => { redirect: () => {
return (store.state.users.currentUser return (
(store.state.users.currentUser
? store.state.instance.redirectRootLogin ? store.state.instance.redirectRootLogin
: store.state.instance.redirectRootNoLogin) || '/main/all' : store.state.instance.redirectRootNoLogin) || '/main/all'
} )
},
},
{
name: 'public-external-timeline',
path: '/main/all',
component: PublicAndExternalTimeline,
},
{
name: 'public-timeline',
path: '/main/public',
component: PublicTimeline,
},
{
name: 'friends',
path: '/main/friends',
component: FriendsTimeline,
beforeEnter: validateAuthenticatedRoute,
}, },
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline }, { name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline }, { name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
{ name: 'bubble', path: '/bubble', component: BubbleTimeline }, { name: 'bubble', path: '/bubble', component: BubbleTimeline },
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, {
name: 'conversation',
path: '/notice/:id',
component: ConversationPage,
meta: { dontScroll: true },
},
{ name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline }, { name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline },
{ {
name: 'remote-user-profile-acct', name: 'remote-user-profile-acct',
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)', path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
component: RemoteUserResolver, component: RemoteUserResolver,
beforeEnter: validateAuthenticatedRoute beforeEnter: validateAuthenticatedRoute,
}, },
{ {
name: 'remote-user-profile', name: 'remote-user-profile',
path: '/remote-users/:hostname/:username', path: '/remote-users/:hostname/:username',
component: RemoteUserResolver, component: RemoteUserResolver,
beforeEnter: validateAuthenticatedRoute beforeEnter: validateAuthenticatedRoute,
},
{
name: 'external-user-profile',
path: '/users/$:id',
component: UserProfile,
},
{
name: 'interactions',
path: '/users/:username/interactions',
component: Interactions,
beforeEnter: validateAuthenticatedRoute,
},
{
name: 'dms',
path: '/users/:username/dms',
component: DMs,
beforeEnter: validateAuthenticatedRoute,
}, },
{ name: 'external-user-profile', path: '/users/$:id', component: UserProfile },
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
{ name: 'registration', path: '/registration', component: Registration }, { name: 'registration', path: '/registration', component: Registration },
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true }, {
{ name: 'registration-token', path: '/registration/:token', component: Registration }, name: 'password-reset',
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, path: '/password-reset',
{ name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute }, component: PasswordReset,
props: true,
},
{
name: 'registration-token',
path: '/registration/:token',
component: Registration,
},
{
name: 'friend-requests',
path: '/friend-requests',
component: FollowRequests,
beforeEnter: validateAuthenticatedRoute,
},
{
name: 'notifications',
path: '/:username/notifications',
component: Notifications,
props: () => ({ disableTeleport: true }),
beforeEnter: validateAuthenticatedRoute,
},
{ name: 'login', path: '/login', component: AuthForm }, { name: 'login', path: '/login', component: AuthForm },
{ name: 'shout-panel', path: '/shout-panel', component: ShoutPanel, props: () => ({ floating: false }) }, {
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) }, name: 'shout-panel',
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) }, path: '/shout-panel',
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute }, component: ShoutPanel,
props: () => ({ floating: false }),
},
{
name: 'oauth-callback',
path: '/oauth-callback',
component: OAuthCallback,
props: (route) => ({ code: route.query.code }),
},
{
name: 'search',
path: '/search',
component: Search,
props: (route) => ({ query: route.query.query }),
},
{
name: 'who-to-follow',
path: '/who-to-follow',
component: WhoToFollow,
beforeEnter: validateAuthenticatedRoute,
},
{ name: 'about', path: '/about', component: About }, { name: 'about', path: '/about', component: About },
{ name: 'announcements', path: '/announcements', component: AnnouncementsPage }, {
name: 'announcements',
path: '/announcements',
component: AnnouncementsPage,
},
{ name: 'drafts', path: '/drafts', component: Drafts }, { name: 'drafts', path: '/drafts', component: Drafts },
{ name: 'user-profile', path: '/users/:name', component: UserProfile }, { name: 'user-profile', path: '/users/:name', component: UserProfile },
{ name: 'legacy-user-profile', path: '/:name', component: UserProfile }, { name: 'legacy-user-profile', path: '/:name', component: UserProfile },
@ -92,17 +171,51 @@ export default (store) => {
{ name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline }, { name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline },
{ name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit }, { name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit },
{ name: 'lists-new', path: '/lists/new', component: ListsEdit }, { name: 'lists-new', path: '/lists/new', component: ListsEdit },
{ name: 'edit-navigation', path: '/nav-edit', component: NavPanel, props: () => ({ forceExpand: true, forceEditMode: true }), beforeEnter: validateAuthenticatedRoute }, {
{ name: 'bookmark-folders', path: '/bookmark_folders', component: BookmarkFolders }, name: 'edit-navigation',
{ name: 'bookmark-folder-new', path: '/bookmarks/new-folder', component: BookmarkFolderEdit }, path: '/nav-edit',
{ name: 'bookmark-folder', path: '/bookmarks/:id', component: BookmarkTimeline }, component: NavPanel,
{ name: 'bookmark-folder-edit', path: '/bookmarks/:id/edit', component: BookmarkFolderEdit } props: () => ({ forceExpand: true, forceEditMode: true }),
beforeEnter: validateAuthenticatedRoute,
},
{
name: 'bookmark-folders',
path: '/bookmark_folders',
component: BookmarkFolders,
},
{
name: 'bookmark-folder-new',
path: '/bookmarks/new-folder',
component: BookmarkFolderEdit,
},
{
name: 'bookmark-folder',
path: '/bookmarks/:id',
component: BookmarkTimeline,
},
{
name: 'bookmark-folder-edit',
path: '/bookmarks/:id/edit',
component: BookmarkFolderEdit,
},
] ]
if (store.state.instance.pleromaChatMessagesAvailable) { if (store.state.instance.pleromaChatMessagesAvailable) {
routes = routes.concat([ routes = routes.concat([
{ name: 'chat', path: '/users/:username/chats/:recipient_id', component: Chat, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute }, {
{ name: 'chats', path: '/users/:username/chats', component: ChatList, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute } name: 'chat',
path: '/users/:username/chats/:recipient_id',
component: Chat,
meta: { dontScroll: false },
beforeEnter: validateAuthenticatedRoute,
},
{
name: 'chats',
path: '/users/:username/chats',
component: ChatList,
meta: { dontScroll: false },
beforeEnter: validateAuthenticatedRoute,
},
]) ])
} }

View file

@ -1,8 +1,8 @@
import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue'
import FeaturesPanel from '../features_panel/features_panel.vue' import FeaturesPanel from '../features_panel/features_panel.vue'
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue' import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue'
import StaffPanel from '../staff_panel/staff_panel.vue'
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue' import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue'
import StaffPanel from '../staff_panel/staff_panel.vue'
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
const About = { const About = {
components: { components: {
@ -10,16 +10,20 @@ const About = {
FeaturesPanel, FeaturesPanel,
TermsOfServicePanel, TermsOfServicePanel,
StaffPanel, StaffPanel,
MRFTransparencyPanel MRFTransparencyPanel,
}, },
computed: { computed: {
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, showFeaturesPanel() {
return this.$store.state.instance.showFeaturesPanel
},
showInstanceSpecificPanel() { showInstanceSpecificPanel() {
return this.$store.state.instance.showInstanceSpecificPanel && return (
this.$store.state.instance.showInstanceSpecificPanel &&
!this.$store.getters.mergedConfig.hideISP && !this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent this.$store.state.instance.instanceSpecificPanelContent
} )
} },
},
} }
export default About export default About

View file

@ -1,27 +1,23 @@
import { mapState } from 'vuex' import { mapState } from 'vuex'
import ProgressButton from '../progress_button/progress_button.vue'
import Popover from '../popover/popover.vue'
import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEllipsisV
} from '@fortawesome/free-solid-svg-icons'
import { useReportsStore } from 'src/stores/reports'
library.add( import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
faEllipsisV import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
) import { useReportsStore } from 'src/stores/reports'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import Popover from '../popover/popover.vue'
import ProgressButton from '../progress_button/progress_button.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'
library.add(faEllipsisV)
const AccountActions = { const AccountActions = {
props: [ props: ['user', 'relationship'],
'user', 'relationship'
],
data() { data() {
return { return {
showingConfirmBlock: false, showingConfirmBlock: false,
showingConfirmRemoveFollower: false showingConfirmRemoveFollower: false,
} }
}, },
components: { components: {
@ -29,7 +25,7 @@ const AccountActions = {
Popover, Popover,
UserListMenu, UserListMenu,
ConfirmModal, ConfirmModal,
UserTimedFilterModal UserTimedFilterModal,
}, },
methods: { methods: {
showConfirmRemoveUserFromFollowers() { showConfirmRemoveUserFromFollowers() {
@ -82,9 +78,12 @@ const AccountActions = {
openChat() { openChat() {
this.$router.push({ this.$router.push({
name: 'chat', name: 'chat',
params: { username: this.$store.state.users.currentUser.screen_name, recipient_id: this.user.id } params: {
username: this.$store.state.users.currentUser.screen_name,
recipient_id: this.user.id,
},
}) })
} },
}, },
computed: { computed: {
shouldConfirmBlock() { shouldConfirmBlock() {
@ -94,10 +93,11 @@ const AccountActions = {
return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
}, },
...mapState({ ...mapState({
blockExpirationSupported: state => state.instance.blockExpiration, blockExpirationSupported: (state) => state.instance.blockExpiration,
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable pleromaChatMessagesAvailable: (state) =>
}) state.instance.pleromaChatMessagesAvailable,
} }),
},
} }
export default AccountActions export default AccountActions

View file

@ -1,57 +1,51 @@
export default { export default {
name: 'Alert', name: 'Alert',
selector: '.alert', selector: '.alert',
validInnerComponents: [ validInnerComponents: ['Text', 'Icon', 'Link', 'Border', 'ButtonUnstyled'],
'Text',
'Icon',
'Link',
'Border',
'ButtonUnstyled'
],
variants: { variants: {
normal: '.neutral', normal: '.neutral',
error: '.error', error: '.error',
warning: '.warning', warning: '.warning',
success: '.success' success: '.success',
}, },
editor: { editor: {
border: 1, border: 1,
aspect: '3 / 1' aspect: '3 / 1',
}, },
defaultRules: [ defaultRules: [
{ {
directives: { directives: {
background: '--text', background: '--text',
opacity: 0.5, opacity: 0.5,
blur: '9px' blur: '9px',
} },
}, },
{ {
parent: { parent: {
component: 'Alert' component: 'Alert',
}, },
component: 'Border', component: 'Border',
directives: { directives: {
textColor: '--parent' textColor: '--parent',
} },
}, },
{ {
variant: 'error', variant: 'error',
directives: { directives: {
background: '--cRed' background: '--cRed',
} },
}, },
{ {
variant: 'warning', variant: 'warning',
directives: { directives: {
background: '--cOrange' background: '--cOrange',
} },
}, },
{ {
variant: 'success', variant: 'success',
directives: { directives: {
background: '--cGreen' background: '--cGreen',
} },
} },
] ],
} }

View file

@ -1,13 +1,14 @@
import { mapState } from 'vuex' import { mapState } from 'vuex'
import { useAnnouncementsStore } from 'src/stores/announcements'
import localeService from '../../services/locale/locale.service.js'
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue' import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
import RichContent from '../rich_content/rich_content.jsx' import RichContent from '../rich_content/rich_content.jsx'
import localeService from '../../services/locale/locale.service.js'
import { useAnnouncementsStore } from 'src/stores/announcements'
const Announcement = { const Announcement = {
components: { components: {
AnnouncementEditor, AnnouncementEditor,
RichContent RichContent,
}, },
data() { data() {
return { return {
@ -16,20 +17,25 @@ const Announcement = {
content: '', content: '',
startsAt: undefined, startsAt: undefined,
endsAt: undefined, endsAt: undefined,
allDay: undefined allDay: undefined,
}, },
editError: '' editError: '',
} }
}, },
props: { props: {
announcement: Object announcement: Object,
}, },
computed: { computed: {
...mapState({ ...mapState({
currentUser: state => state.users.currentUser currentUser: (state) => state.users.currentUser,
}), }),
canEditAnnouncement() { canEditAnnouncement() {
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements') return (
this.currentUser &&
this.currentUser.privileges.includes(
'announcements_manage_announcements',
)
)
}, },
content() { content() {
return this.announcement.content return this.announcement.content
@ -43,7 +49,10 @@ const Announcement = {
return return
} }
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale)) return this.formatTimeOrDate(
time,
localeService.internalToBrowserLocale(this.$i18n.locale),
)
}, },
startsAt() { startsAt() {
const time = this.announcement.starts_at const time = this.announcement.starts_at
@ -51,7 +60,10 @@ const Announcement = {
return return
} }
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale)) return this.formatTimeOrDate(
time,
localeService.internalToBrowserLocale(this.$i18n.locale),
)
}, },
endsAt() { endsAt() {
const time = this.announcement.ends_at const time = this.announcement.ends_at
@ -59,16 +71,21 @@ const Announcement = {
return return
} }
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale)) return this.formatTimeOrDate(
time,
localeService.internalToBrowserLocale(this.$i18n.locale),
)
}, },
inactive() { inactive() {
return this.announcement.inactive return this.announcement.inactive
} },
}, },
methods: { methods: {
markAsRead() { markAsRead() {
if (!this.isRead) { if (!this.isRead) {
return useAnnouncementsStore().markAnnouncementAsRead(this.announcement.id) return useAnnouncementsStore().markAnnouncementAsRead(
this.announcement.id,
)
} }
}, },
deleteAnnouncement() { deleteAnnouncement() {
@ -76,7 +93,9 @@ const Announcement = {
}, },
formatTimeOrDate(time, locale) { formatTimeOrDate(time, locale) {
const d = new Date(time) const d = new Date(time)
return this.announcement.all_day ? d.toLocaleDateString(locale) : d.toLocaleString(locale) return this.announcement.all_day
? d.toLocaleDateString(locale)
: d.toLocaleString(locale)
}, },
enterEditMode() { enterEditMode() {
this.editedAnnouncement.content = this.announcement.pleroma.raw_content this.editedAnnouncement.content = this.announcement.pleroma.raw_content
@ -86,14 +105,15 @@ const Announcement = {
this.editing = true this.editing = true
}, },
submitEdit() { submitEdit() {
useAnnouncementsStore().editAnnouncement({ useAnnouncementsStore()
.editAnnouncement({
id: this.announcement.id, id: this.announcement.id,
...this.editedAnnouncement ...this.editedAnnouncement,
}) })
.then(() => { .then(() => {
this.editing = false this.editing = false
}) })
.catch(error => { .catch((error) => {
this.editError = error.error this.editError = error.error
}) })
}, },
@ -102,8 +122,8 @@ const Announcement = {
}, },
clearError() { clearError() {
this.editError = undefined this.editError = undefined
} },
} },
} }
export default Announcement export default Announcement

View file

@ -2,12 +2,12 @@ import Checkbox from '../checkbox/checkbox.vue'
const AnnouncementEditor = { const AnnouncementEditor = {
components: { components: {
Checkbox Checkbox,
}, },
props: { props: {
announcement: Object, announcement: Object,
disabled: Boolean disabled: Boolean,
} },
} }
export default AnnouncementEditor export default AnnouncementEditor

View file

@ -1,12 +1,13 @@
import { mapState } from 'vuex' import { mapState } from 'vuex'
import { useAnnouncementsStore } from 'src/stores/announcements'
import Announcement from '../announcement/announcement.vue' import Announcement from '../announcement/announcement.vue'
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue' import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
import { useAnnouncementsStore } from 'src/stores/announcements'
const AnnouncementsPage = { const AnnouncementsPage = {
components: { components: {
Announcement, Announcement,
AnnouncementEditor AnnouncementEditor,
}, },
data() { data() {
return { return {
@ -14,10 +15,10 @@ const AnnouncementsPage = {
content: '', content: '',
startsAt: undefined, startsAt: undefined,
endsAt: undefined, endsAt: undefined,
allDay: false allDay: false,
}, },
posting: false, posting: false,
error: undefined error: undefined,
} }
}, },
mounted() { mounted() {
@ -25,25 +26,31 @@ const AnnouncementsPage = {
}, },
computed: { computed: {
...mapState({ ...mapState({
currentUser: state => state.users.currentUser currentUser: (state) => state.users.currentUser,
}), }),
announcements() { announcements() {
return useAnnouncementsStore().announcements return useAnnouncementsStore().announcements
}, },
canPostAnnouncement() { canPostAnnouncement() {
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements') return (
} this.currentUser &&
this.currentUser.privileges.includes(
'announcements_manage_announcements',
)
)
},
}, },
methods: { methods: {
postAnnouncement() { postAnnouncement() {
this.posting = true this.posting = true
useAnnouncementsStore().postAnnouncement(this.newAnnouncement) useAnnouncementsStore()
.postAnnouncement(this.newAnnouncement)
.then(() => { .then(() => {
this.newAnnouncement.content = '' this.newAnnouncement.content = ''
this.startsAt = undefined this.startsAt = undefined
this.endsAt = undefined this.endsAt = undefined
}) })
.catch(error => { .catch((error) => {
this.error = error.error this.error = error.error
}) })
.finally(() => { .finally(() => {
@ -52,8 +59,8 @@ const AnnouncementsPage = {
}, },
clearError() { clearError() {
this.error = undefined this.error = undefined
} },
} },
} }
export default AnnouncementsPage export default AnnouncementsPage

View file

@ -23,8 +23,8 @@ export default {
methods: { methods: {
retry() { retry() {
this.$emit('resetAsyncComponent') this.$emit('resetAsyncComponent')
} },
} },
} }
</script> </script>

View file

@ -1,24 +1,26 @@
import StillImage from '../still-image/still-image.vue' import { mapGetters } from 'vuex'
import Flash from '../flash/flash.vue'
import VideoAttachment from '../video_attachment/video_attachment.vue' import { useMediaViewerStore } from 'src/stores/media_viewer'
import nsfwImage from '../../assets/nsfw.png' import nsfwImage from '../../assets/nsfw.png'
import fileTypeService from '../../services/file_type/file_type.service.js' import fileTypeService from '../../services/file_type/file_type.service.js'
import { mapGetters } from 'vuex' import Flash from '../flash/flash.vue'
import StillImage from '../still-image/still-image.vue'
import VideoAttachment from '../video_attachment/video_attachment.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faAlignRight,
faFile, faFile,
faMusic,
faImage, faImage,
faVideo, faMusic,
faPlayCircle,
faTimes,
faStop,
faSearchPlus,
faTrashAlt,
faPencilAlt, faPencilAlt,
faAlignRight faPlayCircle,
faSearchPlus,
faStop,
faTimes,
faTrashAlt,
faVideo,
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { useMediaViewerStore } from 'src/stores/media_viewer'
library.add( library.add(
faFile, faFile,
@ -31,7 +33,7 @@ library.add(
faSearchPlus, faSearchPlus,
faTrashAlt, faTrashAlt,
faPencilAlt, faPencilAlt,
faAlignRight faAlignRight,
) )
const Attachment = { const Attachment = {
@ -46,7 +48,7 @@ const Attachment = {
'remove', 'remove',
'shiftUp', 'shiftUp',
'shiftDn', 'shiftDn',
'edit' 'edit',
], ],
data() { data() {
return { return {
@ -55,17 +57,19 @@ const Attachment = {
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw, hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
preloadImage: this.$store.getters.mergedConfig.preloadImage, preloadImage: this.$store.getters.mergedConfig.preloadImage,
loading: false, loading: false,
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'), img:
fileTypeService.fileType(this.attachment.mimetype) === 'image' &&
document.createElement('img'),
modalOpen: false, modalOpen: false,
showHidden: false, showHidden: false,
flashLoaded: false, flashLoaded: false,
showDescription: false showDescription: false,
} }
}, },
components: { components: {
Flash, Flash,
StillImage, StillImage,
VideoAttachment VideoAttachment,
}, },
computed: { computed: {
classNames() { classNames() {
@ -74,11 +78,11 @@ const Attachment = {
'-loading': this.loading, '-loading': this.loading,
'-nsfw-placeholder': this.hidden, '-nsfw-placeholder': this.hidden,
'-editable': this.edit !== undefined, '-editable': this.edit !== undefined,
'-compact': this.compact '-compact': this.compact,
}, },
'-type-' + this.type, '-type-' + this.type,
this.size && '-size-' + this.size, this.size && '-size-' + this.size,
`-${this.useContainFit ? 'contain' : 'cover'}-fit` `-${this.useContainFit ? 'contain' : 'cover'}-fit`,
] ]
}, },
usePlaceholder() { usePlaceholder() {
@ -109,7 +113,7 @@ const Attachment = {
return this.nsfw && this.hideNsfwLocal && !this.showHidden return this.nsfw && this.hideNsfwLocal && !this.showHidden
}, },
isEmpty() { isEmpty() {
return (this.type === 'html' && !this.attachment.oembed) return this.type === 'html' && !this.attachment.oembed
}, },
useModal() { useModal() {
let modalTypes = [] let modalTypes = []
@ -129,7 +133,7 @@ const Attachment = {
videoTag() { videoTag() {
return this.useModal ? 'button' : 'span' return this.useModal ? 'button' : 'span'
}, },
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig']),
}, },
watch: { watch: {
'attachment.description'(newVal) { 'attachment.description'(newVal) {
@ -137,7 +141,7 @@ const Attachment = {
}, },
localDescription(newVal) { localDescription(newVal) {
this.onEdit(newVal) this.onEdit(newVal)
} },
}, },
methods: { methods: {
linkClicked({ target }) { linkClicked({ target }) {
@ -180,7 +184,8 @@ const Attachment = {
}, },
toggleHidden(event) { toggleHidden(event) {
if ( if (
(this.mergedConfig.useOneClickNsfw && !this.showHidden) && this.mergedConfig.useOneClickNsfw &&
!this.showHidden &&
(this.type !== 'video' || this.mergedConfig.playVideosInModal) (this.type !== 'video' || this.mergedConfig.playVideosInModal)
) { ) {
this.openModal(event) this.openModal(event)
@ -205,8 +210,8 @@ const Attachment = {
const width = image.naturalWidth const width = image.naturalWidth
const height = image.naturalHeight const height = image.naturalHeight
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height }) this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height })
} },
} },
} }
export default Attachment export default Attachment

View file

@ -1,9 +1,10 @@
import { mapState } from 'pinia'
import { h, resolveComponent } from 'vue' import { h, resolveComponent } from 'vue'
import { useAuthFlowStore } from 'src/stores/auth_flow'
import LoginForm from '../login_form/login_form.vue' import LoginForm from '../login_form/login_form.vue'
import MFARecoveryForm from '../mfa_form/recovery_form.vue' import MFARecoveryForm from '../mfa_form/recovery_form.vue'
import MFATOTPForm from '../mfa_form/totp_form.vue' import MFATOTPForm from '../mfa_form/totp_form.vue'
import { mapState } from 'pinia'
import { useAuthFlowStore } from 'src/stores/auth_flow'
const AuthForm = { const AuthForm = {
name: 'AuthForm', name: 'AuthForm',
@ -12,17 +13,21 @@ const AuthForm = {
}, },
computed: { computed: {
authForm() { authForm() {
if (this.requiredTOTP) { return 'MFATOTPForm' } if (this.requiredTOTP) {
if (this.requiredRecovery) { return 'MFARecoveryForm' } return 'MFATOTPForm'
}
if (this.requiredRecovery) {
return 'MFARecoveryForm'
}
return 'LoginForm' return 'LoginForm'
}, },
...mapState(useAuthFlowStore, ['requiredTOTP', 'requiredRecovery']) ...mapState(useAuthFlowStore, ['requiredTOTP', 'requiredRecovery']),
}, },
components: { components: {
MFARecoveryForm, MFARecoveryForm,
MFATOTPForm, MFATOTPForm,
LoginForm LoginForm,
} },
} }
export default AuthForm export default AuthForm

View file

@ -2,35 +2,37 @@ const debounceMilliseconds = 500
export default { export default {
props: { props: {
query: { // function to query results and return a promise query: {
// function to query results and return a promise
type: Function, type: Function,
required: true required: true,
}, },
filter: { // function to filter results in real time filter: {
type: Function // function to filter results in real time
type: Function,
}, },
placeholder: { placeholder: {
type: String, type: String,
default: 'Search...' default: 'Search...',
} },
}, },
data() { data() {
return { return {
term: '', term: '',
timeout: null, timeout: null,
results: [], results: [],
resultsVisible: false resultsVisible: false,
} }
}, },
computed: { computed: {
filtered() { filtered() {
return this.filter ? this.filter(this.results) : this.results return this.filter ? this.filter(this.results) : this.results
} },
}, },
watch: { watch: {
term(val) { term(val) {
this.fetchResults(val) this.fetchResults(val)
} },
}, },
methods: { methods: {
fetchResults(term) { fetchResults(term) {
@ -38,7 +40,9 @@ export default {
this.timeout = setTimeout(() => { this.timeout = setTimeout(() => {
this.results = [] this.results = []
if (term) { if (term) {
this.query(term).then((results) => { this.results = results }) this.query(term).then((results) => {
this.results = results
})
} }
}, debounceMilliseconds) }, debounceMilliseconds)
}, },
@ -47,6 +51,6 @@ export default {
}, },
onClickOutside() { onClickOutside() {
this.resultsVisible = false this.resultsVisible = false
} },
} },
} }

View file

@ -1,21 +1,25 @@
import UserAvatar from '../user_avatar/user_avatar.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import UserAvatar from '../user_avatar/user_avatar.vue'
const AvatarList = { const AvatarList = {
props: ['users'], props: ['users'],
computed: { computed: {
slicedUsers() { slicedUsers() {
return this.users ? this.users.slice(0, 15) : [] return this.users ? this.users.slice(0, 15) : []
} },
}, },
components: { components: {
UserAvatar UserAvatar,
}, },
methods: { methods: {
userProfileLink(user) { userProfileLink(user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) return generateProfileLink(
} user.id,
} user.screen_name,
this.$store.state.instance.restrictedNicknames,
)
},
},
} }
export default AvatarList export default AvatarList

View file

@ -1,30 +1,27 @@
export default { export default {
name: 'Badge', name: 'Badge',
selector: '.badge', selector: '.badge',
validInnerComponents: [ validInnerComponents: ['Text', 'Icon'],
'Text',
'Icon'
],
variants: { variants: {
notification: '.-notification' notification: '.-notification',
}, },
defaultRules: [ defaultRules: [
{ {
component: 'Root', component: 'Root',
directives: { directives: {
'--badgeNotification': 'color | --cRed' '--badgeNotification': 'color | --cRed',
} },
}, },
{ {
directives: { directives: {
background: '--cGreen' background: '--cGreen',
} },
}, },
{ {
variant: 'notification', variant: 'notification',
directives: { directives: {
background: '--cRed' background: '--cRed',
} },
} },
] ],
} }

View file

@ -1,24 +1,26 @@
import UserPopover from '../user_popover/user_popover.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import UserLink from '../user_link/user_link.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import UserAvatar from '../user_avatar/user_avatar.vue'
import UserLink from '../user_link/user_link.vue'
import UserPopover from '../user_popover/user_popover.vue'
const BasicUserCard = { const BasicUserCard = {
props: [ props: ['user'],
'user'
],
components: { components: {
UserPopover, UserPopover,
UserAvatar, UserAvatar,
RichContent, RichContent,
UserLink UserLink,
}, },
methods: { methods: {
userProfileLink(user) { userProfileLink(user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) return generateProfileLink(
} user.id,
} user.screen_name,
this.$store.state.instance.restrictedNicknames,
)
},
},
} }
export default BasicUserCard export default BasicUserCard

View file

@ -20,14 +20,16 @@ const BlockCard = {
blockExpiry() { blockExpiry() {
return this.user.block_expires_at == null return this.user.block_expires_at == null
? this.$t('user_card.block_expires_forever') ? this.$t('user_card.block_expires_forever')
: this.$t('user_card.block_expires_at', [new Date(this.user.mute_expires_at).toLocaleString()]) : this.$t('user_card.block_expires_at', [
new Date(this.user.mute_expires_at).toLocaleString(),
])
}, },
...mapState({ ...mapState({
blockExpirationSupported: state => state.instance.blockExpiration, blockExpirationSupported: (state) => state.instance.blockExpiration,
}) }),
}, },
components: { components: {
BasicUserCard BasicUserCard,
}, },
methods: { methods: {
unblockUser() { unblockUser() {
@ -39,8 +41,8 @@ const BlockCard = {
} else { } else {
this.$store.dispatch('blockUser', { id: this.user.id }) this.$store.dispatch('blockUser', { id: this.user.id })
} }
} },
} },
} }
export default BlockCard export default BlockCard

View file

@ -1,22 +1,15 @@
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'
faEllipsisH
} from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faEllipsisH)
faEllipsisH
)
const BookmarkFolderCard = { const BookmarkFolderCard = {
props: [ props: ['folder', 'allBookmarks'],
'folder',
'allBookmarks'
],
computed: { computed: {
firstLetter() { firstLetter() {
return this.folder ? this.folder.name[0] : null return this.folder ? this.folder.name[0] : null
} },
} },
} }
export default BookmarkFolderCard export default BookmarkFolderCard

View file

@ -1,7 +1,7 @@
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import apiService from '../../services/api/api.service'
import { useInterfaceStore } from 'src/stores/interface'
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders' import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
import { useInterfaceStore } from 'src/stores/interface'
import apiService from '../../services/api/api.service'
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
const BookmarkFolderEdit = { const BookmarkFolderEdit = {
data() { data() {
@ -13,18 +13,17 @@ const BookmarkFolderEdit = {
emojiDraft: '', emojiDraft: '',
emojiUrlDraft: null, emojiUrlDraft: null,
emojiPickerExpanded: false, emojiPickerExpanded: false,
reallyDelete: false reallyDelete: false,
} }
}, },
components: { components: {
EmojiPicker EmojiPicker,
}, },
created() { created() {
if (!this.id) return if (!this.id) return
const credentials = this.$store.state.users.currentUser.credentials const credentials = this.$store.state.users.currentUser.credentials
apiService.fetchBookmarkFolders({ credentials }) apiService.fetchBookmarkFolders({ credentials }).then((folders) => {
.then((folders) => { const folder = folders.find((folder) => folder.id === this.id)
const folder = folders.find(folder => folder.id === this.id)
if (!folder) return if (!folder) return
this.nameDraft = this.name = folder.name this.nameDraft = this.name = folder.name
@ -35,7 +34,7 @@ const BookmarkFolderEdit = {
computed: { computed: {
id() { id() {
return this.$route.params.id return this.$route.params.id
} },
}, },
methods: { methods: {
selectEmoji(event) { selectEmoji(event) {
@ -54,13 +53,19 @@ const BookmarkFolderEdit = {
this.emojiPickerExpanded = false this.emojiPickerExpanded = false
}, },
updateFolder() { updateFolder() {
useBookmarkFoldersStore().updateBookmarkFolder({ folderId: this.id, name: this.nameDraft, emoji: this.emojiDraft }) useBookmarkFoldersStore()
.updateBookmarkFolder({
folderId: this.id,
name: this.nameDraft,
emoji: this.emojiDraft,
})
.then(() => { .then(() => {
this.$router.push({ name: 'bookmark-folders' }) this.$router.push({ name: 'bookmark-folders' })
}) })
}, },
createFolder() { createFolder() {
useBookmarkFoldersStore().createBookmarkFolder({ name: this.nameDraft, emoji: this.emojiDraft }) useBookmarkFoldersStore()
.createBookmarkFolder({ name: this.nameDraft, emoji: this.emojiDraft })
.then(() => { .then(() => {
this.$router.push({ name: 'bookmark-folders' }) this.$router.push({ name: 'bookmark-folders' })
}) })
@ -68,15 +73,15 @@ const BookmarkFolderEdit = {
useInterfaceStore().pushGlobalNotice({ useInterfaceStore().pushGlobalNotice({
messageKey: 'bookmark_folders.error', messageKey: 'bookmark_folders.error',
messageArgs: [e.message], messageArgs: [e.message],
level: 'error' level: 'error',
}) })
}) })
}, },
deleteFolder() { deleteFolder() {
useBookmarkFoldersStore().deleteBookmarkFolder({ folderId: this.id }) useBookmarkFoldersStore().deleteBookmarkFolder({ folderId: this.id })
this.$router.push({ name: 'bookmark-folders' }) this.$router.push({ name: 'bookmark-folders' })
} },
} },
} }
export default BookmarkFolderEdit export default BookmarkFolderEdit

View file

@ -1,19 +1,19 @@
import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue'
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders' import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue'
const BookmarkFolders = { const BookmarkFolders = {
data() { data() {
return { return {
isNew: false isNew: false,
} }
}, },
components: { components: {
BookmarkFolderCard BookmarkFolderCard,
}, },
computed: { computed: {
bookmarkFolders() { bookmarkFolders() {
return useBookmarkFoldersStore().allFolders return useBookmarkFoldersStore().allFolders
} },
}, },
methods: { methods: {
cancelNewFolder() { cancelNewFolder() {
@ -21,8 +21,8 @@ const BookmarkFolders = {
}, },
newFolder() { newFolder() {
this.isNew = true this.isNew = true
} },
} },
} }
export default BookmarkFolders export default BookmarkFolders

View file

@ -1,20 +1,19 @@
import { mapState } from 'pinia' import { mapState } from 'pinia'
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
import { getBookmarkFolderEntries } from 'src/components/navigation/filter.js' import { getBookmarkFolderEntries } from 'src/components/navigation/filter.js'
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders' import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
export const BookmarkFoldersMenuContent = { export const BookmarkFoldersMenuContent = {
props: [ props: ['showPin'],
'showPin'
],
components: { components: {
NavigationEntry NavigationEntry,
}, },
computed: { computed: {
...mapState(useBookmarkFoldersStore, { ...mapState(useBookmarkFoldersStore, {
folders: getBookmarkFolderEntries folders: getBookmarkFolderEntries,
}) }),
} },
} }
export default BookmarkFoldersMenuContent export default BookmarkFoldersMenuContent

View file

@ -3,10 +3,13 @@ import Timeline from '../timeline/timeline.vue'
const Bookmarks = { const Bookmarks = {
created() { created() {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' }) this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null }) this.$store.dispatch('startFetchingTimeline', {
timeline: 'bookmarks',
bookmarkFolderId: this.folderId || null,
})
}, },
components: { components: {
Timeline Timeline,
}, },
computed: { computed: {
folderId() { folderId() {
@ -14,19 +17,22 @@ const Bookmarks = {
}, },
timeline() { timeline() {
return this.$store.state.statuses.timelines.bookmarks return this.$store.state.statuses.timelines.bookmarks
} },
}, },
watch: { watch: {
folderId() { folderId() {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' }) this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks') this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null }) this.$store.dispatch('startFetchingTimeline', {
} timeline: 'bookmarks',
bookmarkFolderId: this.folderId || null,
})
},
}, },
unmounted() { unmounted() {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' }) this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks') this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
} },
} }
export default Bookmarks export default Bookmarks

View file

@ -6,8 +6,8 @@ export default {
{ {
directives: { directives: {
textColor: '$mod(--parent 10)', textColor: '$mod(--parent 10)',
textAuto: 'no-auto' textAuto: 'no-auto',
} },
} },
] ],
} }

View file

@ -1,18 +1,20 @@
import Timeline from '../timeline/timeline.vue' import Timeline from '../timeline/timeline.vue'
const BubbleTimeline = { const BubbleTimeline = {
components: { components: {
Timeline Timeline,
}, },
computed: { computed: {
timeline () { return this.$store.state.statuses.timelines.bubble } timeline() {
return this.$store.state.statuses.timelines.bubble
},
}, },
created() { created() {
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' }) this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
}, },
unmounted() { unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'bubble') this.$store.dispatch('stopFetchingTimeline', 'bubble')
} },
} }
export default BubbleTimeline export default BubbleTimeline

View file

@ -12,25 +12,22 @@ export default {
focused: ':focus-within', focused: ':focus-within',
pressed: ':active', pressed: ':active',
hover: ':is(:hover, :focus-visible):not(:disabled)', hover: ':is(:hover, :focus-visible):not(:disabled)',
disabled: ':disabled' disabled: ':disabled',
}, },
// Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it. // Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it.
variants: { variants: {
// Variants save on computation time since adding new variant just adds one more "set". // Variants save on computation time since adding new variant just adds one more "set".
// normal: '', // you can override normal variant, it will be appenended to the main class // normal: '', // you can override normal variant, it will be appenended to the main class
danger: '.-danger', danger: '.-danger',
transparent: '.-transparent' transparent: '.-transparent',
// Overall the compuation difficulty is N*((1/6)M^3+M) where M is number of distinct states and N is number of variants. // Overall the compuation difficulty is N*((1/6)M^3+M) where M is number of distinct states and N is number of variants.
// This (currently) is further multipled by number of places where component can exist. // This (currently) is further multipled by number of places where component can exist.
}, },
editor: { editor: {
aspect: '2 / 1' aspect: '2 / 1',
}, },
// This lists all other components that can possibly exist within one. Recursion is currently not supported (and probably won't be supported ever). // This lists all other components that can possibly exist within one. Recursion is currently not supported (and probably won't be supported ever).
validInnerComponents: [ validInnerComponents: ['Text', 'Icon'],
'Text',
'Icon'
],
// Default rules, used as "default theme", essentially. // Default rules, used as "default theme", essentially.
defaultRules: [ defaultRules: [
{ {
@ -39,9 +36,11 @@ export default {
'--buttonDefaultHoverGlow': 'shadow | 0 0 1 2 --text / 0.4', '--buttonDefaultHoverGlow': 'shadow | 0 0 1 2 --text / 0.4',
'--buttonDefaultFocusGlow': 'shadow | 0 0 1 2 --link / 0.5', '--buttonDefaultFocusGlow': 'shadow | 0 0 1 2 --link / 0.5',
'--buttonDefaultShadow': 'shadow | 0 0 2 #000000', '--buttonDefaultShadow': 'shadow | 0 0 2 #000000',
'--buttonDefaultBevel': 'shadow | $borderSide(#FFFFFF top 0.2 1), $borderSide(#000000 bottom 0.2 1)', '--buttonDefaultBevel':
'--buttonPressedBevel': 'shadow | inset 0 0 4 #000000, $borderSide(#FFFFFF bottom 0.2 1), $borderSide(#000000 top 0.2 1)' 'shadow | $borderSide(#FFFFFF top 0.2 1), $borderSide(#000000 bottom 0.2 1)',
} '--buttonPressedBevel':
'shadow | inset 0 0 4 #000000, $borderSide(#FFFFFF bottom 0.2 1), $borderSide(#000000 top 0.2 1)',
},
}, },
{ {
// component: 'Button', // no need to specify components every time unless you're specifying how other component should look // component: 'Button', // no need to specify components every time unless you're specifying how other component should look
@ -49,128 +48,128 @@ export default {
directives: { directives: {
background: '--fg', background: '--fg',
shadow: ['--buttonDefaultShadow', '--buttonDefaultBevel'], shadow: ['--buttonDefaultShadow', '--buttonDefaultBevel'],
roundness: 3 roundness: 3,
} },
}, },
{ {
variant: 'danger', variant: 'danger',
directives: { directives: {
background: '--cRed' background: '--cRed',
} },
}, },
{ {
variant: 'transparent', variant: 'transparent',
directives: { directives: {
opacity: 0.5 opacity: 0.5,
} },
}, },
{ {
component: 'Text', component: 'Text',
parent: { parent: {
component: 'Button', component: 'Button',
variant: 'transparent' variant: 'transparent',
}, },
directives: { directives: {
textColor: '--text' textColor: '--text',
} },
}, },
{ {
component: 'Icon', component: 'Icon',
parent: { parent: {
component: 'Button', component: 'Button',
variant: 'transparent' variant: 'transparent',
}, },
directives: { directives: {
textColor: '--text' textColor: '--text',
} },
}, },
{ {
state: ['hover'], state: ['hover'],
directives: { directives: {
shadow: ['--buttonDefaultHoverGlow', '--buttonDefaultBevel'] shadow: ['--buttonDefaultHoverGlow', '--buttonDefaultBevel'],
} },
}, },
{ {
state: ['focused'], state: ['focused'],
directives: { directives: {
shadow: ['--buttonDefaultFocusGlow', '--buttonDefaultBevel'] shadow: ['--buttonDefaultFocusGlow', '--buttonDefaultBevel'],
} },
}, },
{ {
state: ['pressed'], state: ['pressed'],
directives: { directives: {
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel'] shadow: ['--buttonDefaultShadow', '--buttonPressedBevel'],
} },
}, },
{ {
state: ['pressed', 'hover'], state: ['pressed', 'hover'],
directives: { directives: {
shadow: ['--buttonPressedBevel', '--buttonDefaultHoverGlow'] shadow: ['--buttonPressedBevel', '--buttonDefaultHoverGlow'],
} },
}, },
{ {
state: ['toggled'], state: ['toggled'],
directives: { directives: {
background: '--accent,-24.2', background: '--accent,-24.2',
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel'] shadow: ['--buttonDefaultShadow', '--buttonPressedBevel'],
} },
}, },
{ {
state: ['toggled', 'hover'], state: ['toggled', 'hover'],
directives: { directives: {
background: '--accent,-24.2', background: '--accent,-24.2',
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'] shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'],
} },
}, },
{ {
state: ['toggled', 'focused'], state: ['toggled', 'focused'],
directives: { directives: {
background: '--accent,-24.2', background: '--accent,-24.2',
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'] shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'],
} },
}, },
{ {
state: ['toggled', 'hover', 'focused'], state: ['toggled', 'hover', 'focused'],
directives: { directives: {
background: '--accent,-24.2', background: '--accent,-24.2',
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'] shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'],
} },
}, },
{ {
state: ['toggled', 'disabled'], state: ['toggled', 'disabled'],
directives: { directives: {
background: '$blend(--accent 0.25 --parent)', background: '$blend(--accent 0.25 --parent)',
shadow: ['--buttonPressedBevel'] shadow: ['--buttonPressedBevel'],
} },
}, },
{ {
state: ['disabled'], state: ['disabled'],
directives: { directives: {
background: '$blend(--inheritedBackground 0.25 --parent)', background: '$blend(--inheritedBackground 0.25 --parent)',
shadow: ['--buttonDefaultBevel'] shadow: ['--buttonDefaultBevel'],
} },
}, },
{ {
component: 'Text', component: 'Text',
parent: { parent: {
component: 'Button', component: 'Button',
state: ['disabled'] state: ['disabled'],
}, },
directives: { directives: {
textOpacity: 0.25, textOpacity: 0.25,
textOpacityMode: 'blend' textOpacityMode: 'blend',
} },
}, },
{ {
component: 'Icon', component: 'Icon',
parent: { parent: {
component: 'Button', component: 'Button',
state: ['disabled'] state: ['disabled'],
}, },
directives: { directives: {
textOpacity: 0.25, textOpacity: 0.25,
textOpacityMode: 'blend' textOpacityMode: 'blend',
} },
} },
] ],
} }

View file

@ -7,91 +7,86 @@ export default {
toggled: '.toggled', toggled: '.toggled',
disabled: ':disabled', disabled: ':disabled',
hover: ':is(:hover, :focus-visible):not(:disabled)', hover: ':is(:hover, :focus-visible):not(:disabled)',
focused: ':focus-within:not(:is(:focus-visible))' focused: ':focus-within:not(:is(:focus-visible))',
}, },
validInnerComponents: [ validInnerComponents: ['Text', 'Link', 'Icon', 'Badge'],
'Text',
'Link',
'Icon',
'Badge'
],
defaultRules: [ defaultRules: [
{ {
directives: { directives: {
shadow: [] shadow: [],
} },
}, },
{ {
component: 'Icon', component: 'Icon',
parent: { parent: {
component: 'ButtonUnstyled', component: 'ButtonUnstyled',
state: ['hover'] state: ['hover'],
}, },
directives: { directives: {
textColor: '--parent--text' textColor: '--parent--text',
} },
}, },
{ {
component: 'Icon', component: 'Icon',
parent: { parent: {
component: 'ButtonUnstyled', component: 'ButtonUnstyled',
state: ['toggled'] state: ['toggled'],
}, },
directives: { directives: {
textColor: '--parent--text' textColor: '--parent--text',
} },
}, },
{ {
component: 'Icon', component: 'Icon',
parent: { parent: {
component: 'ButtonUnstyled', component: 'ButtonUnstyled',
state: ['toggled', 'hover'] state: ['toggled', 'hover'],
}, },
directives: { directives: {
textColor: '--parent--text' textColor: '--parent--text',
} },
}, },
{ {
component: 'Icon', component: 'Icon',
parent: { parent: {
component: 'ButtonUnstyled', component: 'ButtonUnstyled',
state: ['toggled', 'focused'] state: ['toggled', 'focused'],
}, },
directives: { directives: {
textColor: '--parent--text' textColor: '--parent--text',
} },
}, },
{ {
component: 'Icon', component: 'Icon',
parent: { parent: {
component: 'ButtonUnstyled', component: 'ButtonUnstyled',
state: ['toggled', 'focused', 'hover'] state: ['toggled', 'focused', 'hover'],
}, },
directives: { directives: {
textColor: '--parent--text' textColor: '--parent--text',
} },
}, },
{ {
component: 'Text', component: 'Text',
parent: { parent: {
component: 'ButtonUnstyled', component: 'ButtonUnstyled',
state: ['disabled'] state: ['disabled'],
}, },
directives: { directives: {
textOpacity: 0.25, textOpacity: 0.25,
textOpacityMode: 'blend' textOpacityMode: 'blend',
} },
}, },
{ {
component: 'Icon', component: 'Icon',
parent: { parent: {
component: 'ButtonUnstyled', component: 'ButtonUnstyled',
state: ['disabled'] state: ['disabled'],
}, },
directives: { directives: {
textOpacity: 0.25, textOpacity: 0.25,
textOpacityMode: 'blend' textOpacityMode: 'blend',
} },
} },
] ],
} }

View file

@ -1,25 +1,26 @@
import _ from 'lodash' import _ from 'lodash'
import { WSConnectionStatus } from '../../services/api/api.service.js'
import { mapGetters, mapState } from 'vuex'
import { mapState as mapPiniaState } from 'pinia' import { mapState as mapPiniaState } from 'pinia'
import ChatMessage from '../chat_message/chat_message.vue' import { mapGetters, mapState } from 'vuex'
import PostStatusForm from '../post_status_form/post_status_form.vue'
import ChatTitle from '../chat_title/chat_title.vue'
import chatService from '../../services/chat_service/chat_service.js'
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
import { getScrollPosition, getNewTopPosition, isBottomedOut, isScrollable } from './chat_layout_utils.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faChevronDown,
faChevronLeft
} from '@fortawesome/free-solid-svg-icons'
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
import { useInterfaceStore } from 'src/stores/interface.js'
library.add( import { useInterfaceStore } from 'src/stores/interface.js'
faChevronDown, import { WSConnectionStatus } from '../../services/api/api.service.js'
faChevronLeft import chatService from '../../services/chat_service/chat_service.js'
) import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
import ChatMessage from '../chat_message/chat_message.vue'
import ChatTitle from '../chat_title/chat_title.vue'
import PostStatusForm from '../post_status_form/post_status_form.vue'
import {
getNewTopPosition,
getScrollPosition,
isBottomedOut,
isScrollable,
} from './chat_layout_utils.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
library.add(faChevronDown, faChevronLeft)
const BOTTOMED_OUT_OFFSET = 10 const BOTTOMED_OUT_OFFSET = 10
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 10 const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 10
@ -31,7 +32,7 @@ const Chat = {
components: { components: {
ChatMessage, ChatMessage,
ChatTitle, ChatTitle,
PostStatusForm PostStatusForm,
}, },
data() { data() {
return { return {
@ -40,7 +41,7 @@ const Chat = {
lastScrollPosition: {}, lastScrollPosition: {},
scrollableContainerHeight: '100%', scrollableContainerHeight: '100%',
errorLoadingChat: false, errorLoadingChat: false,
messageRetriers: {} messageRetriers: {},
} }
}, },
created() { created() {
@ -50,7 +51,11 @@ const Chat = {
mounted() { mounted() {
window.addEventListener('scroll', this.handleScroll) window.addEventListener('scroll', this.handleScroll)
if (typeof document.hidden !== 'undefined') { if (typeof document.hidden !== 'undefined') {
document.addEventListener('visibilitychange', this.handleVisibilityChange, false) document.addEventListener(
'visibilitychange',
this.handleVisibilityChange,
false,
)
} }
this.$nextTick(() => { this.$nextTick(() => {
@ -60,7 +65,12 @@ const Chat = {
unmounted() { unmounted() {
window.removeEventListener('scroll', this.handleScroll) window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('resize', this.handleResize) window.removeEventListener('resize', this.handleResize)
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false) if (typeof document.hidden !== 'undefined')
document.removeEventListener(
'visibilitychange',
this.handleVisibilityChange,
false,
)
this.$store.dispatch('clearCurrentChat') this.$store.dispatch('clearCurrentChat')
}, },
computed: { computed: {
@ -72,7 +82,9 @@ const Chat = {
}, },
formPlaceholder() { formPlaceholder() {
if (this.recipient) { if (this.recipient) {
return this.$t('chats.message_user', { nickname: this.recipient.screen_name_ui }) return this.$t('chats.message_user', {
nickname: this.recipient.screen_name_ui,
})
} else { } else {
return '' return ''
} }
@ -81,25 +93,31 @@ const Chat = {
return chatService.getView(this.currentChatMessageService) return chatService.getView(this.currentChatMessageService)
}, },
newMessageCount() { newMessageCount() {
return this.currentChatMessageService && this.currentChatMessageService.newMessageCount return (
this.currentChatMessageService &&
this.currentChatMessageService.newMessageCount
)
}, },
streamingEnabled() { streamingEnabled() {
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED return (
this.mergedConfig.useStreamingApi &&
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
)
}, },
...mapGetters([ ...mapGetters([
'currentChat', 'currentChat',
'currentChatMessageService', 'currentChatMessageService',
'findOpenedChatByRecipientId', 'findOpenedChatByRecipientId',
'mergedConfig' 'mergedConfig',
]), ]),
...mapPiniaState(useInterfaceStore, { ...mapPiniaState(useInterfaceStore, {
mobileLayout: store => store.layoutType === 'mobile' mobileLayout: (store) => store.layoutType === 'mobile',
}), }),
...mapState({ ...mapState({
backendInteractor: state => state.api.backendInteractor, backendInteractor: (state) => state.api.backendInteractor,
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus, mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
currentUser: state => state.users.currentUser currentUser: (state) => state.users.currentUser,
}) }),
}, },
watch: { watch: {
chatViewItems() { chatViewItems() {
@ -119,7 +137,7 @@ const Chat = {
if (newValue === WSConnectionStatus.JOINED) { if (newValue === WSConnectionStatus.JOINED) {
this.fetchChat({ isFirstFetch: true }) this.fetchChat({ isFirstFetch: true })
} }
} },
}, },
methods: { methods: {
// Used to animate the avatar near the first message of the message chain when any message belonging to the chain is hovered // Used to animate the avatar near the first message of the message chain when any message belonging to the chain is hovered
@ -163,19 +181,30 @@ const Chat = {
scrollDown(options = {}) { scrollDown(options = {}) {
const { behavior = 'auto', forceRead = false } = options const { behavior = 'auto', forceRead = false } = options
this.$nextTick(() => { this.$nextTick(() => {
window.scrollTo({ top: document.documentElement.scrollHeight, behavior }) window.scrollTo({
top: document.documentElement.scrollHeight,
behavior,
})
}) })
if (forceRead) { if (forceRead) {
this.readChat() this.readChat()
} }
}, },
readChat() { readChat() {
if (!(this.currentChatMessageService && this.currentChatMessageService.maxId)) { return } if (
if (document.hidden) { return } !(
this.currentChatMessageService && this.currentChatMessageService.maxId
)
) {
return
}
if (document.hidden) {
return
}
const lastReadId = this.currentChatMessageService.maxId const lastReadId = this.currentChatMessageService.maxId
this.$store.dispatch('readChat', { this.$store.dispatch('readChat', {
id: this.currentChat.id, id: this.currentChat.id,
lastReadId lastReadId,
}) })
}, },
bottomedOut(offset) { bottomedOut(offset) {
@ -187,13 +216,18 @@ const Chat = {
cullOlderCheck() { cullOlderCheck() {
window.setTimeout(() => { window.setTimeout(() => {
if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) { if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
this.$store.dispatch('cullOlderMessages', this.currentChatMessageService.chatId) this.$store.dispatch(
'cullOlderMessages',
this.currentChatMessageService.chatId,
)
} }
}, 5000) }, 5000)
}, },
handleScroll: _.throttle(function () { handleScroll: _.throttle(function () {
this.lastScrollPosition = getScrollPosition() this.lastScrollPosition = getScrollPosition()
if (!this.currentChat) { return } if (!this.currentChat) {
return
}
if (this.reachedTop()) { if (this.reachedTop()) {
this.fetchChat({ maxId: this.currentChatMessageService.minId }) this.fetchChat({ maxId: this.currentChatMessageService.minId })
@ -216,19 +250,24 @@ const Chat = {
handleScrollUp(positionBeforeLoading) { handleScrollUp(positionBeforeLoading) {
const positionAfterLoading = getScrollPosition() const positionAfterLoading = getScrollPosition()
window.scrollTo({ window.scrollTo({
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading) top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
}) })
}, },
fetchChat({ isFirstFetch = false, fetchLatest = false, maxId }) { fetchChat({ isFirstFetch = false, fetchLatest = false, maxId }) {
const chatMessageService = this.currentChatMessageService const chatMessageService = this.currentChatMessageService
if (!chatMessageService) { return } if (!chatMessageService) {
if (fetchLatest && this.streamingEnabled) { return } return
}
if (fetchLatest && this.streamingEnabled) {
return
}
const chatId = chatMessageService.chatId const chatId = chatMessageService.chatId
const fetchOlderMessages = !!maxId const fetchOlderMessages = !!maxId
const sinceId = fetchLatest && chatMessageService.maxId const sinceId = fetchLatest && chatMessageService.maxId
return this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId }) return this.backendInteractor
.chatMessages({ id: chatId, maxId, sinceId })
.then((messages) => { .then((messages) => {
// Clear the current chat in case we're recovering from a ws connection loss. // Clear the current chat in case we're recovering from a ws connection loss.
if (isFirstFetch) { if (isFirstFetch) {
@ -236,7 +275,9 @@ const Chat = {
} }
const positionBeforeUpdate = getScrollPosition() const positionBeforeUpdate = getScrollPosition()
this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => { this.$store
.dispatch('addChatMessages', { chatId, messages })
.then(() => {
this.$nextTick(() => { this.$nextTick(() => {
if (fetchOlderMessages) { if (fetchOlderMessages) {
this.handleScrollUp(positionBeforeUpdate) this.handleScrollUp(positionBeforeUpdate)
@ -247,7 +288,9 @@ const Chat = {
// If this is the case, we want to fetch the messages until the scrollable container // If this is the case, we want to fetch the messages until the scrollable container
// is fully populated so that the user has the ability to scroll up and load the history. // is fully populated so that the user has the ability to scroll up and load the history.
if (!isScrollable() && messages.length > 0) { if (!isScrollable() && messages.length > 0) {
this.fetchChat({ maxId: this.currentChatMessageService.minId }) this.fetchChat({
maxId: this.currentChatMessageService.minId,
})
} }
}) })
}) })
@ -257,7 +300,9 @@ const Chat = {
let chat = this.findOpenedChatByRecipientId(this.recipientId) let chat = this.findOpenedChatByRecipientId(this.recipientId)
if (!chat) { if (!chat) {
try { try {
chat = await this.backendInteractor.getOrCreateChat({ accountId: this.recipientId }) chat = await this.backendInteractor.getOrCreateChat({
accountId: this.recipientId,
})
} catch (e) { } catch (e) {
console.error('Error creating or getting a chat', e) console.error('Error creating or getting a chat', e)
this.errorLoadingChat = true this.errorLoadingChat = true
@ -273,7 +318,8 @@ const Chat = {
}, },
doStartFetching() { doStartFetching() {
this.$store.dispatch('startFetchingCurrentChat', { this.$store.dispatch('startFetchingCurrentChat', {
fetcher: () => promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000) fetcher: () =>
promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000),
}) })
this.fetchChat({ isFirstFetch: true }) this.fetchChat({ isFirstFetch: true })
}, },
@ -289,7 +335,7 @@ const Chat = {
const params = { const params = {
id: this.currentChat.id, id: this.currentChat.id,
content: status, content: status,
idempotencyKey idempotencyKey,
} }
if (media[0]) { if (media[0]) {
@ -301,42 +347,59 @@ const Chat = {
chatId: this.currentChat.id, chatId: this.currentChat.id,
content: status, content: status,
userId: this.currentUser.id, userId: this.currentUser.id,
idempotencyKey idempotencyKey,
}) })
this.$store.dispatch('addChatMessages', { this.$store
.dispatch('addChatMessages', {
chatId: this.currentChat.id, chatId: this.currentChat.id,
messages: [fakeMessage] messages: [fakeMessage],
}).then(() => { })
.then(() => {
this.handleAttachmentPosting() this.handleAttachmentPosting()
}) })
return this.doSendMessage({ params, fakeMessage, retriesLeft: MAX_RETRIES }) return this.doSendMessage({
params,
fakeMessage,
retriesLeft: MAX_RETRIES,
})
}, },
doSendMessage({ params, fakeMessage, retriesLeft = MAX_RETRIES }) { doSendMessage({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
if (retriesLeft <= 0) return if (retriesLeft <= 0) return
this.backendInteractor.sendChatMessage(params) this.backendInteractor
.then(data => { .sendChatMessage(params)
.then((data) => {
this.$store.dispatch('addChatMessages', { this.$store.dispatch('addChatMessages', {
chatId: this.currentChat.id, chatId: this.currentChat.id,
updateMaxId: false, updateMaxId: false,
messages: [{ ...data, fakeId: fakeMessage.id }] messages: [{ ...data, fakeId: fakeMessage.id }],
}) })
return data return data
}) })
.catch(error => { .catch((error) => {
console.error('Error sending message', error) console.error('Error sending message', error)
this.$store.dispatch('handleMessageError', { this.$store.dispatch('handleMessageError', {
chatId: this.currentChat.id, chatId: this.currentChat.id,
fakeId: fakeMessage.id, fakeId: fakeMessage.id,
isRetry: retriesLeft !== MAX_RETRIES isRetry: retriesLeft !== MAX_RETRIES,
}) })
if ((error.statusCode >= 500 && error.statusCode < 600) || error.message === 'Failed to fetch') { if (
this.messageRetriers[fakeMessage.id] = setTimeout(() => { (error.statusCode >= 500 && error.statusCode < 600) ||
this.doSendMessage({ params, fakeMessage, retriesLeft: retriesLeft - 1 }) error.message === 'Failed to fetch'
}, 1000 * (2 ** (MAX_RETRIES - retriesLeft))) ) {
this.messageRetriers[fakeMessage.id] = setTimeout(
() => {
this.doSendMessage({
params,
fakeMessage,
retriesLeft: retriesLeft - 1,
})
},
1000 * 2 ** (MAX_RETRIES - retriesLeft),
)
} }
return {} return {}
}) })
@ -344,9 +407,12 @@ const Chat = {
return Promise.resolve(fakeMessage) return Promise.resolve(fakeMessage)
}, },
goBack() { goBack() {
this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } }) this.$router.push({
} name: 'chats',
} params: { username: this.currentUser.screen_name },
})
},
},
} }
export default Chat export default Chat

View file

@ -1,19 +1,13 @@
export default { export default {
name: 'Chat', name: 'Chat',
selector: '.chat-message-list', selector: '.chat-message-list',
validInnerComponents: [ validInnerComponents: ['Text', 'Link', 'Icon', 'Avatar', 'ChatMessage'],
'Text',
'Link',
'Icon',
'Avatar',
'ChatMessage'
],
defaultRules: [ defaultRules: [
{ {
directives: { directives: {
background: '--bg', background: '--bg',
blur: '5px' blur: '5px',
} },
} },
] ],
} }

View file

@ -3,14 +3,17 @@ export const getScrollPosition = () => {
return { return {
scrollTop: window.scrollY, scrollTop: window.scrollY,
scrollHeight: document.documentElement.scrollHeight, scrollHeight: document.documentElement.scrollHeight,
offsetHeight: window.innerHeight offsetHeight: window.innerHeight,
} }
} }
// A helper function that is used to keep the scroll position fixed as the new elements are added to the top // A helper function that is used to keep the scroll position fixed as the new elements are added to the top
// Takes two scroll positions, before and after the update. // Takes two scroll positions, before and after the update.
export const getNewTopPosition = (previousPosition, newPosition) => { export const getNewTopPosition = (previousPosition, newPosition) => {
return previousPosition.scrollTop + (newPosition.scrollHeight - previousPosition.scrollHeight) return (
previousPosition.scrollTop +
(newPosition.scrollHeight - previousPosition.scrollHeight)
)
} }
export const isBottomedOut = (offset = 0) => { export const isBottomedOut = (offset = 0) => {

View file

@ -1,4 +1,5 @@
import { mapState, mapGetters } from 'vuex' import { mapGetters, mapState } from 'vuex'
import ChatListItem from '../chat_list_item/chat_list_item.vue' import ChatListItem from '../chat_list_item/chat_list_item.vue'
import ChatNew from '../chat_new/chat_new.vue' import ChatNew from '../chat_new/chat_new.vue'
import List from '../list/list.vue' import List from '../list/list.vue'
@ -7,17 +8,17 @@ const ChatList = {
components: { components: {
ChatListItem, ChatListItem,
List, List,
ChatNew ChatNew,
}, },
computed: { computed: {
...mapState({ ...mapState({
currentUser: state => state.users.currentUser currentUser: (state) => state.users.currentUser,
}), }),
...mapGetters(['sortedChatList']) ...mapGetters(['sortedChatList']),
}, },
data() { data() {
return { return {
isNew: false isNew: false,
} }
}, },
created() { created() {
@ -30,8 +31,8 @@ const ChatList = {
}, },
newChat() { newChat() {
this.isNew = true this.isNew = true
} },
} },
} }
export default ChatList export default ChatList

View file

@ -1,31 +1,34 @@
import { mapState } from 'vuex' import { mapState } from 'vuex'
import StatusBody from '../status_content/status_content.vue'
import fileType from 'src/services/file_type/file_type.service' import fileType from 'src/services/file_type/file_type.service'
import UserAvatar from '../user_avatar/user_avatar.vue'
import AvatarList from '../avatar_list/avatar_list.vue' import AvatarList from '../avatar_list/avatar_list.vue'
import Timeago from '../timeago/timeago.vue'
import ChatTitle from '../chat_title/chat_title.vue' import ChatTitle from '../chat_title/chat_title.vue'
import StatusBody from '../status_content/status_content.vue'
import Timeago from '../timeago/timeago.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
const ChatListItem = { const ChatListItem = {
name: 'ChatListItem', name: 'ChatListItem',
props: [ props: ['chat'],
'chat'
],
components: { components: {
UserAvatar, UserAvatar,
AvatarList, AvatarList,
Timeago, Timeago,
ChatTitle, ChatTitle,
StatusBody StatusBody,
}, },
computed: { computed: {
...mapState({ ...mapState({
currentUser: state => state.users.currentUser currentUser: (state) => state.users.currentUser,
}), }),
attachmentInfo() { attachmentInfo() {
if (this.chat.lastMessage.attachments.length === 0) { return } if (this.chat.lastMessage.attachments.length === 0) {
return
}
const types = this.chat.lastMessage.attachments.map(file => fileType.fileType(file.mimetype)) const types = this.chat.lastMessage.attachments.map((file) =>
fileType.fileType(file.mimetype),
)
if (types.includes('video')) { if (types.includes('video')) {
return this.$t('file_type.video') return this.$t('file_type.video')
} else if (types.includes('audio')) { } else if (types.includes('audio')) {
@ -40,17 +43,19 @@ const ChatListItem = {
const message = this.chat.lastMessage const message = this.chat.lastMessage
const messageEmojis = message ? message.emojis : [] const messageEmojis = message ? message.emojis : []
const isYou = message && message.account_id === this.currentUser.id const isYou = message && message.account_id === this.currentUser.id
const content = message ? (this.attachmentInfo || message.content) : '' const content = message ? this.attachmentInfo || message.content : ''
const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content const messagePreview = isYou
? `<i>${this.$t('chats.you')}</i> ${content}`
: content
return { return {
summary: '', summary: '',
emojis: messageEmojis, emojis: messageEmojis,
raw_html: messagePreview, raw_html: messagePreview,
text: messagePreview, text: messagePreview,
attachments: [] attachments: [],
}
} }
}, },
},
methods: { methods: {
openChat() { openChat() {
if (this.chat.id) { if (this.chat.id) {
@ -58,12 +63,12 @@ const ChatListItem = {
name: 'chat', name: 'chat',
params: { params: {
username: this.currentUser.screen_name, username: this.currentUser.screen_name,
recipient_id: this.chat.account.id recipient_id: this.chat.account.id,
} },
}) })
} }
} },
} },
} }
export default ChatListItem export default ChatListItem

View file

@ -1,24 +1,20 @@
import { mapState, mapGetters } from 'vuex'
import { mapState as mapPiniaState } from 'pinia' import { mapState as mapPiniaState } from 'pinia'
import Popover from '../popover/popover.vue' import { defineAsyncComponent } from 'vue'
import { mapGetters, mapState } from 'vuex'
import { useInterfaceStore } from 'src/stores/interface'
import Attachment from '../attachment/attachment.vue' import Attachment from '../attachment/attachment.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
import Gallery from '../gallery/gallery.vue' import Gallery from '../gallery/gallery.vue'
import LinkPreview from '../link-preview/link-preview.vue' import LinkPreview from '../link-preview/link-preview.vue'
import Popover from '../popover/popover.vue'
import StatusContent from '../status_content/status_content.vue' import StatusContent from '../status_content/status_content.vue'
import ChatMessageDate from '../chat_message_date/chat_message_date.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import { defineAsyncComponent } from 'vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faTimes,
faEllipsisH
} from '@fortawesome/free-solid-svg-icons'
import { useInterfaceStore } from 'src/stores/interface'
library.add( import { library } from '@fortawesome/fontawesome-svg-core'
faTimes, import { faEllipsisH, faTimes } from '@fortawesome/free-solid-svg-icons'
faEllipsisH
) library.add(faTimes, faEllipsisH)
const ChatMessage = { const ChatMessage = {
name: 'ChatMessage', name: 'ChatMessage',
@ -27,7 +23,7 @@ const ChatMessage = {
'edited', 'edited',
'noHeading', 'noHeading',
'chatViewItem', 'chatViewItem',
'hoveredMessageChain' 'hoveredMessageChain',
], ],
emits: ['hover'], emits: ['hover'],
components: { components: {
@ -38,13 +34,19 @@ const ChatMessage = {
Gallery, Gallery,
LinkPreview, LinkPreview,
ChatMessageDate, ChatMessageDate,
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue')) UserPopover: defineAsyncComponent(
() => import('../user_popover/user_popover.vue'),
),
}, },
computed: { computed: {
// Returns HH:MM (hours and minutes) in local time. // Returns HH:MM (hours and minutes) in local time.
createdAt() { createdAt() {
const time = this.chatViewItem.data.created_at const time = this.chatViewItem.data.created_at
return time.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hour12: false }) return time.toLocaleTimeString('en', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
})
}, },
isCurrentUser() { isCurrentUser() {
return this.message.account_id === this.currentUser.id return this.message.account_id === this.currentUser.id
@ -61,18 +63,18 @@ const ChatMessage = {
emojis: this.message.emojis, emojis: this.message.emojis,
raw_html: this.message.content || '', raw_html: this.message.content || '',
text: this.message.content || '', text: this.message.content || '',
attachments: this.message.attachments attachments: this.message.attachments,
} }
}, },
hasAttachment() { hasAttachment() {
return this.message.attachments.length > 0 return this.message.attachments.length > 0
}, },
...mapPiniaState(useInterfaceStore, { ...mapPiniaState(useInterfaceStore, {
betterShadow: store => store.browserSupport.cssFilter betterShadow: (store) => store.browserSupport.cssFilter,
}), }),
...mapState({ ...mapState({
currentUser: state => state.users.currentUser, currentUser: (state) => state.users.currentUser,
restrictedNicknames: state => state.instance.restrictedNicknames restrictedNicknames: (state) => state.instance.restrictedNicknames,
}), }),
popoverMarginStyle() { popoverMarginStyle() {
if (this.isCurrentUser) { if (this.isCurrentUser) {
@ -81,30 +83,33 @@ const ChatMessage = {
return { left: 50 } return { left: 50 }
} }
}, },
...mapGetters(['mergedConfig', 'findUser']) ...mapGetters(['mergedConfig', 'findUser']),
}, },
data() { data() {
return { return {
hovered: false, hovered: false,
menuOpened: false menuOpened: false,
} }
}, },
methods: { methods: {
onHover(bool) { onHover(bool) {
this.$emit('hover', { isHovered: bool, messageChainId: this.chatViewItem.messageChainId }) this.$emit('hover', {
isHovered: bool,
messageChainId: this.chatViewItem.messageChainId,
})
}, },
async deleteMessage() { async deleteMessage() {
const confirmed = window.confirm(this.$t('chats.delete_confirm')) const confirmed = window.confirm(this.$t('chats.delete_confirm'))
if (confirmed) { if (confirmed) {
await this.$store.dispatch('deleteChatMessage', { await this.$store.dispatch('deleteChatMessage', {
messageId: this.chatViewItem.data.id, messageId: this.chatViewItem.data.id,
chatId: this.chatViewItem.data.chat_id chatId: this.chatViewItem.data.chat_id,
}) })
} }
this.hovered = false this.hovered = false
this.menuOpened = false this.menuOpened = false
} },
} },
} }
export default ChatMessage export default ChatMessage

View file

@ -2,26 +2,21 @@ export default {
name: 'ChatMessage', name: 'ChatMessage',
selector: '.chat-message', selector: '.chat-message',
variants: { variants: {
outgoing: '.outgoing' outgoing: '.outgoing',
}, },
validInnerComponents: [ validInnerComponents: ['Text', 'Icon', 'Border', 'PollGraph'],
'Text',
'Icon',
'Border',
'PollGraph'
],
defaultRules: [ defaultRules: [
{ {
directives: { directives: {
background: '--bg, 2', background: '--bg, 2',
backgroundNoCssColor: 'yes' backgroundNoCssColor: 'yes',
} },
}, },
{ {
variant: 'outgoing', variant: 'outgoing',
directives: { directives: {
background: '--bg, 5' background: '--bg, 5',
} },
} },
] ],
} }

View file

@ -18,9 +18,12 @@ export default {
if (this.date.getTime() === today.getTime()) { if (this.date.getTime() === today.getTime()) {
return this.$t('display_date.today') return this.$t('display_date.today')
} else { } else {
return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' }) return this.date.toLocaleDateString(
} localeService.internalToBrowserLocale(this.$i18n.locale),
} { day: 'numeric', month: 'long' },
)
} }
},
},
} }
</script> </script>

View file

@ -1,37 +1,33 @@
import { mapState, mapGetters } from 'vuex' import { mapGetters, mapState } from 'vuex'
import BasicUserCard from '../basic_user_card/basic_user_card.vue' import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faSearch,
faChevronLeft
} from '@fortawesome/free-solid-svg-icons'
library.add( import { library } from '@fortawesome/fontawesome-svg-core'
faSearch, import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
faChevronLeft
) library.add(faSearch, faChevronLeft)
const chatNew = { const chatNew = {
components: { components: {
BasicUserCard, BasicUserCard,
UserAvatar UserAvatar,
}, },
data() { data() {
return { return {
suggestions: [], suggestions: [],
userIds: [], userIds: [],
loading: false, loading: false,
query: '' query: '',
} }
}, },
async created() { async created() {
const { chats } = await this.backendInteractor.chats() const { chats } = await this.backendInteractor.chats()
chats.forEach(chat => this.suggestions.push(chat.account)) chats.forEach((chat) => this.suggestions.push(chat.account))
}, },
computed: { computed: {
users() { users() {
return this.userIds.map(userId => this.findUser(userId)) return this.userIds.map((userId) => this.findUser(userId))
}, },
availableUsers() { availableUsers() {
if (this.query.length !== 0) { if (this.query.length !== 0) {
@ -41,10 +37,10 @@ const chatNew = {
} }
}, },
...mapState({ ...mapState({
currentUser: state => state.users.currentUser, currentUser: (state) => state.users.currentUser,
backendInteractor: state => state.api.backendInteractor backendInteractor: (state) => state.api.backendInteractor,
}), }),
...mapGetters(['findUser']) ...mapGetters(['findUser']),
}, },
methods: { methods: {
goBack() { goBack() {
@ -61,7 +57,7 @@ const chatNew = {
this.query = '' this.query = ''
}, },
removeUser(userId) { removeUser(userId) {
this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId) this.selectedUserIds = this.selectedUserIds.filter((id) => id !== userId)
}, },
search(query) { search(query) {
if (!query) { if (!query) {
@ -71,13 +67,14 @@ const chatNew = {
this.loading = true this.loading = true
this.userIds = [] this.userIds = []
this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts' }) this.$store
.then(data => { .dispatch('search', { q: query, resolve: true, type: 'accounts' })
.then((data) => {
this.loading = false this.loading = false
this.userIds = data.accounts.map(a => a.id) this.userIds = data.accounts.map((a) => a.id)
}) })
} },
} },
} }
export default chatNew export default chatNew

View file

@ -1,23 +1,24 @@
import UserAvatar from '../user_avatar/user_avatar.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import UserAvatar from '../user_avatar/user_avatar.vue'
export default { export default {
name: 'ChatTitle', name: 'ChatTitle',
components: { components: {
UserAvatar, UserAvatar,
RichContent, RichContent,
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue')) UserPopover: defineAsyncComponent(
() => import('../user_popover/user_popover.vue'),
),
}, },
props: [ props: ['user', 'withAvatar'],
'user', 'withAvatar'
],
computed: { computed: {
title() { title() {
return this.user ? this.user.screen_name_ui : '' return this.user ? this.user.screen_name_ui : ''
}, },
htmlTitle() { htmlTitle() {
return this.user ? this.user.name_html : '' return this.user ? this.user.name_html : ''
} },
} },
} }

View file

@ -36,30 +36,25 @@
<script> <script>
export default { export default {
props: [ props: ['radio', 'modelValue', 'indeterminate', 'disabled'],
'radio',
'modelValue',
'indeterminate',
'disabled'
],
emits: ['update:modelValue'], emits: ['update:modelValue'],
data: (vm) => ({ data: (vm) => ({
indeterminateTransitionFix: vm.indeterminate indeterminateTransitionFix: vm.indeterminate,
}), }),
watch: { watch: {
indeterminate(e) { indeterminate(e) {
if (e) { if (e) {
this.indeterminateTransitionFix = true this.indeterminateTransitionFix = true
} }
} },
}, },
methods: { methods: {
onTransitionEnd() { onTransitionEnd() {
if (!this.indeterminate) { if (!this.indeterminate) {
this.indeterminateTransitionFix = false this.indeterminateTransitionFix = false
} }
} },
} },
} }
</script> </script>

View file

@ -64,74 +64,71 @@
</div> </div>
</template> </template>
<script> <script>
import Checkbox from '../checkbox/checkbox.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js'
import { throttle } from 'lodash' import { throttle } from 'lodash'
import { library } from '@fortawesome/fontawesome-svg-core' import { hex2rgb } from '../../services/color_convert/color_convert.js'
import { import Checkbox from '../checkbox/checkbox.vue'
faEyeDropper
} from '@fortawesome/free-solid-svg-icons'
library.add( import { library } from '@fortawesome/fontawesome-svg-core'
faEyeDropper import { faEyeDropper } from '@fortawesome/free-solid-svg-icons'
)
library.add(faEyeDropper)
export default { export default {
components: { components: {
Checkbox Checkbox,
}, },
props: { props: {
// Name of color, used for identifying // Name of color, used for identifying
name: { name: {
required: true, required: true,
type: String type: String,
}, },
// Readable label // Readable label
label: { label: {
required: true, required: true,
type: String type: String,
}, },
// use unstyled, uh, style // use unstyled, uh, style
unstyled: { unstyled: {
required: false, required: false,
type: Boolean type: Boolean,
}, },
// Color value, should be required but vue cannot tell the difference // Color value, should be required but vue cannot tell the difference
// between "property missing" and "property set to undefined" // between "property missing" and "property set to undefined"
modelValue: { modelValue: {
required: false, required: false,
type: String, type: String,
default: undefined default: undefined,
}, },
// Color fallback to use when value is not defeind // Color fallback to use when value is not defeind
fallback: { fallback: {
required: false, required: false,
type: String, type: String,
default: undefined default: undefined,
}, },
// Disable the control // Disable the control
disabled: { disabled: {
required: false, required: false,
type: Boolean, type: Boolean,
default: false default: false,
}, },
// Show "optional" tickbox, for when value might become mandatory // Show "optional" tickbox, for when value might become mandatory
showOptionalCheckbox: { showOptionalCheckbox: {
required: false, required: false,
type: Boolean, type: Boolean,
default: true default: true,
}, },
// Force "optional" tickbox to hide // Force "optional" tickbox to hide
hideOptionalCheckbox: { hideOptionalCheckbox: {
required: false, required: false,
type: Boolean, type: Boolean,
default: false default: false,
}, },
compact: { compact: {
required: false, required: false,
type: Boolean type: Boolean,
} },
}, },
emits: ['update:modelValue'], emits: ['update:modelValue'],
computed: { computed: {
@ -145,14 +142,17 @@ export default {
return this.modelValue === 'transparent' return this.modelValue === 'transparent'
}, },
computedColor() { computedColor() {
return this.modelValue && (this.modelValue.startsWith('--') || this.modelValue.startsWith('$')) return (
} this.modelValue &&
(this.modelValue.startsWith('--') || this.modelValue.startsWith('$'))
)
},
}, },
methods: { methods: {
updateValue: throttle(function (value) { updateValue: throttle(function (value) {
this.$emit('update:modelValue', value) this.$emit('update:modelValue', value)
}, 100) }, 100),
} },
} }
</script> </script>
<style lang="scss" src="./color_input.scss"></style> <style lang="scss" src="./color_input.scss"></style>

View file

@ -1,13 +1,15 @@
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
import ColorInput from 'src/components/color_input/color_input.vue' import ColorInput from 'src/components/color_input/color_input.vue'
import genRandomSeed from 'src/services/random_seed/random_seed.service.js' import genRandomSeed from 'src/services/random_seed/random_seed.service.js'
import { createStyleSheet, adoptStyleSheets } from 'src/services/style_setter/style_setter.js' import {
adoptStyleSheets,
createStyleSheet,
} from 'src/services/style_setter/style_setter.js'
export default { export default {
components: { components: {
Checkbox, Checkbox,
ColorInput ColorInput,
}, },
props: [ props: [
'shadow', 'shadow',
@ -17,7 +19,7 @@ export default {
'previewCss', 'previewCss',
'disabled', 'disabled',
'invalid', 'invalid',
'noColorControl' 'noColorControl',
], ],
emits: ['update:shadow'], emits: ['update:shadow'],
data() { data() {
@ -25,7 +27,7 @@ export default {
colorOverride: undefined, colorOverride: undefined,
lightGrid: false, lightGrid: false,
zoom: 100, zoom: 100,
randomSeed: genRandomSeed() randomSeed: genRandomSeed(),
} }
}, },
mounted() { mounted() {
@ -34,7 +36,7 @@ export default {
computed: { computed: {
hideControls() { hideControls() {
return typeof this.shadow === 'string' return typeof this.shadow === 'string'
} },
}, },
watch: { watch: {
previewCss() { previewCss() {
@ -45,7 +47,7 @@ export default {
}, },
zoom() { zoom() {
this.update() this.update()
} },
}, },
methods: { methods: {
updateProperty(axis, value) { updateProperty(axis, value) {
@ -60,23 +62,25 @@ export default {
if (this.colorOverride) result.push(`--background: ${this.colorOverride}`) if (this.colorOverride) result.push(`--background: ${this.colorOverride}`)
const styleRule = [ const styleRule = [
'#component-preview-', this.randomSeed, ' {\n', '#component-preview-',
this.randomSeed,
' {\n',
'.preview-block {\n', '.preview-block {\n',
`zoom: ${this.zoom / 100};`, `zoom: ${this.zoom / 100};`,
this.previewStyle, this.previewStyle,
'\n}', '\n}',
'\n}' '\n}',
].join('') ].join('')
sheet.addRule(styleRule) sheet.addRule(styleRule)
sheet.addRule([ sheet.addRule(
'#component-preview-', this.randomSeed, ' {\n', ['#component-preview-', this.randomSeed, ' {\n', ...result, '\n}'].join(
...result, '',
'\n}' ),
].join('')) )
sheet.ready = true sheet.ready = true
adoptStyleSheets() adoptStyleSheets()
} },
} },
} }

View file

@ -9,30 +9,29 @@ import DialogModal from '../dialog_modal/dialog_modal.vue'
*/ */
const ConfirmModal = { const ConfirmModal = {
components: { components: {
DialogModal DialogModal,
}, },
props: { props: {
title: { title: {
type: String type: String,
}, },
cancelText: { cancelText: {
type: String type: String,
}, },
confirmText: { confirmText: {
type: String type: String,
} },
}, },
emits: ['cancelled', 'accepted'], emits: ['cancelled', 'accepted'],
computed: { computed: {},
},
methods: { methods: {
onCancel() { onCancel() {
this.$emit('cancelled') this.$emit('cancelled')
}, },
onAccept() { onAccept() {
this.$emit('accepted') this.$emit('accepted')
} },
} },
} }
export default ConfirmModal export default ConfirmModal

View file

@ -1,17 +1,17 @@
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import ConfirmModal from './confirm_modal.vue'
import Select from 'src/components/select/select.vue' import Select from 'src/components/select/select.vue'
import ConfirmModal from './confirm_modal.vue'
export default { export default {
props: ['type', 'user', 'status'], props: ['type', 'user', 'status'],
emits: ['hide', 'show', 'muted'], emits: ['hide', 'show', 'muted'],
data: () => ({ data: () => ({
showing: false showing: false,
}), }),
components: { components: {
ConfirmModal, ConfirmModal,
Select Select,
}, },
computed: { computed: {
domain() { domain() {
@ -28,19 +28,22 @@ export default {
return this.status.conversation_muted return this.status.conversation_muted
}, },
domainIsMuted() { domainIsMuted() {
return new Set(this.$store.state.users.currentUser.domainMutes).has(this.domain) return new Set(this.$store.state.users.currentUser.domainMutes).has(
this.domain,
)
}, },
shouldConfirm() { shouldConfirm() {
switch (this.type) { switch (this.type) {
case 'domain': { case 'domain': {
return this.mergedConfig.modalOnMuteDomain return this.mergedConfig.modalOnMuteDomain
} }
default: { // conversation default: {
// conversation
return this.mergedConfig.modalOnMuteConversation return this.mergedConfig.modalOnMuteConversation
} }
} }
}, },
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig']),
}, },
methods: { methods: {
optionallyPrompt() { optionallyPrompt() {
@ -79,6 +82,6 @@ export default {
} }
this.$emit('muted') this.$emit('muted')
this.hide() this.hide()
} },
} },
} }

View file

@ -63,54 +63,68 @@ import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faAdjust, faAdjust,
faExclamationTriangle, faExclamationTriangle,
faThumbsUp faThumbsUp,
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faAdjust, faExclamationTriangle, faThumbsUp)
faAdjust,
faExclamationTriangle,
faThumbsUp
)
export default { export default {
components: { components: {
Tooltip Tooltip,
}, },
props: { props: {
large: { large: {
required: false, required: false,
type: Boolean, type: Boolean,
default: false default: false,
}, },
// TODO: Make theme switcher compute theme initially so that contrast // TODO: Make theme switcher compute theme initially so that contrast
// component won't be called without contrast data // component won't be called without contrast data
contrast: { contrast: {
required: false, required: false,
type: Object, type: Object,
default: () => ({}) default: () => ({
/* no-op */
}),
}, },
showRatio: { showRatio: {
required: false, required: false,
type: Boolean, type: Boolean,
default: false default: false,
} },
}, },
computed: { computed: {
hint() { hint() {
const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad') const levelVal = this.contrast.aaa
? 'aaa'
: this.contrast.aa
? 'aa'
: 'bad'
const level = this.$t(`settings.style.common.contrast.level.${levelVal}`) const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
const context = this.$t('settings.style.common.contrast.context.text') const context = this.$t('settings.style.common.contrast.context.text')
const ratio = this.contrast.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() { hint_18pt() {
const levelVal = this.contrast.laaa ? 'aaa' : (this.contrast.laa ? 'aa' : 'bad') const levelVal = this.contrast.laaa
? 'aaa'
: this.contrast.laa
? 'aa'
: 'bad'
const level = this.$t(`settings.style.common.contrast.level.${levelVal}`) const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
const context = this.$t('settings.style.common.contrast.context.18pt') const context = this.$t('settings.style.common.contrast.context.18pt')
const ratio = this.contrast.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,
})
},
},
} }
</script> </script>

View file

@ -2,13 +2,13 @@ import Conversation from '../conversation/conversation.vue'
const conversationPage = { const conversationPage = {
components: { components: {
Conversation Conversation,
}, },
computed: { computed: {
statusId() { statusId() {
return this.$route.params.id return this.$route.params.id
} },
} },
} }
export default conversationPage export default conversationPage

View file

@ -1,25 +1,22 @@
import { reduce, filter, findIndex, clone, get } from 'lodash' import { clone, filter, findIndex, get, reduce } 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 { mapState as mapPiniaState } from 'pinia'
import { mapGetters, mapState } from 'vuex'
import { useInterfaceStore } from 'src/stores/interface'
import { WSConnectionStatus } from '../../services/api/api.service.js'
import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.vue' import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.vue'
import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue' import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue'
import { useInterfaceStore } from 'src/stores/interface' import Status from '../status/status.vue'
import ThreadTree from '../thread_tree/thread_tree.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faAngleDoubleDown, faAngleDoubleDown,
faAngleDoubleLeft, faAngleDoubleLeft,
faChevronLeft faChevronLeft,
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faAngleDoubleDown, faAngleDoubleLeft, faChevronLeft)
faAngleDoubleDown,
faAngleDoubleLeft,
faChevronLeft
)
const sortById = (a, b) => { const sortById = (a, b) => {
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
@ -43,12 +40,14 @@ const sortAndFilterConversation = (conversation, statusoid) => {
if (statusoid.type === 'retweet') { if (statusoid.type === 'retweet') {
conversation = filter( conversation = filter(
conversation, conversation,
(status) => (status.type === 'retweet' || status.id !== statusoid.retweeted_status.id) (status) =>
status.type === 'retweet' ||
status.id !== statusoid.retweeted_status.id,
) )
} else { } else {
conversation = filter(conversation, (status) => status.type !== 'retweet') conversation = filter(conversation, (status) => status.type !== 'retweet')
} }
return conversation.filter(_ => _).sort(sortById) return conversation.filter((_) => _).sort(sortById)
} }
const conversation = { const conversation = {
@ -59,7 +58,7 @@ const conversation = {
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden' threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
statusContentPropertiesObject: {}, statusContentPropertiesObject: {},
inlineDivePosition: null, inlineDivePosition: null,
loadStatusError: null loadStatusError: null,
} }
}, },
props: [ props: [
@ -69,7 +68,7 @@ const conversation = {
'pinnedStatusIdsObject', 'pinnedStatusIdsObject',
'inProfile', 'inProfile',
'profileUserId', 'profileUserId',
'virtualHidden' 'virtualHidden',
], ],
created() { created() {
if (this.isPage) { if (this.isPage) {
@ -85,7 +84,10 @@ const conversation = {
return maxDepth >= 1 ? maxDepth : 1 return maxDepth >= 1 ? maxDepth : 1
}, },
streamingEnabled() { streamingEnabled() {
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED return (
this.mergedConfig.useStreamingApi &&
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
)
}, },
displayStyle() { displayStyle() {
return this.$store.getters.mergedConfig.conversationDisplay return this.$store.getters.mergedConfig.conversationDisplay
@ -113,11 +115,12 @@ const conversation = {
}, },
suspendable() { suspendable() {
if (this.isTreeView) { if (this.isTreeView) {
return Object.entries(this.statusContentProperties) return Object.entries(this.statusContentProperties).every(
.every(([, prop]) => !prop.replying && prop.mediaPlaying.length === 0) ([, prop]) => !prop.replying && prop.mediaPlaying.length === 0,
)
} }
if (this.$refs.statusComponent && this.$refs.statusComponent[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 { } else {
return true return true
} }
@ -147,7 +150,9 @@ const conversation = {
return [this.status] 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 }) const statusIndex = findIndex(conversation, { id: this.originalStatusId })
if (statusIndex !== -1) { if (statusIndex !== -1) {
conversation[statusIndex] = this.status conversation[statusIndex] = this.status
@ -162,42 +167,57 @@ const conversation = {
}, {}) }, {})
}, },
threadTree() { threadTree() {
const reverseLookupTable = this.conversation.reduce((table, status, index) => { const reverseLookupTable = this.conversation.reduce(
(table, status, index) => {
table[status.id] = index table[status.id] = index
return table return table
}, {}) },
{},
)
const threads = this.conversation.reduce((a, cur) => { const threads = this.conversation.reduce(
(a, cur) => {
const id = cur.id const id = cur.id
a.forest[id] = this.getReplies(id) a.forest[id] = this.getReplies(id).map((s) => s.id)
.map(s => s.id)
return a return a
}, { },
forest: {} {
}) forest: {},
},
)
const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => { const walk = (forest, topLevel, depth = 0, processed = {}) =>
topLevel
.map((id) => {
if (processed[id]) { if (processed[id]) {
return [] return []
} }
processed[id] = true processed[id] = true
return [{ return [
{
status: this.conversation[reverseLookupTable[id]], status: this.conversation[reverseLookupTable[id]],
id, id,
depth depth,
}, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), []) },
}).reduce((a, b) => a.concat(b), []) 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 return linearized
}, },
replyIds() { replyIds() {
return this.conversation.map(k => k.id) return this.conversation
.map((k) => k.id)
.reduce((res, id) => { .reduce((res, id) => {
res[id] = (this.replies[id] || []).map(k => k.id) res[id] = (this.replies[id] || []).map((k) => k.id)
return res return res
}, {}) }, {})
}, },
@ -207,10 +227,14 @@ const conversation = {
if (sizes[id]) { if (sizes[id]) {
return 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] 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) => { return Object.keys(sizes).reduce((res, id) => {
res[id] = sizes[id] - 1 // exclude itself res[id] = sizes[id] - 1 // exclude itself
return res return res
@ -222,10 +246,14 @@ const conversation = {
if (depths[id]) { if (depths[id]) {
return 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] 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) => { return Object.keys(depths).reduce((res, id) => {
res[id] = depths[id] - 1 // exclude itself res[id] = depths[id] - 1 // exclude itself
return res return res
@ -238,8 +266,16 @@ const conversation = {
}, {}) }, {})
}, },
topLevel() { topLevel() {
const topLevel = this.conversation.reduce((tl, cur) => const topLevel = this.conversation.reduce(
tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation) (tl, cur) =>
tl.filter(
(k) =>
this.getReplies(cur.id)
.map((v) => v.id)
.indexOf(k.id) === -1,
),
this.conversation,
)
return topLevel return topLevel
}, },
otherTopLevelCount() { otherTopLevelCount() {
@ -265,25 +301,38 @@ const conversation = {
shouldShowAllConversationButton() { shouldShowAllConversationButton() {
// The "show all conversation" button tells the user that there exist // 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 // 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() { shouldShowAncestors() {
return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length return (
this.isTreeView &&
this.isExpanded &&
this.ancestorsOf(this.diveRoot).length
)
}, },
replies() { replies() {
let i = 1 let i = 1
return reduce(this.conversation, (result, { id, in_reply_to_status_id: irid }) => { return reduce(
this.conversation,
(result, { id, in_reply_to_status_id: irid }) => {
if (irid) { if (irid) {
result[irid] = result[irid] || [] result[irid] = result[irid] || []
result[irid].push({ result[irid].push({
name: `#${i}`, name: `#${i}`,
id id,
}) })
} }
i++ i++
return result return result
}, {}) },
{},
)
}, },
isExpanded() { isExpanded() {
return !!(this.expanded || this.isPage) return !!(this.expanded || this.isPage)
@ -300,7 +349,7 @@ const conversation = {
if (this.threadDisplayStatusObject[id]) { if (this.threadDisplayStatusObject[id]) {
return this.threadDisplayStatusObject[id] return this.threadDisplayStatusObject[id]
} }
if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) { if (depth - this.diveDepth <= this.maxDepthToShowByDefault) {
return 'showing' return 'showing'
} else { } else {
return 'hidden' return 'hidden'
@ -320,13 +369,13 @@ const conversation = {
expandingSubject: false, expandingSubject: false,
showingLongSubject: false, showingLongSubject: false,
isReplying: false, isReplying: false,
mediaPlaying: [] mediaPlaying: [],
} }
if (this.statusContentPropertiesObject[id]) { if (this.statusContentPropertiesObject[id]) {
return { return {
...def, ...def,
...this.statusContentPropertiesObject[id] ...this.statusContentPropertiesObject[id],
} }
} }
return def return def
@ -344,23 +393,27 @@ const conversation = {
}, },
...mapGetters(['mergedConfig']), ...mapGetters(['mergedConfig']),
...mapState({ ...mapState({
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
}), }),
...mapPiniaState(useInterfaceStore, { ...mapPiniaState(useInterfaceStore, {
mobileLayout: store => store.layoutType === 'mobile' mobileLayout: (store) => store.layoutType === 'mobile',
}) }),
}, },
components: { components: {
Status, Status,
ThreadTree, ThreadTree,
QuickFilterSettings, QuickFilterSettings,
QuickViewSettings QuickViewSettings,
}, },
watch: { watch: {
statusId(newVal, oldVal) { statusId(newVal, oldVal) {
const newConversationId = this.getConversationId(newVal) const newConversationId = this.getConversationId(newVal)
const oldConversationId = this.getConversationId(oldVal) const oldConversationId = this.getConversationId(oldVal)
if (newConversationId && oldConversationId && newConversationId === oldConversationId) { if (
newConversationId &&
oldConversationId &&
newConversationId === oldConversationId
) {
this.setHighlight(this.originalStatusId) this.setHighlight(this.originalStatusId)
} else { } else {
this.fetchConversation() this.fetchConversation()
@ -374,16 +427,17 @@ const conversation = {
} }
}, },
virtualHidden() { virtualHidden() {
this.$store.dispatch( this.$store.dispatch('setVirtualHeight', {
'setVirtualHeight', statusId: this.statusId,
{ statusId: this.statusId, height: `${this.$el.clientHeight}px` } height: `${this.$el.clientHeight}px`,
) })
} },
}, },
methods: { methods: {
fetchConversation() { fetchConversation() {
if (this.status) { if (this.status) {
this.$store.state.api.backendInteractor.fetchConversation({ id: this.statusId }) this.$store.state.api.backendInteractor
.fetchConversation({ id: this.statusId })
.then(({ ancestors, descendants }) => { .then(({ ancestors, descendants }) => {
this.$store.dispatch('addNewStatuses', { statuses: ancestors }) this.$store.dispatch('addNewStatuses', { statuses: ancestors })
this.$store.dispatch('addNewStatuses', { statuses: descendants }) this.$store.dispatch('addNewStatuses', { statuses: descendants })
@ -391,7 +445,8 @@ const conversation = {
}) })
} else { } else {
this.loadStatusError = null this.loadStatusError = null
this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId }) this.$store.state.api.backendInteractor
.fetchStatus({ id: this.statusId })
.then((status) => { .then((status) => {
this.$store.dispatch('addNewStatuses', { statuses: [status] }) this.$store.dispatch('addNewStatuses', { statuses: [status] })
this.fetchConversation() this.fetchConversation()
@ -402,7 +457,7 @@ const conversation = {
} }
}, },
isFocused(id) { isFocused(id) {
return (this.isExpanded) && id === this.highlight return this.isExpanded && id === this.highlight
}, },
getReplies(id) { getReplies(id) {
return this.replies[id] || [] return this.replies[id] || []
@ -426,12 +481,16 @@ const conversation = {
}, },
getConversationId(statusId) { getConversationId(statusId) {
const status = this.$store.state.statuses.allStatusesObject[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 = {
...this.threadDisplayStatusObject, ...this.threadDisplayStatusObject,
[id]: nextStatus [id]: nextStatus,
} }
}, },
toggleThreadDisplay(id) { toggleThreadDisplay(id) {
@ -441,7 +500,9 @@ const conversation = {
}, },
setThreadDisplayRecursively(id, nextStatus) { setThreadDisplayRecursively(id, nextStatus) {
this.setThreadDisplay(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') this.setThreadDisplayRecursively(id, 'showing')
@ -451,12 +512,16 @@ const conversation = {
...this.statusContentPropertiesObject, ...this.statusContentPropertiesObject,
[id]: { [id]: {
...this.statusContentPropertiesObject[id], ...this.statusContentPropertiesObject[id],
[name]: value [name]: value,
} },
} }
}, },
toggleStatusContentProperty(id, name) { toggleStatusContentProperty(id, name) {
this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name]) this.setStatusContentProperty(
id,
name,
!this.statusContentProperties[id][name],
)
}, },
leastVisibleAncestor(id) { leastVisibleAncestor(id) {
let cur = id let cur = id
@ -476,7 +541,9 @@ const conversation = {
this.tryScrollTo(id) this.tryScrollTo(id)
}, },
diveToTopLevel() { diveToTopLevel() {
this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id) this.tryScrollTo(
this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id,
)
}, },
// only used when we are not on a page // only used when we are not on a page
undive() { undive() {
@ -554,8 +621,8 @@ const conversation = {
resetDisplayState() { resetDisplayState() {
this.undive() this.undive()
this.threadDisplayStatusObject = {} this.threadDisplayStatusObject = {}
} },
} },
} }
export default conversation export default conversation

View file

@ -1,20 +1,22 @@
import SearchBar from 'components/search_bar/search_bar.vue' import SearchBar from 'components/search_bar/search_bar.vue'
import { useInterfaceStore } from 'src/stores/interface'
import ConfirmModal from '../confirm_modal/confirm_modal.vue' import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faBell,
faBullhorn,
faCog,
faComments,
faHome,
faInfoCircle,
faSearch,
faSignInAlt, faSignInAlt,
faSignOutAlt, faSignOutAlt,
faHome,
faComments,
faBell,
faUserPlus,
faBullhorn,
faSearch,
faTachometerAlt, faTachometerAlt,
faCog, faUserPlus,
faInfoCircle
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { useInterfaceStore } from 'src/stores/interface'
library.add( library.add(
faSignInAlt, faSignInAlt,
@ -27,60 +29,78 @@ library.add(
faSearch, faSearch,
faTachometerAlt, faTachometerAlt,
faCog, faCog,
faInfoCircle faInfoCircle,
) )
export default { export default {
components: { components: {
SearchBar, SearchBar,
ConfirmModal ConfirmModal,
}, },
data: () => ({ data: () => ({
searchBarHidden: true, searchBarHidden: true,
supportsMask: window.CSS && window.CSS.supports && ( supportsMask:
window.CSS.supports('mask-size', 'contain') || window.CSS &&
window.CSS.supports &&
(window.CSS.supports('mask-size', 'contain') ||
window.CSS.supports('-webkit-mask-size', 'contain') || window.CSS.supports('-webkit-mask-size', 'contain') ||
window.CSS.supports('-moz-mask-size', 'contain') || window.CSS.supports('-moz-mask-size', 'contain') ||
window.CSS.supports('-ms-mask-size', 'contain') || window.CSS.supports('-ms-mask-size', 'contain') ||
window.CSS.supports('-o-mask-size', 'contain') window.CSS.supports('-o-mask-size', 'contain')),
), showingConfirmLogout: false,
showingConfirmLogout: false
}), }),
computed: { computed: {
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask }, enableMask() {
return this.supportsMask && this.$store.state.instance.logoMask
},
logoStyle() { logoStyle() {
return { return {
visibility: this.enableMask ? 'hidden' : 'visible' visibility: this.enableMask ? 'hidden' : 'visible',
} }
}, },
logoMaskStyle() { logoMaskStyle() {
return this.enableMask return this.enableMask
? { ? {
'mask-image': `url(${this.$store.state.instance.logo})` 'mask-image': `url(${this.$store.state.instance.logo})`,
} }
: { : {
'background-color': this.enableMask ? '' : 'transparent' 'background-color': this.enableMask ? '' : 'transparent',
} }
}, },
logoBgStyle() { logoBgStyle() {
return Object.assign({ return Object.assign(
{
margin: `${this.$store.state.instance.logoMargin} 0`, margin: `${this.$store.state.instance.logoMargin} 0`,
opacity: this.searchBarHidden ? 1 : 0 opacity: this.searchBarHidden ? 1 : 0,
}, this.enableMask },
this.enableMask
? {} ? {}
: { : {
'background-color': this.enableMask ? '' : 'transparent' 'background-color': this.enableMask ? '' : 'transparent',
}) },
)
},
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
}, },
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() { shouldConfirmLogout() {
return this.$store.getters.mergedConfig.modalOnLogout return this.$store.getters.mergedConfig.modalOnLogout
} },
}, },
methods: { methods: {
scrollToTop() { scrollToTop() {
@ -112,6 +132,6 @@ export default {
}, },
openAdminModal() { openAdminModal() {
useInterfaceStore().openSettingsModal('admin') useInterfaceStore().openSettingsModal('admin')
} },
} },
} }

View file

@ -2,18 +2,20 @@ const DialogModal = {
props: { props: {
darkOverlay: { darkOverlay: {
default: true, default: true,
type: Boolean type: Boolean,
}, },
onCancel: { onCancel: {
default: () => {}, default: () => {
type: Function /* no-op */
} },
type: Function,
},
}, },
computed: { computed: {
mobileCenter() { mobileCenter() {
return this.$store.getters.mergedConfig.modalMobileCenter return this.$store.getters.mergedConfig.modalMobileCenter
} },
} },
} }
export default DialogModal export default DialogModal

View file

@ -4,11 +4,11 @@ const DMs = {
computed: { computed: {
timeline() { timeline() {
return this.$store.state.statuses.timelines.dms return this.$store.state.statuses.timelines.dms
} },
}, },
components: { components: {
Timeline Timeline,
} },
} }
export default DMs export default DMs

View file

@ -3,7 +3,7 @@ import ProgressButton from '../progress_button/progress_button.vue'
const DomainMuteCard = { const DomainMuteCard = {
props: ['domain'], props: ['domain'],
components: { components: {
ProgressButton ProgressButton,
}, },
computed: { computed: {
user() { user() {
@ -11,7 +11,7 @@ const DomainMuteCard = {
}, },
muted() { muted() {
return this.user.domainMutes.includes(this.domain) return this.user.domainMutes.includes(this.domain)
} },
}, },
methods: { methods: {
unmuteDomain() { unmuteDomain() {
@ -19,8 +19,8 @@ const DomainMuteCard = {
}, },
muteDomain() { muteDomain() {
return this.$store.dispatch('muteDomain', this.domain) return this.$store.dispatch('muteDomain', this.domain)
} },
} },
} }
export default DomainMuteCard export default DomainMuteCard

View file

@ -1,18 +1,15 @@
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
import EditStatusForm from 'src/components/edit_status_form/edit_status_form.vue'
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
import StatusContent from 'src/components/status_content/status_content.vue'
import Gallery from 'src/components/gallery/gallery.vue'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
import { library } from '@fortawesome/fontawesome-svg-core' import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
import { import EditStatusForm from 'src/components/edit_status_form/edit_status_form.vue'
faPollH import Gallery from 'src/components/gallery/gallery.vue'
} from '@fortawesome/free-solid-svg-icons' import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
import StatusContent from 'src/components/status_content/status_content.vue'
library.add( import { library } from '@fortawesome/fontawesome-svg-core'
faPollH import { faPollH } from '@fortawesome/free-solid-svg-icons'
)
library.add(faPollH)
const Draft = { const Draft = {
components: { components: {
@ -20,19 +17,19 @@ const Draft = {
EditStatusForm, EditStatusForm,
ConfirmModal, ConfirmModal,
StatusContent, StatusContent,
Gallery Gallery,
}, },
props: { props: {
draft: { draft: {
type: Object, type: Object,
required: true required: true,
} },
}, },
data() { data() {
return { return {
referenceDraft: cloneDeep(this.draft), referenceDraft: cloneDeep(this.draft),
editing: false, editing: false,
showingConfirmDialog: false showingConfirmDialog: false,
} }
}, },
computed: { computed: {
@ -46,18 +43,18 @@ const Draft = {
} }
}, },
safeToSave() { safeToSave() {
return this.draft.status || return this.draft.status || this.draft.files?.length || this.draft.hasPoll
this.draft.files?.length ||
this.draft.hasPoll
}, },
postStatusFormProps() { postStatusFormProps() {
return { return {
draftId: this.draft.id, draftId: this.draft.id,
...this.relAttrs ...this.relAttrs,
} }
}, },
refStatus() { refStatus() {
return this.draft.refId ? this.$store.state.statuses.allStatusesObject[this.draft.refId] : undefined return this.draft.refId
? this.$store.state.statuses.allStatusesObject[this.draft.refId]
: undefined
}, },
localCollapseSubjectDefault() { localCollapseSubjectDefault() {
return this.$store.getters.mergedConfig.collapseMessageWithSubject return this.$store.getters.mergedConfig.collapseMessageWithSubject
@ -70,7 +67,7 @@ const Draft = {
return false return false
} }
return true return true
} },
}, },
watch: { watch: {
editing(newVal) { editing(newVal) {
@ -80,7 +77,7 @@ const Draft = {
} else { } else {
this.$store.dispatch('addOrSaveDraft', { draft: this.referenceDraft }) this.$store.dispatch('addOrSaveDraft', { draft: this.referenceDraft })
} }
} },
}, },
methods: { methods: {
toggleEditing() { toggleEditing() {
@ -90,15 +87,14 @@ const Draft = {
this.showingConfirmDialog = true this.showingConfirmDialog = true
}, },
doAbandon() { doAbandon() {
this.$store.dispatch('abandonDraft', { id: this.draft.id }) this.$store.dispatch('abandonDraft', { id: this.draft.id }).then(() => {
.then(() => {
this.hideConfirmDialog() this.hideConfirmDialog()
}) })
}, },
hideConfirmDialog() { hideConfirmDialog() {
this.showingConfirmDialog = false this.showingConfirmDialog = false
} },
} },
} }
export default Draft export default Draft

View file

@ -3,16 +3,13 @@ import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
const DraftCloser = { const DraftCloser = {
data() { data() {
return { return {
showing: false showing: false,
} }
}, },
components: { components: {
DialogModal DialogModal,
}, },
emits: [ emits: ['save', 'discard'],
'save',
'discard'
],
computed: { computed: {
action() { action() {
if (this.$store.getters.mergedConfig.autoSaveDraft) { if (this.$store.getters.mergedConfig.autoSaveDraft) {
@ -23,7 +20,7 @@ const DraftCloser = {
}, },
shouldConfirm() { shouldConfirm() {
return this.action === 'confirm' return this.action === 'confirm'
} },
}, },
methods: { methods: {
requestClose() { requestClose() {
@ -45,8 +42,8 @@ const DraftCloser = {
}, },
cancel() { cancel() {
this.showing = false this.showing = false
} },
} },
} }
export default DraftCloser export default DraftCloser

View file

@ -4,13 +4,13 @@ import List from 'src/components/list/list.vue'
const Drafts = { const Drafts = {
components: { components: {
Draft, Draft,
List List,
}, },
computed: { computed: {
drafts() { drafts() {
return this.$store.getters.draftsArray return this.$store.getters.draftsArray
} },
} },
} }
export default Drafts export default Drafts

View file

@ -1,15 +1,15 @@
import PostStatusForm from '../post_status_form/post_status_form.vue'
import statusPosterService from '../../services/status_poster/status_poster.service.js' import statusPosterService from '../../services/status_poster/status_poster.service.js'
import PostStatusForm from '../post_status_form/post_status_form.vue'
const EditStatusForm = { const EditStatusForm = {
components: { components: {
PostStatusForm PostStatusForm,
}, },
props: { props: {
params: { params: {
type: Object, type: Object,
required: true required: true,
} },
}, },
methods: { methods: {
requestClose() { requestClose() {
@ -24,21 +24,22 @@ const EditStatusForm = {
sensitive, sensitive,
poll, poll,
media, media,
contentType contentType,
} }
return statusPosterService.editStatus(params) return statusPosterService
.editStatus(params)
.then((data) => { .then((data) => {
return data return data
}) })
.catch((err) => { .catch((err) => {
console.error('Error editing status', err) console.error('Error editing status', err)
return { return {
error: err.message error: err.message,
} }
}) })
} },
} },
} }
export default EditStatusForm export default EditStatusForm

View file

@ -1,16 +1,17 @@
import get from 'lodash/get'
import { useEditStatusStore } from 'src/stores/editStatus'
import EditStatusForm from '../edit_status_form/edit_status_form.vue' import EditStatusForm from '../edit_status_form/edit_status_form.vue'
import Modal from '../modal/modal.vue' import Modal from '../modal/modal.vue'
import get from 'lodash/get'
import { useEditStatusStore } from 'src/stores/editStatus'
const EditStatusModal = { const EditStatusModal = {
components: { components: {
EditStatusForm, EditStatusForm,
Modal Modal,
}, },
data() { data() {
return { return {
resettingForm: false resettingForm: false,
} }
}, },
computed: { computed: {
@ -25,7 +26,7 @@ const EditStatusModal = {
}, },
params() { params() {
return useEditStatusStore().params || {} return useEditStatusStore().params || {}
} },
}, },
watch: { watch: {
params(newVal, oldVal) { params(newVal, oldVal) {
@ -38,18 +39,20 @@ const EditStatusModal = {
}, },
isFormVisible(val) { isFormVisible(val) {
if (val) { if (val) {
this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus()) this.$nextTick(
} () => this.$el && this.$el.querySelector('textarea').focus(),
)
} }
}, },
},
methods: { methods: {
closeModal() { closeModal() {
this.$refs.editStatusForm.requestClose() this.$refs.editStatusForm.requestClose()
}, },
doCloseModal() { doCloseModal() {
useEditStatusStore().closeEditStatusModal() useEditStatusStore().closeEditStatusModal()
} },
} },
} }
export default EditStatusModal export default EditStatusModal

View file

@ -1,20 +1,18 @@
import Completion from '../../services/completion/completion.js' import { take } from 'lodash'
import genRandomSeed from '../../services/random_seed/random_seed.service.js'
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import Popover from 'src/components/popover/popover.vue' import Popover from 'src/components/popover/popover.vue'
import ScreenReaderNotice from 'src/components/screen_reader_notice/screen_reader_notice.vue' import ScreenReaderNotice from 'src/components/screen_reader_notice/screen_reader_notice.vue'
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
import { take } from 'lodash'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
import { ensureFinalFallback } from '../../i18n/languages.js' import { ensureFinalFallback } from '../../i18n/languages.js'
import { library } from '@fortawesome/fontawesome-svg-core' import Completion from '../../services/completion/completion.js'
import { import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
faSmileBeam import genRandomSeed from '../../services/random_seed/random_seed.service.js'
} from '@fortawesome/free-regular-svg-icons' import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
library.add( import { library } from '@fortawesome/fontawesome-svg-core'
faSmileBeam import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
)
library.add(faSmileBeam)
/** /**
* EmojiInput - augmented inputs for emoji and autocomplete support in inputs * EmojiInput - augmented inputs for emoji and autocomplete support in inputs
@ -60,14 +58,14 @@ const EmojiInput = {
* For commonly used suggestors (emoji, users, both) use suggestor.js * For commonly used suggestors (emoji, users, both) use suggestor.js
*/ */
required: true, required: true,
type: Function type: Function,
}, },
modelValue: { modelValue: {
/** /**
* Used for v-model * Used for v-model
*/ */
required: true, required: true,
type: String type: String,
}, },
enableEmojiPicker: { enableEmojiPicker: {
/** /**
@ -75,7 +73,7 @@ const EmojiInput = {
*/ */
required: false, required: false,
type: Boolean, type: Boolean,
default: false default: false,
}, },
hideEmojiButton: { hideEmojiButton: {
/** /**
@ -84,7 +82,7 @@ const EmojiInput = {
*/ */
required: false, required: false,
type: Boolean, type: Boolean,
default: false default: false,
}, },
enableStickerPicker: { enableStickerPicker: {
/** /**
@ -92,7 +90,7 @@ const EmojiInput = {
*/ */
required: false, required: false,
type: Boolean, type: Boolean,
default: false default: false,
}, },
placement: { placement: {
/** /**
@ -101,13 +99,13 @@ const EmojiInput = {
*/ */
required: false, required: false,
type: String, // 'auto', 'top', 'bottom' type: String, // 'auto', 'top', 'bottom'
default: 'auto' default: 'auto',
}, },
newlineOnCtrlEnter: { newlineOnCtrlEnter: {
required: false, required: false,
type: Boolean, type: Boolean,
default: false default: false,
} },
}, },
data() { data() {
return { return {
@ -122,14 +120,14 @@ const EmojiInput = {
disableClickOutside: false, disableClickOutside: false,
suggestions: [], suggestions: [],
overlayStyle: {}, overlayStyle: {},
pickerShown: false pickerShown: false,
} }
}, },
components: { components: {
Popover, Popover,
EmojiPicker, EmojiPicker,
UnicodeDomainIndicator, UnicodeDomainIndicator,
ScreenReaderNotice ScreenReaderNotice,
}, },
computed: { computed: {
padEmoji() { padEmoji() {
@ -145,35 +143,42 @@ const EmojiInput = {
return this.modelValue.slice(this.caret) return this.modelValue.slice(this.caret)
}, },
showSuggestions() { showSuggestions() {
return this.focused && return (
this.focused &&
this.suggestions && this.suggestions &&
this.suggestions.length > 0 && this.suggestions.length > 0 &&
!this.pickerShown && !this.pickerShown &&
!this.temporarilyHideSuggestions !this.temporarilyHideSuggestions
)
}, },
textAtCaret() { textAtCaret() {
return this.wordAtCaret?.word return this.wordAtCaret?.word
}, },
wordAtCaret() { wordAtCaret() {
if (this.modelValue && this.caret) { if (this.modelValue && this.caret) {
const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {} const word =
Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
return word return word
} }
}, },
languages() { languages() {
return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage) return ensureFinalFallback(
this.$store.getters.mergedConfig.interfaceLanguage,
)
}, },
maybeLocalizedEmojiNamesAndKeywords() { maybeLocalizedEmojiNamesAndKeywords() {
return emoji => { return (emoji) => {
const names = [emoji.displayText] const names = [emoji.displayText]
const keywords = [] const keywords = []
if (emoji.displayTextI18n) { if (emoji.displayTextI18n) {
names.push(this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)) names.push(
this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args),
)
} }
if (emoji.annotations) { if (emoji.annotations) {
this.languages.forEach(lang => { this.languages.forEach((lang) => {
names.push(emoji.annotations[lang]?.name) names.push(emoji.annotations[lang]?.name)
keywords.push(...(emoji.annotations[lang]?.keywords || [])) keywords.push(...(emoji.annotations[lang]?.keywords || []))
@ -181,13 +186,13 @@ const EmojiInput = {
} }
return { return {
names: names.filter(k => k), names: names.filter((k) => k),
keywords: keywords.filter(k => k) keywords: keywords.filter((k) => k),
} }
} }
}, },
maybeLocalizedEmojiName() { maybeLocalizedEmojiName() {
return emoji => { return (emoji) => {
if (!emoji.annotations) { if (!emoji.annotations) {
return emoji.displayText return emoji.displayText
} }
@ -210,11 +215,13 @@ const EmojiInput = {
}, },
suggestionItemId() { suggestionItemId() {
return (index) => `suggestion-item-${index}-${this.randomSeed}` return (index) => `suggestion-item-${index}-${this.randomSeed}`
} },
}, },
mounted() { mounted() {
const { root, hiddenOverlayCaret, suggestorPopover } = this.$refs const { root, hiddenOverlayCaret, suggestorPopover } = this.$refs
const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea') const input =
root.querySelector('.emoji-input > input') ||
root.querySelector('.emoji-input > textarea')
if (!input) return if (!input) return
this.input = input this.input = input
this.caretEl = hiddenOverlayCaret this.caretEl = hiddenOverlayCaret
@ -273,32 +280,36 @@ const EmojiInput = {
this.suggestions = [] this.suggestions = []
return return
} }
const matchedSuggestions = await this.suggest(newWord, this.maybeLocalizedEmojiNamesAndKeywords) const matchedSuggestions = await this.suggest(
newWord,
this.maybeLocalizedEmojiNamesAndKeywords,
)
// Async: cancel if textAtCaret has changed during wait // Async: cancel if textAtCaret has changed during wait
if (this.textAtCaret !== newWord || matchedSuggestions.length <= 0) { if (this.textAtCaret !== newWord || matchedSuggestions.length <= 0) {
this.suggestions = [] this.suggestions = []
return return
} }
this.suggestions = take(matchedSuggestions, 5) this.suggestions = take(matchedSuggestions, 5).map(
.map(({ imageUrl, ...rest }) => ({ ({ imageUrl, ...rest }) => ({
...rest, ...rest,
img: imageUrl || '' img: imageUrl || '',
})) }),
)
this.highlighted = this.defaultCandidateIndex this.highlighted = this.defaultCandidateIndex
this.$refs.screenReaderNotice.announce( this.$refs.screenReaderNotice.announce(
this.$t( this.$t(
'tool_tip.autocomplete_available', 'tool_tip.autocomplete_available',
{ number: this.suggestions.length }, { number: this.suggestions.length },
this.suggestions.length this.suggestions.length,
),
) )
) },
}
}, },
methods: { methods: {
onInputScroll(e) { onInputScroll(e) {
this.$refs.hiddenOverlay.scrollTo({ this.$refs.hiddenOverlay.scrollTo({
top: this.input.scrollTop, top: this.input.scrollTop,
left: this.input.scrollLeft left: this.input.scrollLeft,
}) })
this.setCaret(e) this.setCaret(e)
}, },
@ -326,7 +337,11 @@ const EmojiInput = {
} }
}, },
replace(replacement) { replace(replacement) {
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement) const newValue = Completion.replaceWord(
this.modelValue,
this.wordAtCaret,
replacement,
)
this.$emit('update:modelValue', newValue) this.$emit('update:modelValue', newValue)
this.caret = 0 this.caret = 0
}, },
@ -349,18 +364,24 @@ const EmojiInput = {
* them, masto seem to be rendering :emoji::emoji: correctly now so why not * them, masto seem to be rendering :emoji::emoji: correctly now so why not
*/ */
const isSpaceRegex = /\s/ const isSpaceRegex = /\s/
const spaceBefore = (surroundingSpace && !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0) ? ' ' : '' const spaceBefore =
const spaceAfter = (surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji) ? ' ' : '' surroundingSpace &&
!isSpaceRegex.exec(before.slice(-1)) &&
before.length &&
this.padEmoji > 0
? ' '
: ''
const spaceAfter =
surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji
? ' '
: ''
const newValue = [ const newValue = [before, spaceBefore, insertion, spaceAfter, after].join(
before, '',
spaceBefore, )
insertion,
spaceAfter,
after
].join('')
this.$emit('update:modelValue', newValue) this.$emit('update:modelValue', newValue)
const position = this.caret + (insertion + spaceAfter + spaceBefore).length const position =
this.caret + (insertion + spaceAfter + spaceBefore).length
if (!keepOpen) { if (!keepOpen) {
this.input.focus() this.input.focus()
} }
@ -374,11 +395,18 @@ const EmojiInput = {
}, },
replaceText(e, suggestion) { replaceText(e, suggestion) {
const len = this.suggestions.length || 0 const len = this.suggestions.length || 0
if (this.textAtCaret.length === 1) { return } if (this.textAtCaret.length === 1) {
return
}
if (len > 0 || suggestion) { if (len > 0 || suggestion) {
const chosenSuggestion = suggestion || this.suggestions[this.highlighted] const chosenSuggestion =
suggestion || this.suggestions[this.highlighted]
const replacement = chosenSuggestion.replacement const replacement = chosenSuggestion.replacement
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement) const newValue = Completion.replaceWord(
this.modelValue,
this.wordAtCaret,
replacement,
)
this.$emit('update:modelValue', newValue) this.$emit('update:modelValue', newValue)
this.highlighted = 0 this.highlighted = 0
const position = this.wordAtCaret.start + replacement.length const position = this.wordAtCaret.start + replacement.length
@ -424,20 +452,22 @@ const EmojiInput = {
* replies in notifs) or mobile post form. Note that getting and setting * replies in notifs) or mobile post form. Note that getting and setting
* scroll is different for `Window` and `Element`s * scroll is different for `Window` and `Element`s
*/ */
const scrollerRef = this.$el.closest('.sidebar-scroller') || const scrollerRef =
this.$el.closest('.sidebar-scroller') ||
this.$el.closest('.post-form-modal-view') || this.$el.closest('.post-form-modal-view') ||
window window
const currentScroll = scrollerRef === window const currentScroll =
? scrollerRef.scrollY scrollerRef === window ? scrollerRef.scrollY : scrollerRef.scrollTop
: scrollerRef.scrollTop const scrollerHeight =
const scrollerHeight = scrollerRef === window scrollerRef === window
? scrollerRef.innerHeight ? scrollerRef.innerHeight
: scrollerRef.offsetHeight : scrollerRef.offsetHeight
const scrollerBottomBorder = currentScroll + scrollerHeight const scrollerBottomBorder = currentScroll + scrollerHeight
// We check where the bottom border of root element is, this uses findOffset // We check where the bottom border of root element is, this uses findOffset
// to find offset relative to scrollable container (scroller) // to find offset relative to scrollable container (scroller)
const rootBottomBorder = rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top const rootBottomBorder =
rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
const bottomDelta = Math.max(0, rootBottomBorder - scrollerBottomBorder) const bottomDelta = Math.max(0, rootBottomBorder - scrollerBottomBorder)
// could also check top delta but there's no case for it // could also check top delta but there's no case for it
@ -567,8 +597,8 @@ const EmojiInput = {
} else { } else {
return this.maybeLocalizedEmojiName(suggestion) return this.maybeLocalizedEmojiName(suggestion)
} }
} },
} },
} }
export default EmojiInput export default EmojiInput

View file

@ -10,7 +10,7 @@
* doesn't support user linking you can just provide only emoji. * doesn't support user linking you can just provide only emoji.
*/ */
export default data => { export default (data) => {
const emojiCurry = suggestEmoji(data.emoji) const emojiCurry = suggestEmoji(data.emoji)
const usersCurry = data.store && suggestUsers(data.store) const usersCurry = data.store && suggestUsers(data.store)
return (input, nameKeywordLocalizer) => { return (input, nameKeywordLocalizer) => {
@ -25,22 +25,35 @@ export default data => {
} }
} }
export const suggestEmoji = emojis => (input, nameKeywordLocalizer) => { export const suggestEmoji = (emojis) => (input, nameKeywordLocalizer) => {
const noPrefix = input.toLowerCase().substr(1) const noPrefix = input.toLowerCase().substr(1)
return emojis return emojis
.map(emoji => ({ ...emoji, ...nameKeywordLocalizer(emoji) })) .map((emoji) => ({ ...emoji, ...nameKeywordLocalizer(emoji) }))
.filter((emoji) => (emoji.names.concat(emoji.keywords)).filter(kw => kw.toLowerCase().match(noPrefix)).length) .filter(
.map(k => { (emoji) =>
emoji.names
.concat(emoji.keywords)
.filter((kw) => kw.toLowerCase().match(noPrefix)).length,
)
.map((k) => {
let score = 0 let score = 0
// An exact match always wins // An exact match always wins
score += Math.max(...k.names.map(name => name.toLowerCase() === noPrefix ? 200 : 0), 0) score += Math.max(
...k.names.map((name) => (name.toLowerCase() === noPrefix ? 200 : 0)),
0,
)
// Prioritize custom emoji a lot // Prioritize custom emoji a lot
score += k.imageUrl ? 100 : 0 score += k.imageUrl ? 100 : 0
// Prioritize prefix matches somewhat // Prioritize prefix matches somewhat
score += Math.max(...k.names.map(kw => kw.toLowerCase().startsWith(noPrefix) ? 10 : 0), 0) score += Math.max(
...k.names.map((kw) =>
kw.toLowerCase().startsWith(noPrefix) ? 10 : 0,
),
0,
)
// Sort by length // Sort by length
score -= k.displayText.length score -= k.displayText.length
@ -78,7 +91,7 @@ export const suggestUsers = ({ dispatch, state }) => {
}) })
} }
return async input => { return async (input) => {
const noPrefix = input.toLowerCase().substr(1) const noPrefix = input.toLowerCase().substr(1)
if (previousQuery === noPrefix) return suggestions if (previousQuery === noPrefix) return suggestions
@ -92,12 +105,16 @@ export const suggestUsers = ({ dispatch, state }) => {
await debounceUserSearch(noPrefix) await debounceUserSearch(noPrefix)
} }
const newSuggestions = state.users.users.filter( const newSuggestions = state.users.users
user => .filter(
user.screen_name && user.name && ( (user) =>
user.screen_name.toLowerCase().startsWith(noPrefix) || user.screen_name &&
user.name.toLowerCase().startsWith(noPrefix)) user.name &&
).slice(0, 20).sort((a, b) => { (user.screen_name.toLowerCase().startsWith(noPrefix) ||
user.name.toLowerCase().startsWith(noPrefix)),
)
.slice(0, 20)
.sort((a, b) => {
let aScore = 0 let aScore = 0
let bScore = 0 let bScore = 0
@ -116,12 +133,13 @@ export const suggestUsers = ({ dispatch, state }) => {
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1 const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
return diff + nameAlphabetically + screenNameAlphabetically return diff + nameAlphabetically + screenNameAlphabetically
}).map((user) => ({ })
.map((user) => ({
user, user,
displayText: user.screen_name_ui, displayText: user.screen_name_ui,
detailText: user.name, detailText: user.name,
imageUrl: user.profile_image_url_original, imageUrl: user.profile_image_url_original,
replacement: '@' + user.screen_name + ' ' replacement: '@' + user.screen_name + ' ',
})) }))
suggestions = newSuggestions || [] suggestions = newSuggestions || []

View file

@ -1,24 +1,26 @@
import { chunk, debounce, trim } from 'lodash'
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import Checkbox from '../checkbox/checkbox.vue'
import Popover from 'src/components/popover/popover.vue' import Popover from 'src/components/popover/popover.vue'
import StillImage from '../still-image/still-image.vue'
import { ensureFinalFallback } from '../../i18n/languages.js' import { ensureFinalFallback } from '../../i18n/languages.js'
import Checkbox from '../checkbox/checkbox.vue'
import StillImage from '../still-image/still-image.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faBoxOpen,
faStickyNote,
faSmileBeam,
faSmile,
faUser,
faPaw,
faIceCream,
faBus,
faBasketballBall, faBasketballBall,
faLightbulb, faBoxOpen,
faBus,
faCode, faCode,
faFlag faFlag,
faIceCream,
faLightbulb,
faPaw,
faSmile,
faSmileBeam,
faStickyNote,
faUser,
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { debounce, trim, chunk } from 'lodash'
library.add( library.add(
faBoxOpen, faBoxOpen,
@ -32,7 +34,7 @@ library.add(
faBasketballBall, faBasketballBall,
faLightbulb, faLightbulb,
faCode, faCode,
faFlag faFlag,
) )
const UNICODE_EMOJI_GROUP_ICON = { const UNICODE_EMOJI_GROUP_ICON = {
@ -44,16 +46,16 @@ const UNICODE_EMOJI_GROUP_ICON = {
activities: 'basketball-ball', activities: 'basketball-ball',
objects: 'lightbulb', objects: 'lightbulb',
symbols: 'code', symbols: 'code',
flags: 'flag' flags: 'flag',
} }
const maybeLocalizedKeywords = (emoji, languages, nameLocalizer) => { const maybeLocalizedKeywords = (emoji, languages, nameLocalizer) => {
const res = [emoji.displayText, nameLocalizer(emoji)] const res = [emoji.displayText, nameLocalizer(emoji)]
if (emoji.annotations) { if (emoji.annotations) {
languages.forEach(lang => { languages.forEach((lang) => {
const keywords = emoji.annotations[lang]?.keywords || [] const keywords = emoji.annotations[lang]?.keywords || []
const name = emoji.annotations[lang]?.name const name = emoji.annotations[lang]?.name
res.push(...(keywords.concat([name]).filter(k => k))) res.push(...keywords.concat([name]).filter((k) => k))
}) })
} }
return res return res
@ -66,8 +68,8 @@ const filterByKeyword = (list, keyword = '', languages, nameLocalizer) => {
const orderedEmojiList = [] const orderedEmojiList = []
for (const emoji of list) { for (const emoji of list) {
const indices = maybeLocalizedKeywords(emoji, languages, nameLocalizer) const indices = maybeLocalizedKeywords(emoji, languages, nameLocalizer)
.map(k => k.toLowerCase().indexOf(keywordLowercase)) .map((k) => k.toLowerCase().indexOf(keywordLowercase))
.filter(k => k > -1) .filter((k) => k > -1)
const indexOfKeyword = indices.length ? Math.min(...indices) : -1 const indexOfKeyword = indices.length ? Math.min(...indices) : -1
@ -84,11 +86,13 @@ const filterByKeyword = (list, keyword = '', languages, nameLocalizer) => {
const getOffset = (elem) => { const getOffset = (elem) => {
const style = elem.style.transform const style = elem.style.transform
const res = /translateY\((\d+)px\)/.exec(style) const res = /translateY\((\d+)px\)/.exec(style)
if (!res) { return 0 } if (!res) {
return 0
}
return res[1] return res[1]
} }
const toHeaderId = id => { const toHeaderId = (id) => {
return id.replace(/^row-\d+-/, '') return id.replace(/^row-\d+-/, '')
} }
@ -97,18 +101,18 @@ const EmojiPicker = {
enableStickerPicker: { enableStickerPicker: {
required: false, required: false,
type: Boolean, type: Boolean,
default: true default: true,
}, },
hideCustomEmoji: { hideCustomEmoji: {
required: false, required: false,
type: Boolean, type: Boolean,
default: false default: false,
} },
}, },
inject: { inject: {
popoversZLayer: { popoversZLayer: {
default: '' default: '',
} },
}, },
data() { data() {
return { return {
@ -125,14 +129,16 @@ const EmojiPicker = {
emojiRefs: {}, emojiRefs: {},
filteredEmojiGroups: [], filteredEmojiGroups: [],
emojiSize: 0, emojiSize: 0,
width: 0 width: 0,
} }
}, },
components: { components: {
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')), StickerPicker: defineAsyncComponent(
() => import('../sticker_picker/sticker_picker.vue'),
),
Checkbox, Checkbox,
StillImage, StillImage,
Popover Popover,
}, },
methods: { methods: {
groupScroll(e) { groupScroll(e) {
@ -163,7 +169,7 @@ const EmojiPicker = {
emojiSizeReal = emojiSizeValue emojiSizeReal = emojiSizeValue
} }
const fullEmojiSize = emojiSizeReal + (2 * 0.2 * fontSizeMultiplier * 14) const fullEmojiSize = emojiSizeReal + 2 * 0.2 * fontSizeMultiplier * 14
this.emojiSize = fullEmojiSize this.emojiSize = fullEmojiSize
}, },
showPicker() { showPicker() {
@ -179,7 +185,9 @@ const EmojiPicker = {
this.$refs.popover.setAnchorEl(el) this.$refs.popover.setAnchorEl(el)
}, },
setGroupRef(name) { setGroupRef(name) {
return el => { this.groupRefs[name] = el } return (el) => {
this.groupRefs[name] = el
}
}, },
onPopoverShown() { onPopoverShown() {
this.$emit('show') this.$emit('show')
@ -194,11 +202,17 @@ const EmojiPicker = {
this.$emit('sticker-upload-failed', e) this.$emit('sticker-upload-failed', e)
}, },
onEmoji(emoji) { onEmoji(emoji) {
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement const value = emoji.imageUrl
? `:${emoji.displayText}:`
: emoji.replacement
if (!this.keepOpen) { if (!this.keepOpen) {
this.$refs.popover.hidePopover() this.$refs.popover.hidePopover()
} }
this.$emit('emoji', { insertion: value, insertionUrl: emoji.imageUrl, keepOpen: this.keepOpen }) this.$emit('emoji', {
insertion: value,
insertionUrl: emoji.imageUrl,
keepOpen: this.keepOpen,
})
}, },
onScroll(startIndex, endIndex, visibleStartIndex, visibleEndIndex) { onScroll(startIndex, endIndex, visibleStartIndex, visibleEndIndex) {
const target = this.$refs['emoji-groups'].$el const target = this.$refs['emoji-groups'].$el
@ -207,12 +221,16 @@ const EmojiPicker = {
scrolledGroup(target, start, end) { scrolledGroup(target, start, end) {
const top = target.scrollTop + 5 const top = target.scrollTop + 5
this.$nextTick(() => { this.$nextTick(() => {
this.emojiItems.slice(start, end + 1).forEach(group => { this.emojiItems.slice(start, end + 1).forEach((group) => {
const headerId = toHeaderId(group.id) const headerId = toHeaderId(group.id)
const ref = this.groupRefs['group-' + group.id] const ref = this.groupRefs['group-' + group.id]
if (!ref) { return } if (!ref) {
return
}
const elem = ref.$el.parentElement const elem = ref.$el.parentElement
if (!elem) { return } if (!elem) {
return
}
if (elem && getOffset(elem) <= top) { if (elem && getOffset(elem) <= top) {
this.activeGroup = headerId this.activeGroup = headerId
} }
@ -228,7 +246,9 @@ const EmojiPicker = {
const headerCont = this.$refs.header const headerCont = this.$refs.header
const currentScroll = headerCont.scrollLeft const currentScroll = headerCont.scrollLeft
const currentScrollRight = currentScroll + headerCont.clientWidth const currentScrollRight = currentScroll + headerCont.clientWidth
const setScroll = s => { headerCont.scrollLeft = s } const setScroll = (s) => {
headerCont.scrollLeft = s
}
const margin = 7 // .emoji-tabs-item: padding const margin = 7 // .emoji-tabs-item: padding
if (left - margin < currentScroll) { if (left - margin < currentScroll) {
@ -239,7 +259,7 @@ const EmojiPicker = {
}, },
highlight(groupId) { highlight(groupId) {
this.setShowStickers(false) this.setShowStickers(false)
const indexInList = this.emojiItems.findIndex(k => k.id === groupId) const indexInList = this.emojiItems.findIndex((k) => k.id === groupId)
this.$refs['emoji-groups'].scrollToItem(indexInList) this.$refs['emoji-groups'].scrollToItem(indexInList)
}, },
updateScrolledClass(target) { updateScrolledClass(target) {
@ -258,7 +278,12 @@ const EmojiPicker = {
this.showingStickers = value this.showingStickers = value
}, },
filterByKeyword(list, keyword) { filterByKeyword(list, keyword) {
return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName) return filterByKeyword(
list,
keyword,
this.languages,
this.maybeLocalizedEmojiName,
)
}, },
onShowing() { onShowing() {
const oldContentLoaded = this.contentLoaded const oldContentLoaded = this.contentLoaded
@ -279,11 +304,11 @@ const EmojiPicker = {
}, },
getFilteredEmojiGroups() { getFilteredEmojiGroups() {
return this.allEmojiGroups return this.allEmojiGroups
.map(group => ({ .map((group) => ({
...group, ...group,
emojis: this.filterByKeyword(group.emojis, trim(this.keyword)) emojis: this.filterByKeyword(group.emojis, trim(this.keyword)),
})) }))
.filter(group => group.emojis.length > 0) .filter((group) => group.emojis.length > 0)
}, },
recalculateItemPerRow() { recalculateItemPerRow() {
this.$nextTick(() => { this.$nextTick(() => {
@ -292,7 +317,7 @@ const EmojiPicker = {
} }
this.width = this.$refs['emoji-groups'].$el.clientWidth this.width = this.$refs['emoji-groups'].$el.clientWidth
}) })
} },
}, },
watch: { watch: {
keyword() { keyword() {
@ -301,7 +326,7 @@ const EmojiPicker = {
}, },
allCustomGroups() { allCustomGroups() {
this.filteredEmojiGroups = this.getFilteredEmojiGroups() this.filteredEmojiGroups = this.getFilteredEmojiGroups()
} },
}, },
computed: { computed: {
minItemSize() { minItemSize() {
@ -343,11 +368,11 @@ const EmojiPicker = {
return Object.keys(this.allCustomGroups)[0] return Object.keys(this.allCustomGroups)[0]
}, },
unicodeEmojiGroups() { unicodeEmojiGroups() {
return this.$store.getters.standardEmojiGroupList.map(group => ({ return this.$store.getters.standardEmojiGroupList.map((group) => ({
id: `standard-${group.id}`, id: `standard-${group.id}`,
text: this.$t(`emoji.unicode_groups.${group.id}`), text: this.$t(`emoji.unicode_groups.${group.id}`),
icon: UNICODE_EMOJI_GROUP_ICON[group.id], icon: UNICODE_EMOJI_GROUP_ICON[group.id],
emojis: group.emojis emojis: group.emojis,
})) }))
}, },
allEmojiGroups() { allEmojiGroups() {
@ -364,21 +389,24 @@ const EmojiPicker = {
}, 500) }, 500)
}, },
emojiItems() { emojiItems() {
return this.filteredEmojiGroups.map(group => return this.filteredEmojiGroups
chunk(group.emojis, this.itemPerRow) .map((group) =>
.map((items, index) => ({ chunk(group.emojis, this.itemPerRow).map((items, index) => ({
...group, ...group,
id: index === 0 ? group.id : `row-${index}-${group.id}`, id: index === 0 ? group.id : `row-${index}-${group.id}`,
emojis: items, emojis: items,
isFirstRow: index === 0 isFirstRow: index === 0,
}))) })),
)
.reduce((a, c) => a.concat(c), []) .reduce((a, c) => a.concat(c), [])
}, },
languages() { languages() {
return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage) return ensureFinalFallback(
this.$store.getters.mergedConfig.interfaceLanguage,
)
}, },
maybeLocalizedEmojiName() { maybeLocalizedEmojiName() {
return emoji => { return (emoji) => {
if (!emoji.annotations) { if (!emoji.annotations) {
return emoji.displayText return emoji.displayText
} }
@ -398,8 +426,8 @@ const EmojiPicker = {
}, },
isInModal() { isInModal() {
return this.popoversZLayer === 'modals' return this.popoversZLayer === 'modals'
} },
} },
} }
export default EmojiPicker export default EmojiPicker

View file

@ -1,18 +1,11 @@
import StillImage from 'src/components/still-image/still-image.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import UserListPopover from '../user_list_popover/user_list_popover.vue' import UserListPopover from '../user_list_popover/user_list_popover.vue'
import StillImage from 'src/components/still-image/still-image.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faPlus,
faMinus,
faCheck
} from '@fortawesome/free-solid-svg-icons'
library.add( import { library } from '@fortawesome/fontawesome-svg-core'
faPlus, import { faCheck, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'
faMinus,
faCheck library.add(faPlus, faMinus, faCheck)
)
const EMOJI_REACTION_COUNT_CUTOFF = 12 const EMOJI_REACTION_COUNT_CUTOFF = 12
@ -21,11 +14,11 @@ const EmojiReactions = {
components: { components: {
UserAvatar, UserAvatar,
UserListPopover, UserListPopover,
StillImage StillImage,
}, },
props: ['status'], props: ['status'],
data: () => ({ data: () => ({
showAll: false showAll: false,
}), }),
computed: { computed: {
tooManyReactions() { tooManyReactions() {
@ -49,20 +42,25 @@ const EmojiReactions = {
return !!this.$store.state.users.currentUser return !!this.$store.state.users.currentUser
}, },
remoteInteractionLink() { remoteInteractionLink() {
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id }) return this.$store.getters.remoteInteractionLink({
} statusId: this.status.id,
})
},
}, },
methods: { methods: {
toggleShowAll() { toggleShowAll() {
this.showAll = !this.showAll this.showAll = !this.showAll
}, },
reactedWith(emoji) { reactedWith(emoji) {
return this.status.emoji_reactions.find(r => r.name === emoji).me return this.status.emoji_reactions.find((r) => r.name === emoji).me
}, },
async fetchEmojiReactionsByIfMissing() { async fetchEmojiReactionsByIfMissing() {
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts) const hasNoAccounts = this.status.emoji_reactions.find((r) => !r.accounts)
if (hasNoAccounts) { if (hasNoAccounts) {
return await this.$store.dispatch('fetchEmojiReactionsBy', this.status.id) return await this.$store.dispatch(
'fetchEmojiReactionsBy',
this.status.id,
)
} }
}, },
reactWith(emoji) { reactWith(emoji) {
@ -87,13 +85,17 @@ const EmojiReactions = {
'emoji-reaction-count-button', 'emoji-reaction-count-button',
{ {
'-picked-reaction': this.reactedWith(reaction.name), '-picked-reaction': this.reactedWith(reaction.name),
toggled: this.reactedWith(reaction.name) toggled: this.reactedWith(reaction.name),
} },
], ],
'aria-label': this.$t('status.reaction_count_label', { num: reaction.count }, reaction.count) 'aria-label': this.$t(
} 'status.reaction_count_label',
} { num: reaction.count },
reaction.count,
),
} }
},
},
} }
export default EmojiReactions export default EmojiReactions

View file

@ -1,45 +1,47 @@
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faCircleNotch)
faCircleNotch
)
const Exporter = { const Exporter = {
props: { props: {
getContent: { getContent: {
type: Function, type: Function,
required: true required: true,
}, },
filename: { filename: {
type: String, type: String,
default: 'export.csv' default: 'export.csv',
}, },
exportButtonLabel: { type: String }, exportButtonLabel: { type: String },
processingMessage: { type: String } processingMessage: { type: String },
}, },
data() { data() {
return { return {
processing: false processing: false,
} }
}, },
methods: { methods: {
process() { process() {
this.processing = true this.processing = true
this.getContent() this.getContent().then((content) => {
.then((content) => {
const fileToDownload = document.createElement('a') const fileToDownload = document.createElement('a')
fileToDownload.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content)) fileToDownload.setAttribute(
'href',
'data:text/plain;charset=utf-8,' + encodeURIComponent(content),
)
fileToDownload.setAttribute('download', this.filename) fileToDownload.setAttribute('download', this.filename)
fileToDownload.style.display = 'none' fileToDownload.style.display = 'none'
document.body.appendChild(fileToDownload) document.body.appendChild(fileToDownload)
fileToDownload.click() fileToDownload.click()
document.body.removeChild(fileToDownload) document.body.removeChild(fileToDownload)
// Add delay before hiding processing state since browser takes some time to handle file download // Add delay before hiding processing state since browser takes some time to handle file download
setTimeout(() => { this.processing = false }, 2000) setTimeout(() => {
this.processing = false
}, 2000)
}) })
} },
} },
} }
export default Exporter export default Exporter

View file

@ -1,55 +1,72 @@
import { mapGetters } from 'vuex'
import { mapState as mapPiniaState } from 'pinia' import { mapState as mapPiniaState } from 'pinia'
import { mapGetters } from 'vuex'
import { useAnnouncementsStore } from 'src/stores/announcements' import { useAnnouncementsStore } from 'src/stores/announcements'
import { useInterfaceStore } from 'src/stores/interface'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faUserPlus, faBullhorn,
faComments, faComments,
faBullhorn faUserPlus,
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { useInterfaceStore } from 'src/stores/interface' library.add(faUserPlus, faComments, faBullhorn)
library.add(
faUserPlus,
faComments,
faBullhorn
)
const ExtraNotifications = { const ExtraNotifications = {
computed: { computed: {
shouldShowChats() { shouldShowChats() {
return this.mergedConfig.showExtraNotifications && this.mergedConfig.showChatsInExtraNotifications && this.unreadChatCount return (
this.mergedConfig.showExtraNotifications &&
this.mergedConfig.showChatsInExtraNotifications &&
this.unreadChatCount
)
}, },
shouldShowAnnouncements() { shouldShowAnnouncements() {
return this.mergedConfig.showExtraNotifications && this.mergedConfig.showAnnouncementsInExtraNotifications && this.unreadAnnouncementCount return (
this.mergedConfig.showExtraNotifications &&
this.mergedConfig.showAnnouncementsInExtraNotifications &&
this.unreadAnnouncementCount
)
}, },
shouldShowFollowRequests() { shouldShowFollowRequests() {
return this.mergedConfig.showExtraNotifications && this.mergedConfig.showFollowRequestsInExtraNotifications && this.followRequestCount return (
this.mergedConfig.showExtraNotifications &&
this.mergedConfig.showFollowRequestsInExtraNotifications &&
this.followRequestCount
)
}, },
hasAnythingToShow() { hasAnythingToShow() {
return this.shouldShowChats || this.shouldShowAnnouncements || this.shouldShowFollowRequests return (
this.shouldShowChats ||
this.shouldShowAnnouncements ||
this.shouldShowFollowRequests
)
}, },
shouldShowCustomizationTip() { shouldShowCustomizationTip() {
return this.mergedConfig.showExtraNotificationsTip && this.hasAnythingToShow return (
this.mergedConfig.showExtraNotificationsTip && this.hasAnythingToShow
)
}, },
currentUser() { currentUser() {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
}, },
...mapGetters(['unreadChatCount', 'followRequestCount', 'mergedConfig']), ...mapGetters(['unreadChatCount', 'followRequestCount', 'mergedConfig']),
...mapPiniaState(useAnnouncementsStore, { ...mapPiniaState(useAnnouncementsStore, {
unreadAnnouncementCount: 'unreadAnnouncementCount' unreadAnnouncementCount: 'unreadAnnouncementCount',
}) }),
}, },
methods: { methods: {
openNotificationSettings() { openNotificationSettings() {
return useInterfaceStore().openSettingsModalTab('notifications') return useInterfaceStore().openSettingsModalTab('notifications')
}, },
dismissConfigurationTip() { dismissConfigurationTip() {
return this.$store.dispatch('setOption', { name: 'showExtraNotificationsTip', value: false }) return this.$store.dispatch('setOption', {
} name: 'showExtraNotificationsTip',
} value: false,
})
},
},
} }
export default ExtraNotifications export default ExtraNotifications

View file

@ -2,15 +2,33 @@ import fileSizeFormatService from '../../services/file_size_format/file_size_for
const FeaturesPanel = { const FeaturesPanel = {
computed: { computed: {
shout: function () { return this.$store.state.instance.shoutAvailable }, shout: function () {
pleromaChatMessages: function () { return this.$store.state.instance.pleromaChatMessagesAvailable }, return this.$store.state.instance.shoutAvailable
gopher: function () { return this.$store.state.instance.gopherAvailable }, },
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled }, pleromaChatMessages: function () {
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable }, return this.$store.state.instance.pleromaChatMessagesAvailable
minimalScopesMode: function () { return this.$store.state.instance.minimalScopesMode }, },
textlimit: function () { return this.$store.state.instance.textlimit }, gopher: function () {
uploadlimit: function () { return fileSizeFormatService.fileSizeFormat(this.$store.state.instance.uploadlimit) } return this.$store.state.instance.gopherAvailable
} },
whoToFollow: function () {
return this.$store.state.instance.suggestionsEnabled
},
mediaProxy: function () {
return this.$store.state.instance.mediaProxyAvailable
},
minimalScopesMode: function () {
return this.$store.state.instance.minimalScopesMode
},
textlimit: function () {
return this.$store.state.instance.textlimit
},
uploadlimit: function () {
return fileSizeFormatService.fileSizeFormat(
this.$store.state.instance.uploadlimit,
)
},
},
} }
export default FeaturesPanel export default FeaturesPanel

View file

@ -1,14 +1,12 @@
import RuffleService from '../../services/ruffle_service/ruffle_service.js' import RuffleService from '../../services/ruffle_service/ruffle_service.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faExclamationTriangle,
faStop, faStop,
faExclamationTriangle
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faStop, faExclamationTriangle)
faStop,
faExclamationTriangle
)
const Flash = { const Flash = {
props: ['src'], props: ['src'],
@ -16,7 +14,7 @@ const Flash = {
return { return {
player: false, // can be true, "hidden", false. hidden = element exists player: false, // can be true, "hidden", false. hidden = element exists
loaded: false, loaded: false,
ruffleInstance: null ruffleInstance: null,
} }
}, },
methods: { methods: {
@ -26,15 +24,18 @@ const Flash = {
RuffleService.getRuffle().then((ruffle) => { RuffleService.getRuffle().then((ruffle) => {
const player = ruffle.newest().createPlayer() const player = ruffle.newest().createPlayer()
player.config = { player.config = {
letterbox: 'on' letterbox: 'on',
} }
const container = this.$refs.container const container = this.$refs.container
container.appendChild(player) container.appendChild(player)
player.style.width = '100%' player.style.width = '100%'
player.style.height = '100%' player.style.height = '100%'
player.load(this.src).then(() => { player
.load(this.src)
.then(() => {
this.player = true this.player = true
}).catch((e) => { })
.catch((e) => {
console.error('Error loading ruffle', e) console.error('Error loading ruffle', e)
this.player = 'error' this.player = 'error'
}) })
@ -46,8 +47,8 @@ const Flash = {
this.ruffleInstance && this.ruffleInstance.remove() this.ruffleInstance && this.ruffleInstance.remove()
this.player = false this.player = false
this.$emit('playerClosed') this.$emit('playerClosed')
} },
} },
} }
export default Flash export default Flash

View file

@ -1,14 +1,17 @@
import {
requestFollow,
requestUnfollow,
} from '../../services/follow_manipulate/follow_manipulate'
import ConfirmModal from '../confirm_modal/confirm_modal.vue' import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
export default { export default {
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'], props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
components: { components: {
ConfirmModal ConfirmModal,
}, },
data() { data() {
return { return {
inProgress: false, inProgress: false,
showingConfirmUnfollow: false showingConfirmUnfollow: false,
} }
}, },
computed: { computed: {
@ -40,7 +43,7 @@ export default {
}, },
disabled() { disabled() {
return this.inProgress || this.user.deactivated return this.inProgress || this.user.deactivated
} },
}, },
methods: { methods: {
showConfirmUnfollow() { showConfirmUnfollow() {
@ -50,7 +53,9 @@ export default {
this.showingConfirmUnfollow = false this.showingConfirmUnfollow = false
}, },
onClick() { onClick() {
this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow() this.relationship.following || this.relationship.requested
? this.unfollow()
: this.follow()
}, },
follow() { follow() {
this.inProgress = true this.inProgress = true
@ -70,10 +75,13 @@ export default {
this.inProgress = true this.inProgress = true
requestUnfollow(this.relationship.id, store).then(() => { requestUnfollow(this.relationship.id, store).then(() => {
this.inProgress = false this.inProgress = false
store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id }) store.commit('removeStatus', {
timeline: 'friends',
userId: this.relationship.id,
})
}) })
this.hideConfirmUnfollow() this.hideConfirmUnfollow()
} },
} },
} }

View file

@ -1,18 +1,15 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue' import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue'
import FollowButton from '../follow_button/follow_button.vue' import FollowButton from '../follow_button/follow_button.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue'
import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue' import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue'
const FollowCard = { const FollowCard = {
props: [ props: ['user', 'noFollowsYou'],
'user',
'noFollowsYou'
],
components: { components: {
BasicUserCard, BasicUserCard,
RemoteFollow, RemoteFollow,
FollowButton, FollowButton,
RemoveFollowerButton RemoveFollowerButton,
}, },
computed: { computed: {
isMe() { isMe() {
@ -23,8 +20,8 @@ const FollowCard = {
}, },
relationship() { relationship() {
return this.$store.getters.relationship(this.user.id) return this.$store.getters.relationship(this.user.id)
} },
} },
} }
export default FollowCard export default FollowCard

View file

@ -1,23 +1,25 @@
import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js'
import BasicUserCard from '../basic_user_card/basic_user_card.vue' import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue' import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js'
const FollowRequestCard = { const FollowRequestCard = {
props: ['user'], props: ['user'],
components: { components: {
BasicUserCard, BasicUserCard,
ConfirmModal ConfirmModal,
}, },
data() { data() {
return { return {
showingApproveConfirmDialog: false, showingApproveConfirmDialog: false,
showingDenyConfirmDialog: false showingDenyConfirmDialog: false,
} }
}, },
methods: { methods: {
findFollowRequestNotificationId() { findFollowRequestNotificationId() {
const notif = notificationsFromStore(this.$store).find( const notif = notificationsFromStore(this.$store).find(
(notif) => notif.from_profile.id === this.user.id && notif.type === 'follow_request' (notif) =>
notif.from_profile.id === this.user.id &&
notif.type === 'follow_request',
) )
return notif && notif.id return notif && notif.id
}, },
@ -48,9 +50,9 @@ const FollowRequestCard = {
this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId }) this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
this.$store.dispatch('updateNotification', { this.$store.dispatch('updateNotification', {
id: notifId, id: notifId,
updater: notification => { updater: (notification) => {
notification.type = 'follow' notification.type = 'follow'
} },
}) })
this.hideApproveConfirmDialog() this.hideApproveConfirmDialog()
}, },
@ -63,13 +65,14 @@ const FollowRequestCard = {
}, },
doDeny() { doDeny() {
const notifId = this.findFollowRequestNotificationId() const notifId = this.findFollowRequestNotificationId()
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) this.$store.state.api.backendInteractor
.denyUser({ id: this.user.id })
.then(() => { .then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: notifId }) this.$store.dispatch('dismissNotificationLocal', { id: notifId })
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
}) })
this.hideDenyConfirmDialog() this.hideDenyConfirmDialog()
} },
}, },
computed: { computed: {
mergedConfig() { mergedConfig() {
@ -80,8 +83,8 @@ const FollowRequestCard = {
}, },
shouldConfirmDeny() { shouldConfirmDeny() {
return this.mergedConfig.modalOnDenyFollow return this.mergedConfig.modalOnDenyFollow
} },
} },
} }
export default FollowRequestCard export default FollowRequestCard

View file

@ -2,13 +2,13 @@ import FollowRequestCard from '../follow_request_card/follow_request_card.vue'
const FollowRequests = { const FollowRequests = {
components: { components: {
FollowRequestCard FollowRequestCard,
}, },
computed: { computed: {
requests() { requests() {
return this.$store.state.api.followRequests return this.$store.state.api.followRequests
} },
} },
} }
export default FollowRequests export default FollowRequests

View file

@ -1,30 +1,24 @@
import Select from '../select/select.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
import Popover from 'src/components/popover/popover.vue' import Popover from 'src/components/popover/popover.vue'
import { useInterfaceStore } from 'src/stores/interface' import { useInterfaceStore } from 'src/stores/interface'
import Select from '../select/select.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faExclamationTriangle, faExclamationTriangle,
faFont,
faKeyboard, faKeyboard,
faFont
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faExclamationTriangle, faKeyboard, faFont)
faExclamationTriangle,
faKeyboard,
faFont
)
export default { export default {
components: { components: {
Select, Select,
Checkbox, Checkbox,
Popover Popover,
}, },
props: [ props: ['name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'],
'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'
],
mounted() { mounted() {
useInterfaceStore().queryLocalFonts() useInterfaceStore().queryLocalFonts()
}, },
@ -37,14 +31,14 @@ export default {
'serif', 'serif',
'sans-serif', 'sans-serif',
'monospace', 'monospace',
...(this.options || []) ...(this.options || []),
].filter(_ => _) ].filter((_) => _),
} }
}, },
methods: { methods: {
toggleManualEntry() { toggleManualEntry() {
this.manualEntry = !this.manualEntry this.manualEntry = !this.manualEntry
} },
}, },
computed: { computed: {
present() { present() {
@ -55,6 +49,6 @@ export default {
}, },
localFontsSize() { localFontsSize() {
return useInterfaceStore().localFonts?.length return useInterfaceStore().localFonts?.length
} },
} },
} }

View file

@ -1,11 +1,14 @@
import Timeline from '../timeline/timeline.vue' import Timeline from '../timeline/timeline.vue'
const FriendsTimeline = { const FriendsTimeline = {
components: { components: {
Timeline Timeline,
}, },
computed: { computed: {
timeline () { return this.$store.state.statuses.timelines.friends } timeline() {
} return this.$store.state.statuses.timelines.friends
},
},
} }
export default FriendsTimeline export default FriendsTimeline

View file

@ -4,37 +4,37 @@ export default {
virtual: true, virtual: true,
variants: { variants: {
greentext: '.greentext', greentext: '.greentext',
cyantext: '.cyantext' cyantext: '.cyantext',
}, },
states: { states: {
faint: '.faint' faint: '.faint',
}, },
defaultRules: [ defaultRules: [
{ {
directives: { directives: {
textColor: '--text', textColor: '--text',
textAuto: 'preserve' textAuto: 'preserve',
} },
}, },
{ {
state: ['faint'], state: ['faint'],
directives: { directives: {
textOpacity: 0.5 textOpacity: 0.5,
} },
}, },
{ {
variant: 'greentext', variant: 'greentext',
directives: { directives: {
textColor: '--cGreen', textColor: '--cGreen',
textAuto: 'preserve' textAuto: 'preserve',
} },
}, },
{ {
variant: 'cyantext', variant: 'cyantext',
directives: { directives: {
textColor: '--cBlue', textColor: '--cBlue',
textAuto: 'preserve' textAuto: 'preserve',
} },
} },
] ],
} }

View file

@ -1,6 +1,7 @@
import { set, sumBy } from 'lodash'
import { useMediaViewerStore } from 'src/stores/media_viewer' import { useMediaViewerStore } from 'src/stores/media_viewer'
import Attachment from '../attachment/attachment.vue' import Attachment from '../attachment/attachment.vue'
import { sumBy, set } from 'lodash'
const Gallery = { const Gallery = {
props: [ props: [
@ -17,12 +18,12 @@ const Gallery = {
'shiftUpAttachment', 'shiftUpAttachment',
'shiftDnAttachment', 'shiftDnAttachment',
'editAttachment', 'editAttachment',
'grid' 'grid',
], ],
data() { data() {
return { return {
sizes: {}, sizes: {},
hidingLong: true hidingLong: true,
} }
}, },
components: { Attachment }, components: { Attachment },
@ -31,35 +32,54 @@ const Gallery = {
if (!this.attachments) { if (!this.attachments) {
return [] return []
} }
const attachments = this.limit > 0 const attachments =
this.limit > 0
? this.attachments.slice(0, this.limit) ? this.attachments.slice(0, this.limit)
: this.attachments : this.attachments
if (this.size === 'hide') { if (this.size === 'hide') {
return attachments.map(item => ({ minimal: true, items: [item] })) return attachments.map((item) => ({ minimal: true, items: [item] }))
} }
const rows = this.grid const rows = this.grid
? [{ grid: true, items: attachments }] ? [{ grid: true, items: attachments }]
: attachments.reduce((acc, attachment, i) => { : attachments
.reduce(
(acc, attachment, i) => {
if (attachment.mimetype.includes('audio')) { if (attachment.mimetype.includes('audio')) {
return [...acc, { audio: true, items: [attachment] }, { items: [] }] return [
...acc,
{ audio: true, items: [attachment] },
{ items: [] },
]
} }
if (!( if (
!(
attachment.mimetype.includes('image') || attachment.mimetype.includes('image') ||
attachment.mimetype.includes('video') || attachment.mimetype.includes('video') ||
attachment.mimetype.includes('flash') attachment.mimetype.includes('flash')
)) { )
return [...acc, { minimal: true, items: [attachment] }, { items: [] }] ) {
return [
...acc,
{ minimal: true, items: [attachment] },
{ items: [] },
]
} }
const maxPerRow = 3 const maxPerRow = 3
const attachmentsRemaining = this.attachments.length - i + 1 const attachmentsRemaining = this.attachments.length - i + 1
const currentRow = acc[acc.length - 1].items const currentRow = acc[acc.length - 1].items
currentRow.push(attachment) currentRow.push(attachment)
if (currentRow.length >= maxPerRow && attachmentsRemaining > maxPerRow) { if (
currentRow.length >= maxPerRow &&
attachmentsRemaining > maxPerRow
) {
return [...acc, { items: [] }] return [...acc, { items: [] }]
} else { } else {
return acc return acc
} }
}, [{ items: [] }]).filter(_ => _.items.length > 0) },
[{ items: [] }],
)
.filter((_) => _.items.length > 0)
return rows return rows
}, },
attachmentsDimensionalScore() { attachmentsDimensionalScore() {
@ -83,7 +103,7 @@ const Gallery = {
} else { } else {
return this.attachmentsDimensionalScore > 1 return this.attachmentsDimensionalScore > 1
} }
} },
}, },
methods: { methods: {
onNaturalSizeLoad({ id, width, height }) { onNaturalSizeLoad({ id, width, height }) {
@ -93,11 +113,11 @@ const Gallery = {
if (row.audio) { if (row.audio) {
return { 'padding-bottom': '25%' } // fixed reduced height for audio return { 'padding-bottom': '25%' } // fixed reduced height for audio
} else if (!row.minimal && !row.grid) { } else if (!row.minimal && !row.grid) {
return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` } return { 'padding-bottom': `${100 / (row.items.length + 0.6)}%` }
} }
}, },
itemStyle(id, row) { itemStyle(id, row) {
const total = sumBy(row, item => this.getAspectRatio(item.id)) const total = sumBy(row, (item) => this.getAspectRatio(item.id))
return { flex: `${this.getAspectRatio(id) / total} 1 0%` } return { flex: `${this.getAspectRatio(id) / total} 1 0%` }
}, },
getAspectRatio(id) { getAspectRatio(id) {
@ -113,8 +133,8 @@ const Gallery = {
}, },
onMedia() { onMedia() {
useMediaViewerStore().setMedia(this.attachments) useMediaViewerStore().setMedia(this.attachments)
} },
} },
} }
export default Gallery export default Gallery

View file

@ -1,24 +1,21 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faTimes
} from '@fortawesome/free-solid-svg-icons'
import { useInterfaceStore } from 'src/stores/interface' import { useInterfaceStore } from 'src/stores/interface'
library.add( import { library } from '@fortawesome/fontawesome-svg-core'
faTimes import { faTimes } from '@fortawesome/free-solid-svg-icons'
)
library.add(faTimes)
const GlobalNoticeList = { const GlobalNoticeList = {
computed: { computed: {
notices() { notices() {
return useInterfaceStore().globalNotices return useInterfaceStore().globalNotices
} },
}, },
methods: { methods: {
closeNotice(notice) { closeNotice(notice) {
useInterfaceStore().removeGlobalNotice(notice) useInterfaceStore().removeGlobalNotice(notice)
} },
} },
} }
export default GlobalNoticeList export default GlobalNoticeList

View file

@ -5,17 +5,17 @@ const HashtagLink = {
props: { props: {
url: { url: {
required: true, required: true,
type: String type: String,
}, },
content: { content: {
required: true, required: true,
type: String type: String,
}, },
tag: { tag: {
required: false, required: false,
type: String, type: String,
default: '' default: '',
} },
}, },
methods: { methods: {
onClick() { onClick() {
@ -29,8 +29,8 @@ const HashtagLink = {
}, },
generateTagLink(tag) { generateTagLink(tag) {
return `/tag/${tag}` return `/tag/${tag}`
} },
} },
} }
export default HashtagLink export default HashtagLink

View file

@ -7,8 +7,8 @@ export default {
component: 'Icon', component: 'Icon',
directives: { directives: {
textColor: '$blend(--stack 0.5 --parent--text)', textColor: '$blend(--stack 0.5 --parent--text)',
textAuto: 'no-auto' textAuto: 'no-auto',
} },
} },
] ],
} }

View file

@ -1,29 +1,26 @@
import 'cropperjs' // This adds all of the cropperjs's components into DOM import 'cropperjs' // This adds all of the cropperjs's components into DOM
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faCircleNotch
} from '@fortawesome/free-solid-svg-icons'
library.add( import { library } from '@fortawesome/fontawesome-svg-core'
faCircleNotch import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
)
library.add(faCircleNotch)
const ImageCropper = { const ImageCropper = {
props: { props: {
// Mime-types to accept, i.e. which filetypes to accept (.gif, .png, etc.) // Mime-types to accept, i.e. which filetypes to accept (.gif, .png, etc.)
mimes: { mimes: {
type: String, type: String,
default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon' default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon',
}, },
// Fixed aspect-ratio for selection box // Fixed aspect-ratio for selection box
aspectRatio: { aspectRatio: {
type: Number type: Number,
} },
}, },
data() { data() {
return { return {
dataUrl: undefined, dataUrl: undefined,
filename: undefined filename: undefined,
} }
}, },
emits: [ emits: [
@ -44,7 +41,7 @@ const ImageCropper = {
cropperPromise = Promise.resolve() cropperPromise = Promise.resolve()
} }
cropperPromise.then(canvas => { cropperPromise.then((canvas) => {
this.$emit('submit', { canvas, file: this.file }) this.$emit('submit', { canvas, file: this.file })
}) })
}, },
@ -66,10 +63,10 @@ const ImageCropper = {
}, },
inSelection(selection, maxSelection) { inSelection(selection, maxSelection) {
return ( return (
selection.x >= maxSelection.x selection.x >= maxSelection.x &&
&& selection.y >= maxSelection.y selection.y >= maxSelection.y &&
&& (selection.x + selection.width) <= (maxSelection.x + maxSelection.width) selection.x + selection.width <= maxSelection.x + maxSelection.width &&
&& (selection.y + selection.height) <= (maxSelection.y + maxSelection.height) selection.y + selection.height <= maxSelection.y + maxSelection.height
) )
}, },
onCropperSelectionChange(event) { onCropperSelectionChange(event) {
@ -84,10 +81,10 @@ const ImageCropper = {
} }
if (!this.inSelection(selection, maxSelection)) { if (!this.inSelection(selection, maxSelection)) {
event.preventDefault(); event.preventDefault()
}
} }
}, },
},
mounted() { mounted() {
// listen for input file changes // listen for input file changes
const fileInput = this.$refs.input const fileInput = this.$refs.input
@ -96,7 +93,7 @@ const ImageCropper = {
beforeUnmount: function () { beforeUnmount: function () {
const fileInput = this.$refs.input const fileInput = this.$refs.input
fileInput.removeEventListener('change', this.readFile) fileInput.removeEventListener('change', this.readFile)
} },
} }
export default ImageCropper export default ImageCropper

View file

@ -1,30 +1,24 @@
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import { faCircleNotch, faTimes } from '@fortawesome/free-solid-svg-icons'
faCircleNotch,
faTimes
} from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faCircleNotch, faTimes)
faCircleNotch,
faTimes
)
const Importer = { const Importer = {
props: { props: {
submitHandler: { submitHandler: {
type: Function, type: Function,
required: true required: true,
}, },
submitButtonLabel: { type: String }, submitButtonLabel: { type: String },
successMessage: { type: String }, successMessage: { type: String },
errorMessage: { type: String } errorMessage: { type: String },
}, },
data() { data() {
return { return {
file: null, file: null,
error: false, error: false,
success: false, success: false,
submitting: false submitting: false,
} }
}, },
methods: { methods: {
@ -35,15 +29,21 @@ const Importer = {
this.dismiss() this.dismiss()
this.submitting = true this.submitting = true
this.submitHandler(this.file) this.submitHandler(this.file)
.then(() => { this.success = true }) .then(() => {
.catch(() => { this.error = true }) this.success = true
.finally(() => { this.submitting = false }) })
.catch(() => {
this.error = true
})
.finally(() => {
this.submitting = false
})
}, },
dismiss() { dismiss() {
this.success = false this.success = false
this.error = false this.error = false
} },
} },
} }
export default Importer export default Importer

View file

@ -4,91 +4,96 @@ export default {
states: { states: {
hover: ':is(:hover, :focus-visible):not(.disabled)', hover: ':is(:hover, :focus-visible):not(.disabled)',
focused: ':focus-within', focused: ':focus-within',
disabled: '.disabled' disabled: '.disabled',
}, },
variants: { variants: {
checkbox: '.-checkbox', checkbox: '.-checkbox',
radio: '.-radio' radio: '.-radio',
}, },
validInnerComponents: [ validInnerComponents: ['Text', 'Icon'],
'Text',
'Icon'
],
defaultRules: [ defaultRules: [
{ {
component: 'Root', component: 'Root',
directives: { directives: {
'--defaultInputBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2), $borderSide(#000000 top 0.2), inset 0 0 2 #000000 / 0.15, 1 0 1 1 --text / 0.15, -1 0 1 1 --text / 0.15', '--defaultInputBevel':
'shadow | $borderSide(#FFFFFF bottom 0.2), $borderSide(#000000 top 0.2), inset 0 0 2 #000000 / 0.15, 1 0 1 1 --text / 0.15, -1 0 1 1 --text / 0.15',
'--defaultInputHoverGlow': 'shadow | 0 0 4 --text / 0.5', '--defaultInputHoverGlow': 'shadow | 0 0 4 --text / 0.5',
'--defaultInputFocusGlow': 'shadow | 0 0 4 4 --link / 0.5' '--defaultInputFocusGlow': 'shadow | 0 0 4 4 --link / 0.5',
} },
}, },
{ {
variant: 'checkbox', variant: 'checkbox',
directives: { directives: {
roundness: 1 roundness: 1,
} },
}, },
{ {
directives: { directives: {
'--font': 'generic | inherit', '--font': 'generic | inherit',
background: '--fg, -5', background: '--fg, -5',
roundness: 3, roundness: 3,
shadow: [{ shadow: [
{
x: 0, x: 0,
y: 0, y: 0,
blur: 2, blur: 2,
spread: 0, spread: 0,
color: '#000000', color: '#000000',
alpha: 1 alpha: 1,
}, '--defaultInputBevel'] },
} '--defaultInputBevel',
],
},
}, },
{ {
state: ['hover'], state: ['hover'],
directives: { directives: {
shadow: ['--defaultInputHoverGlow', '--defaultInputBevel'] shadow: ['--defaultInputHoverGlow', '--defaultInputBevel'],
} },
}, },
{ {
state: ['focused'], state: ['focused'],
directives: { directives: {
shadow: ['--defaultInputFocusGlow', '--defaultInputBevel'] shadow: ['--defaultInputFocusGlow', '--defaultInputBevel'],
} },
}, },
{ {
state: ['focused', 'hover'], state: ['focused', 'hover'],
directives: { directives: {
shadow: ['--defaultInputFocusGlow', '--defaultInputHoverGlow', '--defaultInputBevel'] shadow: [
} '--defaultInputFocusGlow',
'--defaultInputHoverGlow',
'--defaultInputBevel',
],
},
}, },
{ {
state: ['disabled'], state: ['disabled'],
directives: { directives: {
background: '--parent' background: '--parent',
} },
}, },
{ {
component: 'Text', component: 'Text',
parent: { parent: {
component: 'Input', component: 'Input',
state: ['disabled'] state: ['disabled'],
}, },
directives: { directives: {
textOpacity: 0.25, textOpacity: 0.25,
textOpacityMode: 'blend' textOpacityMode: 'blend',
} },
}, },
{ {
component: 'Icon', component: 'Icon',
parent: { parent: {
component: 'Input', component: 'Input',
state: ['disabled'] state: ['disabled'],
}, },
directives: { directives: {
textOpacity: 0.25, textOpacity: 0.25,
textOpacityMode: 'blend' textOpacityMode: 'blend',
} },
} },
] ],
} }

View file

@ -2,8 +2,8 @@ const InstanceSpecificPanel = {
computed: { computed: {
instanceSpecificPanelContent() { instanceSpecificPanelContent() {
return this.$store.state.instance.instanceSpecificPanelContent return this.$store.state.instance.instanceSpecificPanelContent
} },
} },
} }
export default InstanceSpecificPanel export default InstanceSpecificPanel

View file

@ -1,5 +1,5 @@
import Notifications from '../notifications/notifications.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import Notifications from '../notifications/notifications.vue'
const tabModeDict = { const tabModeDict = {
mentions: ['mention'], mentions: ['mention'],
@ -8,26 +8,29 @@ const tabModeDict = {
follows: ['follow'], follows: ['follow'],
reactions: ['pleroma:emoji_reaction'], reactions: ['pleroma:emoji_reaction'],
reports: ['pleroma:report'], reports: ['pleroma:report'],
moves: ['move'] moves: ['move'],
} }
const Interactions = { const Interactions = {
data() { data() {
return { return {
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move, allowFollowingMove:
this.$store.state.users.currentUser.allow_following_move,
filterMode: tabModeDict.mentions, filterMode: tabModeDict.mentions,
canSeeReports: this.$store.state.users.currentUser.privileges.includes('reports_manage_reports') canSeeReports: this.$store.state.users.currentUser.privileges.includes(
'reports_manage_reports',
),
} }
}, },
methods: { methods: {
onModeSwitch(key) { onModeSwitch(key) {
this.filterMode = tabModeDict[key] this.filterMode = tabModeDict[key]
} },
}, },
components: { components: {
Notifications, Notifications,
TabSwitcher TabSwitcher,
} },
} }
export default Interactions export default Interactions

View file

@ -1,27 +1,26 @@
import localeService from '../../services/locale/locale.service.js' import { v4 as uuidv4 } from 'uuid'
import Select from '../select/select.vue'
import ProfileSettingIndicator from 'src/components/settings_modal/helpers/profile_setting_indicator.vue' import ProfileSettingIndicator from 'src/components/settings_modal/helpers/profile_setting_indicator.vue'
import localeService from '../../services/locale/locale.service.js'
import { v4 as uuidv4 } from 'uuid'; import Select from '../select/select.vue'
export default { export default {
components: { components: {
Select, Select,
ProfileSettingIndicator ProfileSettingIndicator,
}, },
props: { props: {
// List of languages (or just one language) // List of languages (or just one language)
modelValue: { modelValue: {
type: [Array, String], type: [Array, String],
required: true required: true,
}, },
// Is this setting stored in user profile (true) or elsewhere (false) // Is this setting stored in user profile (true) or elsewhere (false)
// Doesn't affect storage, just shows an icon if true // Doesn't affect storage, just shows an icon if true
profile: { profile: {
type: Boolean, type: Boolean,
default: false default: false,
} },
}, },
emits: ['update:modelValue'], emits: ['update:modelValue'],
computed: { computed: {
@ -33,12 +32,14 @@ export default {
}, },
controlledLanguage: { controlledLanguage: {
get: function () { get: function () {
return Array.isArray(this.modelValue) ? this.modelValue : [this.modelValue] return Array.isArray(this.modelValue)
? this.modelValue
: [this.modelValue]
}, },
set: function (val) { set: function (val) {
this.$emit('update:modelValue', val) this.$emit('update:modelValue', val)
} },
} },
}, },
methods: { methods: {
@ -57,6 +58,6 @@ export default {
const lang = [...this.controlledLanguage] const lang = [...this.controlledLanguage]
lang.splice(index, 1) lang.splice(index, 1)
this.controlledLanguage = lang this.controlledLanguage = lang
} },
} },
} }

View file

@ -2,14 +2,10 @@ import { mapGetters } from 'vuex'
const LinkPreview = { const LinkPreview = {
name: 'LinkPreview', name: 'LinkPreview',
props: [ props: ['card', 'size', 'nsfw'],
'card',
'size',
'nsfw'
],
data() { data() {
return { return {
imageLoaded: false imageLoaded: false,
} }
}, },
computed: { computed: {
@ -28,9 +24,7 @@ const LinkPreview = {
hideNsfwConfig() { hideNsfwConfig() {
return this.mergedConfig.hideNsfw return this.mergedConfig.hideNsfw
}, },
...mapGetters([ ...mapGetters(['mergedConfig']),
'mergedConfig'
])
}, },
created() { created() {
if (this.useImage) { if (this.useImage) {
@ -40,7 +34,7 @@ const LinkPreview = {
} }
newImg.src = this.card.image newImg.src = this.card.image
} }
} },
} }
export default LinkPreview export default LinkPreview

View file

@ -3,22 +3,22 @@ export default {
selector: 'a', selector: 'a',
virtual: true, virtual: true,
states: { states: {
faint: '.faint' faint: '.faint',
}, },
defaultRules: [ defaultRules: [
{ {
component: 'Link', component: 'Link',
directives: { directives: {
textColor: '--link' textColor: '--link',
} },
}, },
{ {
component: 'Link', component: 'Link',
state: ['faint'], state: ['faint'],
directives: { directives: {
textOpacity: 0.5, textOpacity: 0.5,
textOpacityMode: 'fake' textOpacityMode: 'fake',
} },
} },
] ],
} }

View file

@ -29,20 +29,20 @@ export default {
props: { props: {
items: { items: {
type: Array, type: Array,
default: () => [] default: () => [],
}, },
getKey: { getKey: {
type: Function, type: Function,
default: item => item.id default: (item) => item.id,
}, },
getClass: { getClass: {
type: Function, type: Function,
default: () => '' default: () => '',
}, },
nonInteractive: { nonInteractive: {
type: Boolean, type: Boolean,
default: false default: false,
} },
} },
} }
</script> </script>

View file

@ -4,16 +4,16 @@ import ListsCard from '../lists_card/lists_card.vue'
const Lists = { const Lists = {
data() { data() {
return { return {
isNew: false isNew: false,
} }
}, },
components: { components: {
ListsCard ListsCard,
}, },
computed: { computed: {
lists() { lists() {
return useListsStore().allLists return useListsStore().allLists
} },
}, },
methods: { methods: {
cancelNewList() { cancelNewList() {
@ -21,8 +21,8 @@ const Lists = {
}, },
newList() { newList() {
this.isNew = true this.isNew = true
} },
} },
} }
export default Lists export default Lists

View file

@ -1,16 +1,10 @@
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'
faEllipsisH
} from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faEllipsisH)
faEllipsisH
)
const ListsCard = { const ListsCard = {
props: [ props: ['list'],
'list'
]
} }
export default ListsCard export default ListsCard

Some files were not shown because too many files have changed in this diff Show more