Merge branch 'biome' into 'develop'
Migration to Biome See merge request pleroma/pleroma-fe!2193
This commit is contained in:
commit
aa25cd04b1
426 changed files with 55607 additions and 18546 deletions
|
|
@ -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
145
biome.json
Normal 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/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
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 {
|
||||||
try {
|
try {
|
||||||
|
|
@ -15,4 +15,4 @@ export const getCommitHash = (() => {
|
||||||
return 'UNKNOWN'
|
return 'UNKNOWN'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,28 +13,31 @@ const copyPlugin = ({ inUrl, inFs }) => {
|
||||||
let copyTarget
|
let copyTarget
|
||||||
const handler = serveStatic(inFs)
|
const handler = serveStatic(inFs)
|
||||||
|
|
||||||
return [{
|
return [
|
||||||
name: 'copy-plugin-serve',
|
{
|
||||||
apply: 'serve',
|
name: 'copy-plugin-serve',
|
||||||
configureServer (server) {
|
apply: 'serve',
|
||||||
server.middlewares.use(prefix, handler)
|
configureServer(server) {
|
||||||
}
|
server.middlewares.use(prefix, handler)
|
||||||
}, {
|
},
|
||||||
name: 'copy-plugin-build',
|
|
||||||
apply: 'build',
|
|
||||||
configResolved (config) {
|
|
||||||
copyTarget = resolve(config.root, config.build.outDir, subdir)
|
|
||||||
},
|
},
|
||||||
closeBundle: {
|
{
|
||||||
order: 'post',
|
name: 'copy-plugin-build',
|
||||||
sequential: true,
|
apply: 'build',
|
||||||
async handler () {
|
configResolved(config) {
|
||||||
console.info(`Copying '${inFs}' to ${copyTarget}...`)
|
copyTarget = resolve(config.root, config.build.outDir, subdir)
|
||||||
await cp(inFs, copyTarget, { recursive: true })
|
},
|
||||||
console.info('Done.')
|
closeBundle: {
|
||||||
}
|
order: 'post',
|
||||||
}
|
sequential: true,
|
||||||
}]
|
async handler() {
|
||||||
|
console.info(`Copying '${inFs}' to ${copyTarget}...`)
|
||||||
|
await cp(inFs, copyTarget, { recursive: true })
|
||||||
|
console.info('Done.')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default copyPlugin
|
export default copyPlugin
|
||||||
|
|
|
||||||
|
|
@ -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,11 +25,14 @@ 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)
|
)
|
||||||
.join(',\n')
|
)
|
||||||
|
.filter((k) => k)
|
||||||
|
.join(',\n')
|
||||||
|
|
||||||
return `
|
return `
|
||||||
export const annotationsLoader = {
|
export const annotationsLoader = {
|
||||||
|
|
@ -43,21 +48,21 @@ const emojisPlugin = () => {
|
||||||
let projectRoot
|
let projectRoot
|
||||||
return {
|
return {
|
||||||
name: 'emojis-plugin',
|
name: 'emojis-plugin',
|
||||||
configResolved (conf) {
|
configResolved(conf) {
|
||||||
projectRoot = conf.root
|
projectRoot = conf.root
|
||||||
},
|
},
|
||||||
resolveId (id) {
|
resolveId(id) {
|
||||||
if (id === emojiAnnotationsId) {
|
if (id === emojiAnnotationsId) {
|
||||||
return emojiAnnotationsIdResolved
|
return emojiAnnotationsIdResolved
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
async load (id) {
|
async load(id) {
|
||||||
if (id === emojiAnnotationsIdResolved) {
|
if (id === emojiAnnotationsIdResolved) {
|
||||||
return await getAllAccessibleAnnotations(projectRoot)
|
return await getAllAccessibleAnnotations(projectRoot)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
||||||
|
|
@ -8,10 +8,10 @@ const mswPlugin = () => {
|
||||||
return {
|
return {
|
||||||
name: 'msw-plugin',
|
name: 'msw-plugin',
|
||||||
apply: 'serve',
|
apply: 'serve',
|
||||||
configResolved (conf) {
|
configResolved(conf) {
|
||||||
projectRoot = conf.root
|
projectRoot = conf.root
|
||||||
},
|
},
|
||||||
configureServer (server) {
|
configureServer(server) {
|
||||||
server.middlewares.use(async (req, res, next) => {
|
server.middlewares.use(async (req, res, next) => {
|
||||||
if (req.path === '/mockServiceWorker.js') {
|
if (req.path === '/mockServiceWorker.js') {
|
||||||
const file = await readFile(resolve(projectRoot, target))
|
const file = await readFile(resolve(projectRoot, target))
|
||||||
|
|
@ -21,7 +21,7 @@ const mswPlugin = () => {
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
const fileContent = await readFile(file, 'utf-8')
|
Object.entries(i18nFiles).map(async ([lang, file]) => {
|
||||||
const msg = {
|
const fileContent = await readFile(file, 'utf-8')
|
||||||
notifications: JSON.parse(fileContent).notifications || {}
|
const msg = {
|
||||||
}
|
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
|
||||||
|
|
|
||||||
|
|
@ -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,9 +31,10 @@ 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
|
||||||
if (name === swDest) {
|
if (name === swDest) {
|
||||||
return swFullSrc
|
return swFullSrc
|
||||||
|
|
@ -42,7 +43,7 @@ export const devSwPlugin = ({
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
async load (id) {
|
async load(id) {
|
||||||
if (id === swFullSrc) {
|
if (id === swFullSrc) {
|
||||||
return readFile(swFullSrc, 'utf-8')
|
return readFile(swFullSrc, 'utf-8')
|
||||||
} else if (id === swEnvNameResolved) {
|
} else if (id === swEnvNameResolved) {
|
||||||
|
|
@ -55,7 +56,7 @@ export const devSwPlugin = ({
|
||||||
* during dev, and firefox does not support ESM as service worker
|
* during dev, and firefox does not support ESM as service worker
|
||||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=1360870
|
* https://bugzilla.mozilla.org/show_bug.cgi?id=1360870
|
||||||
*/
|
*/
|
||||||
async transform (code, id) {
|
async transform(code, id) {
|
||||||
if (id === swFullSrc && transformSW) {
|
if (id === swFullSrc && transformSW) {
|
||||||
const res = await esbuild.build({
|
const res = await esbuild.build({
|
||||||
entryPoints: [swSrc],
|
entryPoints: [swSrc],
|
||||||
|
|
@ -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',
|
{
|
||||||
setup (b) {
|
name: 'vite-like-root-resolve',
|
||||||
b.onResolve(
|
setup(b) {
|
||||||
{ filter: new RegExp(/^\//) },
|
b.onResolve({ filter: new RegExp(/^\//) }, (args) => ({
|
||||||
args => ({
|
path: resolve(projectRoot, args.path.slice(1)),
|
||||||
path: resolve(projectRoot, args.path.slice(1))
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
name: 'sw-messages',
|
|
||||||
setup (b) {
|
|
||||||
b.onResolve(
|
|
||||||
{ filter: new RegExp('^' + swMessagesName + '$') },
|
|
||||||
args => ({
|
|
||||||
path: args.path,
|
|
||||||
namespace: 'sw-messages'
|
|
||||||
}))
|
}))
|
||||||
b.onLoad(
|
},
|
||||||
{ filter: /.*/, namespace: 'sw-messages' },
|
},
|
||||||
async () => ({
|
{
|
||||||
contents: await getSWMessagesAsText()
|
name: 'sw-messages',
|
||||||
|
setup(b) {
|
||||||
|
b.onResolve(
|
||||||
|
{ filter: new RegExp('^' + swMessagesName + '$') },
|
||||||
|
(args) => ({
|
||||||
|
path: args.path,
|
||||||
|
namespace: 'sw-messages',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
b.onLoad(
|
||||||
|
{ filter: /.*/, namespace: 'sw-messages' },
|
||||||
|
async () => ({
|
||||||
|
contents: await getSWMessagesAsText(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sw-env',
|
||||||
|
setup(b) {
|
||||||
|
b.onResolve(
|
||||||
|
{ filter: new RegExp('^' + swEnvName + '$') },
|
||||||
|
(args) => ({
|
||||||
|
path: args.path,
|
||||||
|
namespace: 'sw-env',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
b.onLoad({ filter: /.*/, namespace: 'sw-env' }, () => ({
|
||||||
|
contents: getDevSwEnv(),
|
||||||
}))
|
}))
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
name: 'sw-env',
|
],
|
||||||
setup (b) {
|
|
||||||
b.onResolve(
|
|
||||||
{ filter: new RegExp('^' + swEnvName + '$') },
|
|
||||||
args => ({
|
|
||||||
path: args.path,
|
|
||||||
namespace: 'sw-env'
|
|
||||||
}))
|
|
||||||
b.onLoad(
|
|
||||||
{ filter: /.*/, namespace: 'sw-env' },
|
|
||||||
() => ({
|
|
||||||
contents: getDevSwEnv()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
})
|
})
|
||||||
const text = res.outputFiles[0].text
|
const text = res.outputFiles[0].text
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,16 +121,13 @@ 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',
|
||||||
enforce: 'post',
|
enforce: 'post',
|
||||||
apply: 'build',
|
apply: 'build',
|
||||||
configResolved (resolvedConfig) {
|
configResolved(resolvedConfig) {
|
||||||
config = {
|
config = {
|
||||||
define: resolvedConfig.define,
|
define: resolvedConfig.define,
|
||||||
resolve: resolvedConfig.resolve,
|
resolve: resolvedConfig.resolve,
|
||||||
|
|
@ -138,50 +138,50 @@ 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: {
|
||||||
order: 'post',
|
order: 'post',
|
||||||
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) {
|
||||||
if (id === swEnvName) {
|
if (id === swEnvName) {
|
||||||
return swEnvNameResolved
|
return swEnvNameResolved
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
load (id) {
|
load(id) {
|
||||||
if (id === swEnvNameResolved) {
|
if (id === swEnvNameResolved) {
|
||||||
return getProdSwEnv({ assets })
|
return getProdSwEnv({ assets })
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
closeBundle: {
|
closeBundle: {
|
||||||
order: 'post',
|
order: 'post',
|
||||||
sequential: true,
|
sequential: true,
|
||||||
async handler () {
|
async handler() {
|
||||||
console.info('Building service worker for production')
|
console.info('Building service worker for production')
|
||||||
await build(config)
|
await build(config)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,9 +191,9 @@ const swMessagesNameResolved = '\0' + swMessagesName
|
||||||
export const swMessagesPlugin = () => {
|
export const swMessagesPlugin = () => {
|
||||||
return {
|
return {
|
||||||
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
|
||||||
|
|
@ -201,11 +201,11 @@ export const swMessagesPlugin = () => {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async load (id) {
|
async load(id) {
|
||||||
if (id === swMessagesNameResolved) {
|
if (id === swMessagesNameResolved) {
|
||||||
return await getSWMessagesAsText()
|
return await getSWMessagesAsText()
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,21 @@
|
||||||
|
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
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
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]
|
})
|
||||||
})
|
|
||||||
|
|
||||||
console.info('Updating emojis...')
|
console.info('Updating emojis...')
|
||||||
fs.writeFileSync('src/assets/emoji.json', JSON.stringify(res))
|
fs.writeFileSync('src/assets/emoji.json', JSON.stringify(res))
|
||||||
|
|
|
||||||
0
changelog.d/biome.skip
Normal file
0
changelog.d/biome.skip
Normal 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,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
])
|
||||||
|
|
|
||||||
15
package.json
15
package.json
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import autoprefixer from 'autoprefixer'
|
import autoprefixer from 'autoprefixer'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
plugins: [
|
plugins: [autoprefixer],
|
||||||
autoprefixer
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -65,16 +65,17 @@ 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 . . ."
|
"P P . L L"
|
||||||
"P P . . ."
|
"P P . . ."
|
||||||
"P P . E E"
|
"P P . . ."
|
||||||
"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 {
|
||||||
|
|
@ -84,7 +85,7 @@ body {
|
||||||
|
|
||||||
#chunk-P {
|
#chunk-P {
|
||||||
grid-area: P;
|
grid-area: P;
|
||||||
border-top-left-radius: calc(var(--logoChunkSize) / 2);
|
border-top-left-radius: calc(var(--logoChunkSize) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#chunk-L {
|
#chunk-L {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
207
src/App.js
207
src/App.js
|
|
@ -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,29 +40,33 @@ 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() {
|
||||||
this.removeSplash()
|
this.removeSplash()
|
||||||
},
|
},
|
||||||
currentTheme () {
|
currentTheme() {
|
||||||
this.setThemeBodyClass()
|
this.setThemeBodyClass()
|
||||||
},
|
},
|
||||||
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
|
||||||
const val = this.$store.getters.mergedConfig.interfaceLanguage
|
const val = this.$store.getters.mergedConfig.interfaceLanguage
|
||||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||||
|
|
@ -70,7 +76,7 @@ export default {
|
||||||
this.updateScrollState = throttle(this.scrollHandler, 200)
|
this.updateScrollState = throttle(this.scrollHandler, 200)
|
||||||
this.updateMobileState = throttle(this.resizeHandler, 200)
|
this.updateMobileState = throttle(this.resizeHandler, 200)
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted() {
|
||||||
window.addEventListener('resize', this.updateMobileState)
|
window.addEventListener('resize', this.updateMobileState)
|
||||||
this.scrollParent.addEventListener('scroll', this.updateScrollState)
|
this.scrollParent.addEventListener('scroll', this.updateScrollState)
|
||||||
|
|
||||||
|
|
@ -80,108 +86,145 @@ export default {
|
||||||
}
|
}
|
||||||
getOrCreateServiceWorker()
|
getOrCreateServiceWorker()
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted() {
|
||||||
window.removeEventListener('resize', this.updateMobileState)
|
window.removeEventListener('resize', this.updateMobileState)
|
||||||
this.scrollParent.removeEventListener('scroll', this.updateScrollState)
|
this.scrollParent.removeEventListener('scroll', this.updateScrollState)
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
themeApplied () {
|
themeApplied() {
|
||||||
return useInterfaceStore().themeApplied
|
return useInterfaceStore().themeApplied
|
||||||
},
|
},
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'stock'
|
return 'stock'
|
||||||
},
|
},
|
||||||
layoutModalClass () {
|
layoutModalClass() {
|
||||||
return '-' + this.layoutType
|
return '-' + this.layoutType
|
||||||
},
|
},
|
||||||
classes () {
|
classes() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'-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
|
||||||
instanceBackground () {
|
},
|
||||||
|
userBackground() {
|
||||||
|
return this.currentUser.background_image
|
||||||
|
},
|
||||||
|
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() {
|
||||||
bgStyle () {
|
return this.userBackground || this.instanceBackground
|
||||||
|
},
|
||||||
|
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
|
||||||
showInstanceSpecificPanel () {
|
},
|
||||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
suggestionsEnabled() {
|
||||||
|
return this.$store.state.instance.suggestionsEnabled
|
||||||
|
},
|
||||||
|
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'
|
||||||
},
|
},
|
||||||
isListEdit () {
|
isListEdit() {
|
||||||
return this.$route.name === 'lists-edit'
|
return this.$route.name === 'lists-edit'
|
||||||
},
|
},
|
||||||
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 },
|
showFeaturesPanel() {
|
||||||
editingAvailable () { return this.$store.state.instance.editingAvailable },
|
return this.$store.state.instance.showFeaturesPanel
|
||||||
shoutboxPosition () {
|
},
|
||||||
|
editingAvailable() {
|
||||||
|
return this.$store.state.instance.editingAvailable
|
||||||
|
},
|
||||||
|
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
|
||||||
reverseLayout () {
|
},
|
||||||
const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig
|
privateMode() {
|
||||||
|
return this.$store.state.instance.private
|
||||||
|
},
|
||||||
|
reverseLayout() {
|
||||||
|
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() {
|
||||||
useInterfaceStore().setLayoutWidth(windowWidth())
|
useInterfaceStore().setLayoutWidth(windowWidth())
|
||||||
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'])
|
||||||
|
|
@ -189,10 +232,10 @@ export default {
|
||||||
this.$refs.appContentRef.classList.remove(['-scrolled'])
|
this.$refs.appContentRef.classList.remove(['-scrolled'])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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()}`
|
||||||
|
|
@ -208,8 +251,10 @@ export default {
|
||||||
document.body.classList.remove(...oldTheme)
|
document.body.classList.remove(...oldTheme)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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')
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,25 +209,27 @@ 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 = (
|
||||||
Object.entries(values).map(async ([name, path]) => {
|
await Promise.all(
|
||||||
const resPack = await window.fetch(path + 'pack.json')
|
Object.entries(values).map(async ([name, path]) => {
|
||||||
let meta = {}
|
const resPack = await window.fetch(path + 'pack.json')
|
||||||
if (resPack.ok) {
|
let meta = {}
|
||||||
meta = await resPack.json()
|
if (resPack.ok) {
|
||||||
}
|
meta = await resPack.json()
|
||||||
return {
|
}
|
||||||
pack: name,
|
return {
|
||||||
path,
|
pack: name,
|
||||||
meta
|
path,
|
||||||
}
|
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:
|
||||||
? false
|
typeof federation.mrf_policies === 'undefined'
|
||||||
: metadata.federation.mrf_policies.includes('TagPolicy')
|
? false
|
||||||
|
: 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(
|
||||||
throw new Error('Each store file must export exactly one store as a named export. Check your code in src/stores/')
|
'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)
|
}
|
||||||
if (storeFuncName && typeof mod[storeFuncName] === 'function') {
|
const storeFuncName = Object.keys(mod).find(isStoreName)
|
||||||
const p = mod[storeFuncName]().$persistLoaded
|
if (storeFuncName && typeof mod[storeFuncName] === 'function') {
|
||||||
if (!(p instanceof Promise)) {
|
const p = mod[storeFuncName]().$persistLoaded
|
||||||
throw new Error(`${name} store's $persistLoaded is not a Promise. The persist plugin is not applied.`)
|
if (!(p instanceof Promise)) {
|
||||||
}
|
throw new Error(
|
||||||
await p
|
`${name} store's $persistLoaded is not a Promise. The persist plugin is not applied.`,
|
||||||
} else {
|
)
|
||||||
throw new Error(`Store module ${name} does not export a 'use...' function`)
|
|
||||||
}
|
}
|
||||||
}))
|
await p
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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.instance.redirectRootLogin
|
(store.state.users.currentUser
|
||||||
: store.state.instance.redirectRootNoLogin) || '/main/all'
|
? store.state.instance.redirectRootLogin
|
||||||
}
|
: 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,
|
||||||
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
showInstanceSpecificPanel () {
|
return this.$store.state.instance.showFeaturesPanel
|
||||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
},
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -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,25 +25,25 @@ const AccountActions = {
|
||||||
Popover,
|
Popover,
|
||||||
UserListMenu,
|
UserListMenu,
|
||||||
ConfirmModal,
|
ConfirmModal,
|
||||||
UserTimedFilterModal
|
UserTimedFilterModal,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showConfirmRemoveUserFromFollowers () {
|
showConfirmRemoveUserFromFollowers() {
|
||||||
this.showingConfirmRemoveFollower = true
|
this.showingConfirmRemoveFollower = true
|
||||||
},
|
},
|
||||||
hideConfirmRemoveUserFromFollowers () {
|
hideConfirmRemoveUserFromFollowers() {
|
||||||
this.showingConfirmRemoveFollower = false
|
this.showingConfirmRemoveFollower = false
|
||||||
},
|
},
|
||||||
hideConfirmBlock () {
|
hideConfirmBlock() {
|
||||||
this.showingConfirmBlock = false
|
this.showingConfirmBlock = false
|
||||||
},
|
},
|
||||||
showRepeats () {
|
showRepeats() {
|
||||||
this.$store.dispatch('showReblogs', this.user.id)
|
this.$store.dispatch('showReblogs', this.user.id)
|
||||||
},
|
},
|
||||||
hideRepeats () {
|
hideRepeats() {
|
||||||
this.$store.dispatch('hideReblogs', this.user.id)
|
this.$store.dispatch('hideReblogs', this.user.id)
|
||||||
},
|
},
|
||||||
blockUser () {
|
blockUser() {
|
||||||
if (this.$refs.timedBlockDialog) {
|
if (this.$refs.timedBlockDialog) {
|
||||||
this.$refs.timedBlockDialog.optionallyPrompt()
|
this.$refs.timedBlockDialog.optionallyPrompt()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -58,46 +54,50 @@ const AccountActions = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
doBlockUser () {
|
doBlockUser() {
|
||||||
this.$store.dispatch('blockUser', { id: this.user.id })
|
this.$store.dispatch('blockUser', { id: this.user.id })
|
||||||
this.hideConfirmBlock()
|
this.hideConfirmBlock()
|
||||||
},
|
},
|
||||||
unblockUser () {
|
unblockUser() {
|
||||||
this.$store.dispatch('unblockUser', this.user.id)
|
this.$store.dispatch('unblockUser', this.user.id)
|
||||||
},
|
},
|
||||||
removeUserFromFollowers () {
|
removeUserFromFollowers() {
|
||||||
if (!this.shouldConfirmRemoveUserFromFollowers) {
|
if (!this.shouldConfirmRemoveUserFromFollowers) {
|
||||||
this.doRemoveUserFromFollowers()
|
this.doRemoveUserFromFollowers()
|
||||||
} else {
|
} else {
|
||||||
this.showConfirmRemoveUserFromFollowers()
|
this.showConfirmRemoveUserFromFollowers()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
doRemoveUserFromFollowers () {
|
doRemoveUserFromFollowers() {
|
||||||
this.$store.dispatch('removeUserFromFollowers', this.user.id)
|
this.$store.dispatch('removeUserFromFollowers', this.user.id)
|
||||||
this.hideConfirmRemoveUserFromFollowers()
|
this.hideConfirmRemoveUserFromFollowers()
|
||||||
},
|
},
|
||||||
reportUser () {
|
reportUser() {
|
||||||
useReportsStore().openUserReportingModal({ userId: this.user.id })
|
useReportsStore().openUserReportingModal({ userId: this.user.id })
|
||||||
},
|
},
|
||||||
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() {
|
||||||
return this.$store.getters.mergedConfig.modalOnBlock
|
return this.$store.getters.mergedConfig.modalOnBlock
|
||||||
},
|
},
|
||||||
shouldConfirmRemoveUserFromFollowers () {
|
shouldConfirmRemoveUserFromFollowers() {
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,109 +1,129 @@
|
||||||
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 {
|
||||||
editing: false,
|
editing: false,
|
||||||
editedAnnouncement: {
|
editedAnnouncement: {
|
||||||
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
|
||||||
},
|
},
|
||||||
isRead () {
|
isRead() {
|
||||||
return this.announcement.read
|
return this.announcement.read
|
||||||
},
|
},
|
||||||
publishedAt () {
|
publishedAt() {
|
||||||
const time = this.announcement.published_at
|
const time = this.announcement.published_at
|
||||||
if (!time) {
|
if (!time) {
|
||||||
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
|
||||||
if (!time) {
|
if (!time) {
|
||||||
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
|
||||||
if (!time) {
|
if (!time) {
|
||||||
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() {
|
||||||
return useAnnouncementsStore().deleteAnnouncement(this.announcement.id)
|
return useAnnouncementsStore().deleteAnnouncement(this.announcement.id)
|
||||||
},
|
},
|
||||||
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
|
||||||
this.editedAnnouncement.startsAt = this.announcement.starts_at
|
this.editedAnnouncement.startsAt = this.announcement.starts_at
|
||||||
this.editedAnnouncement.endsAt = this.announcement.ends_at
|
this.editedAnnouncement.endsAt = this.announcement.ends_at
|
||||||
this.editedAnnouncement.allDay = this.announcement.all_day
|
this.editedAnnouncement.allDay = this.announcement.all_day
|
||||||
this.editing = true
|
this.editing = true
|
||||||
},
|
},
|
||||||
submitEdit () {
|
submitEdit() {
|
||||||
useAnnouncementsStore().editAnnouncement({
|
useAnnouncementsStore()
|
||||||
id: this.announcement.id,
|
.editAnnouncement({
|
||||||
...this.editedAnnouncement
|
id: this.announcement.id,
|
||||||
})
|
...this.editedAnnouncement,
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.editing = false
|
this.editing = false
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
this.editError = error.error
|
this.editError = error.error
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
cancelEdit () {
|
cancelEdit() {
|
||||||
this.editing = false
|
this.editing = false
|
||||||
},
|
},
|
||||||
clearError () {
|
clearError() {
|
||||||
this.editError = undefined
|
this.editError = undefined
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Announcement
|
export default Announcement
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,66 @@
|
||||||
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 {
|
||||||
newAnnouncement: {
|
newAnnouncement: {
|
||||||
content: '',
|
content: '',
|
||||||
startsAt: undefined,
|
startsAt: undefined,
|
||||||
endsAt: undefined,
|
endsAt: undefined,
|
||||||
allDay: false
|
allDay: false,
|
||||||
},
|
},
|
||||||
posting: false,
|
posting: false,
|
||||||
error: undefined
|
error: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted() {
|
||||||
useAnnouncementsStore().fetchAnnouncements()
|
useAnnouncementsStore().fetchAnnouncements()
|
||||||
},
|
},
|
||||||
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(() => {
|
||||||
this.posting = false
|
this.posting = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
clearError () {
|
clearError() {
|
||||||
this.error = undefined
|
this.error = undefined
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AnnouncementsPage
|
export default AnnouncementsPage
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,10 @@
|
||||||
export default {
|
export default {
|
||||||
emits: ['resetAsyncComponent'],
|
emits: ['resetAsyncComponent'],
|
||||||
methods: {
|
methods: {
|
||||||
retry () {
|
retry() {
|
||||||
this.$emit('resetAsyncComponent')
|
this.$emit('resetAsyncComponent')
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,72 +48,74 @@ const Attachment = {
|
||||||
'remove',
|
'remove',
|
||||||
'shiftUp',
|
'shiftUp',
|
||||||
'shiftDn',
|
'shiftDn',
|
||||||
'edit'
|
'edit',
|
||||||
],
|
],
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
localDescription: this.description || this.attachment.description,
|
localDescription: this.description || this.attachment.description,
|
||||||
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
|
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
|
||||||
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() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'-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() {
|
||||||
return this.size === 'hide'
|
return this.size === 'hide'
|
||||||
},
|
},
|
||||||
useContainFit () {
|
useContainFit() {
|
||||||
return this.$store.getters.mergedConfig.useContainFit
|
return this.$store.getters.mergedConfig.useContainFit
|
||||||
},
|
},
|
||||||
placeholderName () {
|
placeholderName() {
|
||||||
if (this.attachment.description === '' || !this.attachment.description) {
|
if (this.attachment.description === '' || !this.attachment.description) {
|
||||||
return this.type.toUpperCase()
|
return this.type.toUpperCase()
|
||||||
}
|
}
|
||||||
return this.attachment.description
|
return this.attachment.description
|
||||||
},
|
},
|
||||||
placeholderIconClass () {
|
placeholderIconClass() {
|
||||||
if (this.type === 'image') return 'image'
|
if (this.type === 'image') return 'image'
|
||||||
if (this.type === 'video') return 'video'
|
if (this.type === 'video') return 'video'
|
||||||
if (this.type === 'audio') return 'music'
|
if (this.type === 'audio') return 'music'
|
||||||
return 'file'
|
return 'file'
|
||||||
},
|
},
|
||||||
referrerpolicy () {
|
referrerpolicy() {
|
||||||
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
|
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
|
||||||
},
|
},
|
||||||
type () {
|
type() {
|
||||||
return fileTypeService.fileType(this.attachment.mimetype)
|
return fileTypeService.fileType(this.attachment.mimetype)
|
||||||
},
|
},
|
||||||
hidden () {
|
hidden() {
|
||||||
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 = []
|
||||||
switch (this.size) {
|
switch (this.size) {
|
||||||
case 'hide':
|
case 'hide':
|
||||||
|
|
@ -126,26 +130,26 @@ const Attachment = {
|
||||||
}
|
}
|
||||||
return modalTypes.includes(this.type)
|
return modalTypes.includes(this.type)
|
||||||
},
|
},
|
||||||
videoTag () {
|
videoTag() {
|
||||||
return this.useModal ? 'button' : 'span'
|
return this.useModal ? 'button' : 'span'
|
||||||
},
|
},
|
||||||
...mapGetters(['mergedConfig'])
|
...mapGetters(['mergedConfig']),
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'attachment.description' (newVal) {
|
'attachment.description'(newVal) {
|
||||||
this.localDescription = newVal
|
this.localDescription = newVal
|
||||||
},
|
},
|
||||||
localDescription (newVal) {
|
localDescription(newVal) {
|
||||||
this.onEdit(newVal)
|
this.onEdit(newVal)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
linkClicked ({ target }) {
|
linkClicked({ target }) {
|
||||||
if (target.tagName === 'A') {
|
if (target.tagName === 'A') {
|
||||||
window.open(target.href, '_blank')
|
window.open(target.href, '_blank')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openModal () {
|
openModal() {
|
||||||
if (this.useModal) {
|
if (this.useModal) {
|
||||||
this.$emit('setMedia')
|
this.$emit('setMedia')
|
||||||
useMediaViewerStore().setCurrentMedia(this.attachment)
|
useMediaViewerStore().setCurrentMedia(this.attachment)
|
||||||
|
|
@ -153,34 +157,35 @@ const Attachment = {
|
||||||
window.open(this.attachment.url)
|
window.open(this.attachment.url)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openModalForce () {
|
openModalForce() {
|
||||||
this.$emit('setMedia')
|
this.$emit('setMedia')
|
||||||
useMediaViewerStore().setCurrentMedia(this.attachment)
|
useMediaViewerStore().setCurrentMedia(this.attachment)
|
||||||
},
|
},
|
||||||
onEdit (event) {
|
onEdit(event) {
|
||||||
this.edit && this.edit(this.attachment, event)
|
this.edit && this.edit(this.attachment, event)
|
||||||
},
|
},
|
||||||
onRemove () {
|
onRemove() {
|
||||||
this.remove && this.remove(this.attachment)
|
this.remove && this.remove(this.attachment)
|
||||||
},
|
},
|
||||||
onShiftUp () {
|
onShiftUp() {
|
||||||
this.shiftUp && this.shiftUp(this.attachment)
|
this.shiftUp && this.shiftUp(this.attachment)
|
||||||
},
|
},
|
||||||
onShiftDn () {
|
onShiftDn() {
|
||||||
this.shiftDn && this.shiftDn(this.attachment)
|
this.shiftDn && this.shiftDn(this.attachment)
|
||||||
},
|
},
|
||||||
stopFlash () {
|
stopFlash() {
|
||||||
this.$refs.flash.closePlayer()
|
this.$refs.flash.closePlayer()
|
||||||
},
|
},
|
||||||
setFlashLoaded (event) {
|
setFlashLoaded(event) {
|
||||||
this.flashLoaded = event
|
this.flashLoaded = event
|
||||||
},
|
},
|
||||||
toggleDescription () {
|
toggleDescription() {
|
||||||
this.showDescription = !this.showDescription
|
this.showDescription = !this.showDescription
|
||||||
},
|
},
|
||||||
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)
|
||||||
|
|
@ -201,12 +206,12 @@ const Attachment = {
|
||||||
this.showHidden = !this.showHidden
|
this.showHidden = !this.showHidden
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onImageLoad (image) {
|
onImageLoad(image) {
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,33 @@
|
||||||
|
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',
|
||||||
render () {
|
render() {
|
||||||
return h(resolveComponent(this.authForm))
|
return h(resolveComponent(this.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
|
||||||
|
|
|
||||||
|
|
@ -2,51 +2,55 @@ 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) {
|
||||||
clearTimeout(this.timeout)
|
clearTimeout(this.timeout)
|
||||||
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)
|
||||||
},
|
},
|
||||||
onInputClick () {
|
onInputClick() {
|
||||||
this.resultsVisible = true
|
this.resultsVisible = true
|
||||||
},
|
},
|
||||||
onClickOutside () {
|
onClickOutside() {
|
||||||
this.resultsVisible = false
|
this.resultsVisible = false
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -5,42 +5,44 @@ import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||||
const BlockCard = {
|
const BlockCard = {
|
||||||
props: ['userId'],
|
props: ['userId'],
|
||||||
computed: {
|
computed: {
|
||||||
user () {
|
user() {
|
||||||
return this.$store.getters.findUser(this.userId)
|
return this.$store.getters.findUser(this.userId)
|
||||||
},
|
},
|
||||||
relationship () {
|
relationship() {
|
||||||
return this.$store.getters.relationship(this.userId)
|
return this.$store.getters.relationship(this.userId)
|
||||||
},
|
},
|
||||||
blocked () {
|
blocked() {
|
||||||
return this.relationship.blocking
|
return this.relationship.blocking
|
||||||
},
|
},
|
||||||
blockExpiryAvailable () {
|
blockExpiryAvailable() {
|
||||||
return this.user.block_expires_at !== undefined
|
return this.user.block_expires_at !== undefined
|
||||||
},
|
},
|
||||||
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() {
|
||||||
this.$store.dispatch('unblockUser', this.user.id)
|
this.$store.dispatch('unblockUser', this.user.id)
|
||||||
},
|
},
|
||||||
blockUser () {
|
blockUser() {
|
||||||
if (this.blockExpirationSupported) {
|
if (this.blockExpirationSupported) {
|
||||||
this.$refs.timedBlockDialog.optionallyPrompt()
|
this.$refs.timedBlockDialog.optionallyPrompt()
|
||||||
} else {
|
} else {
|
||||||
this.$store.dispatch('blockUser', { id: this.user.id })
|
this.$store.dispatch('blockUser', { id: this.user.id })
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BlockCard
|
export default BlockCard
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
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() {
|
||||||
return {
|
return {
|
||||||
name: '',
|
name: '',
|
||||||
nameDraft: '',
|
nameDraft: '',
|
||||||
|
|
@ -13,54 +13,59 @@ 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
|
||||||
this.emojiDraft = this.emoji = folder.emoji
|
this.emojiDraft = this.emoji = folder.emoji
|
||||||
this.emojiUrlDraft = this.emojiUrl = folder.emoji_url
|
this.emojiUrlDraft = this.emojiUrl = folder.emoji_url
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
id () {
|
id() {
|
||||||
return this.$route.params.id
|
return this.$route.params.id
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
selectEmoji (event) {
|
selectEmoji(event) {
|
||||||
this.emojiDraft = event.insertion
|
this.emojiDraft = event.insertion
|
||||||
this.emojiUrlDraft = event.insertionUrl
|
this.emojiUrlDraft = event.insertionUrl
|
||||||
},
|
},
|
||||||
showEmojiPicker () {
|
showEmojiPicker() {
|
||||||
if (!this.emojiPickerExpanded) {
|
if (!this.emojiPickerExpanded) {
|
||||||
this.$refs.picker.showPicker()
|
this.$refs.picker.showPicker()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onShowPicker () {
|
onShowPicker() {
|
||||||
this.emojiPickerExpanded = true
|
this.emojiPickerExpanded = true
|
||||||
},
|
},
|
||||||
onClosePicker () {
|
onClosePicker() {
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,28 @@
|
||||||
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() {
|
||||||
this.isNew = false
|
this.isNew = false
|
||||||
},
|
},
|
||||||
newFolder () {
|
newFolder() {
|
||||||
this.isNew = true
|
this.isNew = true
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BookmarkFolders
|
export default BookmarkFolders
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,38 @@
|
||||||
import Timeline from '../timeline/timeline.vue'
|
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() {
|
||||||
return this.$route.params.id
|
return this.$route.params.id
|
||||||
},
|
},
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ export default {
|
||||||
{
|
{
|
||||||
directives: {
|
directives: {
|
||||||
textColor: '$mod(--parent 10)',
|
textColor: '$mod(--parent 10)',
|
||||||
textAuto: 'no-auto'
|
textAuto: 'no-auto',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,78 +32,95 @@ const Chat = {
|
||||||
components: {
|
components: {
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
ChatTitle,
|
ChatTitle,
|
||||||
PostStatusForm
|
PostStatusForm,
|
||||||
},
|
},
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
jumpToBottomButtonVisible: false,
|
jumpToBottomButtonVisible: false,
|
||||||
hoveredMessageChainId: undefined,
|
hoveredMessageChainId: undefined,
|
||||||
lastScrollPosition: {},
|
lastScrollPosition: {},
|
||||||
scrollableContainerHeight: '100%',
|
scrollableContainerHeight: '100%',
|
||||||
errorLoadingChat: false,
|
errorLoadingChat: false,
|
||||||
messageRetriers: {}
|
messageRetriers: {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created() {
|
||||||
this.startFetching()
|
this.startFetching()
|
||||||
window.addEventListener('resize', this.handleResize)
|
window.addEventListener('resize', this.handleResize)
|
||||||
},
|
},
|
||||||
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(() => {
|
||||||
this.handleResize()
|
this.handleResize()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
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: {
|
||||||
recipient () {
|
recipient() {
|
||||||
return this.currentChat && this.currentChat.account
|
return this.currentChat && this.currentChat.account
|
||||||
},
|
},
|
||||||
recipientId () {
|
recipientId() {
|
||||||
return this.$route.params.recipient_id
|
return this.$route.params.recipient_id
|
||||||
},
|
},
|
||||||
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 ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
chatViewItems () {
|
chatViewItems() {
|
||||||
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() {
|
||||||
// We don't want to scroll to the bottom on a new message when the user is viewing older messages.
|
// We don't want to scroll to the bottom on a new message when the user is viewing older messages.
|
||||||
// Therefore we need to know whether the scroll position was at the bottom before the DOM update.
|
// Therefore we need to know whether the scroll position was at the bottom before the DOM update.
|
||||||
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
|
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
|
||||||
|
|
@ -115,23 +133,23 @@ const Chat = {
|
||||||
$route: function () {
|
$route: function () {
|
||||||
this.startFetching()
|
this.startFetching()
|
||||||
},
|
},
|
||||||
mastoUserSocketStatus (newValue) {
|
mastoUserSocketStatus(newValue) {
|
||||||
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
|
||||||
onMessageHover ({ isHovered, messageChainId }) {
|
onMessageHover({ isHovered, messageChainId }) {
|
||||||
this.hoveredMessageChainId = isHovered ? messageChainId : undefined
|
this.hoveredMessageChainId = isHovered ? messageChainId : undefined
|
||||||
},
|
},
|
||||||
onFilesDropped () {
|
onFilesDropped() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.handleResize()
|
this.handleResize()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
handleVisibilityChange () {
|
handleVisibilityChange() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (!document.hidden && this.bottomedOut(BOTTOMED_OUT_OFFSET)) {
|
if (!document.hidden && this.bottomedOut(BOTTOMED_OUT_OFFSET)) {
|
||||||
this.scrollDown({ forceRead: true })
|
this.scrollDown({ forceRead: true })
|
||||||
|
|
@ -139,7 +157,7 @@ const Chat = {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
// "Sticks" scroll to bottom instead of top, helps with OSK resizing the viewport
|
// "Sticks" scroll to bottom instead of top, helps with OSK resizing the viewport
|
||||||
handleResize (opts = {}) {
|
handleResize(opts = {}) {
|
||||||
const { delayed = false } = opts
|
const { delayed = false } = opts
|
||||||
|
|
||||||
if (delayed) {
|
if (delayed) {
|
||||||
|
|
@ -160,40 +178,56 @@ const Chat = {
|
||||||
this.lastScrollPosition = getScrollPosition()
|
this.lastScrollPosition = getScrollPosition()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
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) {
|
||||||
return isBottomedOut(offset)
|
return isBottomedOut(offset)
|
||||||
},
|
},
|
||||||
reachedTop () {
|
reachedTop() {
|
||||||
return window.scrollY <= 0
|
return window.scrollY <= 0
|
||||||
},
|
},
|
||||||
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 })
|
||||||
|
|
@ -213,22 +247,27 @@ const Chat = {
|
||||||
this.jumpToBottomButtonVisible = true
|
this.jumpToBottomButtonVisible = true
|
||||||
}
|
}
|
||||||
}, 200),
|
}, 200),
|
||||||
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,28 +275,34 @@ const Chat = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const positionBeforeUpdate = getScrollPosition()
|
const positionBeforeUpdate = getScrollPosition()
|
||||||
this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => {
|
this.$store
|
||||||
this.$nextTick(() => {
|
.dispatch('addChatMessages', { chatId, messages })
|
||||||
if (fetchOlderMessages) {
|
.then(() => {
|
||||||
this.handleScrollUp(positionBeforeUpdate)
|
this.$nextTick(() => {
|
||||||
}
|
if (fetchOlderMessages) {
|
||||||
|
this.handleScrollUp(positionBeforeUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
// In vertical screens, the first batch of fetched messages may not always take the
|
// In vertical screens, the first batch of fetched messages may not always take the
|
||||||
// full height of the scrollable container.
|
// full height of the scrollable container.
|
||||||
// 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async startFetching () {
|
async startFetching() {
|
||||||
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
|
||||||
|
|
@ -271,13 +316,14 @@ const Chat = {
|
||||||
this.doStartFetching()
|
this.doStartFetching()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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 })
|
||||||
},
|
},
|
||||||
handleAttachmentPosting () {
|
handleAttachmentPosting() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.handleResize()
|
this.handleResize()
|
||||||
// When the posting form size changes because of a media attachment, we need an extra resize
|
// When the posting form size changes because of a media attachment, we need an extra resize
|
||||||
|
|
@ -285,11 +331,11 @@ const Chat = {
|
||||||
this.scrollDown({ forceRead: true })
|
this.scrollDown({ forceRead: true })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
sendMessage ({ status, media, idempotencyKey }) {
|
sendMessage({ status, media, idempotencyKey }) {
|
||||||
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,52 +347,72 @@ 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
|
||||||
chatId: this.currentChat.id,
|
.dispatch('addChatMessages', {
|
||||||
messages: [fakeMessage]
|
chatId: this.currentChat.id,
|
||||||
}).then(() => {
|
messages: [fakeMessage],
|
||||||
this.handleAttachmentPosting()
|
})
|
||||||
})
|
.then(() => {
|
||||||
|
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 {}
|
||||||
})
|
})
|
||||||
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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,31 +8,31 @@ 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() {
|
||||||
this.$store.dispatch('fetchChats', { latest: true })
|
this.$store.dispatch('fetchChats', { latest: true })
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
cancelNewChat () {
|
cancelNewChat() {
|
||||||
this.isNew = false
|
this.isNew = false
|
||||||
this.$store.dispatch('fetchChats', { latest: true })
|
this.$store.dispatch('fetchChats', { latest: true })
|
||||||
},
|
},
|
||||||
newChat () {
|
newChat() {
|
||||||
this.isNew = true
|
this.isNew = true
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChatList
|
export default ChatList
|
||||||
|
|
|
||||||
|
|
@ -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')) {
|
||||||
|
|
@ -36,34 +39,36 @@ const ChatListItem = {
|
||||||
return this.$t('file_type.file')
|
return this.$t('file_type.file')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
messageForStatusContent () {
|
messageForStatusContent() {
|
||||||
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) {
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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,73 +34,82 @@ 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
|
||||||
},
|
},
|
||||||
message () {
|
message() {
|
||||||
return this.chatViewItem.data
|
return this.chatViewItem.data
|
||||||
},
|
},
|
||||||
isMessage () {
|
isMessage() {
|
||||||
return this.chatViewItem.type === 'message'
|
return this.chatViewItem.type === 'message'
|
||||||
},
|
},
|
||||||
messageForStatusContent () {
|
messageForStatusContent() {
|
||||||
return {
|
return {
|
||||||
summary: '',
|
summary: '',
|
||||||
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) {
|
||||||
return {}
|
return {}
|
||||||
} else {
|
} else {
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,16 +11,19 @@ export default {
|
||||||
name: 'Timeago',
|
name: 'Timeago',
|
||||||
props: ['date'],
|
props: ['date'],
|
||||||
computed: {
|
computed: {
|
||||||
displayDate () {
|
displayDate() {
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
today.setHours(0, 0, 0, 0)
|
today.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
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>
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,35 @@
|
||||||
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) {
|
||||||
return this.users
|
return this.users
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -41,29 +37,29 @@ 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() {
|
||||||
this.$emit('cancel')
|
this.$emit('cancel')
|
||||||
},
|
},
|
||||||
goToChat (user) {
|
goToChat(user) {
|
||||||
this.$router.push({ name: 'chat', params: { recipient_id: user.id } })
|
this.$router.push({ name: 'chat', params: { recipient_id: user.id } })
|
||||||
},
|
},
|
||||||
onInput () {
|
onInput() {
|
||||||
this.search(this.query)
|
this.search(this.query)
|
||||||
},
|
},
|
||||||
addUser (user) {
|
addUser(user) {
|
||||||
this.selectedUserIds.push(user.id)
|
this.selectedUserIds.push(user.id)
|
||||||
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) {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
return
|
return
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 : ''
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,95 +64,95 @@
|
||||||
</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: {
|
||||||
present () {
|
present() {
|
||||||
return typeof this.modelValue !== 'undefined'
|
return typeof this.modelValue !== 'undefined'
|
||||||
},
|
},
|
||||||
validColor () {
|
validColor() {
|
||||||
return hex2rgb(this.modelValue || this.fallback)
|
return hex2rgb(this.modelValue || this.fallback)
|
||||||
},
|
},
|
||||||
transparentColor () {
|
transparentColor() {
|
||||||
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>
|
||||||
|
|
|
||||||
|
|
@ -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,41 +19,41 @@ export default {
|
||||||
'previewCss',
|
'previewCss',
|
||||||
'disabled',
|
'disabled',
|
||||||
'invalid',
|
'invalid',
|
||||||
'noColorControl'
|
'noColorControl',
|
||||||
],
|
],
|
||||||
emits: ['update:shadow'],
|
emits: ['update:shadow'],
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
colorOverride: undefined,
|
colorOverride: undefined,
|
||||||
lightGrid: false,
|
lightGrid: false,
|
||||||
zoom: 100,
|
zoom: 100,
|
||||||
randomSeed: genRandomSeed()
|
randomSeed: genRandomSeed(),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted() {
|
||||||
this.update()
|
this.update()
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hideControls () {
|
hideControls() {
|
||||||
return typeof this.shadow === 'string'
|
return typeof this.shadow === 'string'
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
previewCss () {
|
previewCss() {
|
||||||
this.update()
|
this.update()
|
||||||
},
|
},
|
||||||
previewStyle () {
|
previewStyle() {
|
||||||
this.update()
|
this.update()
|
||||||
},
|
},
|
||||||
zoom () {
|
zoom() {
|
||||||
this.update()
|
this.update()
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateProperty (axis, value) {
|
updateProperty(axis, value) {
|
||||||
this.$emit('update:shadow', { axis, value: Number(value) })
|
this.$emit('update:shadow', { axis, value: Number(value) })
|
||||||
},
|
},
|
||||||
update () {
|
update() {
|
||||||
const sheet = createStyleSheet('style-component-preview', 90)
|
const sheet = createStyleSheet('style-component-preview', 90)
|
||||||
|
|
||||||
sheet.clear()
|
sheet.clear()
|
||||||
|
|
@ -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()
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,64 +1,67 @@
|
||||||
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() {
|
||||||
return this.user.fqn.split('@')[1]
|
return this.user.fqn.split('@')[1]
|
||||||
},
|
},
|
||||||
keypath () {
|
keypath() {
|
||||||
if (this.type === 'domain') {
|
if (this.type === 'domain') {
|
||||||
return 'status.mute_domain_confirm'
|
return 'status.mute_domain_confirm'
|
||||||
} else if (this.type === 'conversation') {
|
} else if (this.type === 'conversation') {
|
||||||
return 'status.mute_conversation_confirm'
|
return 'status.mute_conversation_confirm'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
conversationIsMuted () {
|
conversationIsMuted() {
|
||||||
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() {
|
||||||
if (this.shouldConfirm) {
|
if (this.shouldConfirm) {
|
||||||
this.show()
|
this.show()
|
||||||
} else {
|
} else {
|
||||||
this.doMute()
|
this.doMute()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
show () {
|
show() {
|
||||||
this.showing = true
|
this.showing = true
|
||||||
this.$emit('show')
|
this.$emit('show')
|
||||||
},
|
},
|
||||||
hide () {
|
hide() {
|
||||||
this.showing = false
|
this.showing = false
|
||||||
this.$emit('hide')
|
this.$emit('hide')
|
||||||
},
|
},
|
||||||
doMute () {
|
doMute() {
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
case 'domain': {
|
case 'domain': {
|
||||||
if (!this.domainIsMuted) {
|
if (!this.domainIsMuted) {
|
||||||
|
|
@ -79,6 +82,6 @@ export default {
|
||||||
}
|
}
|
||||||
this.$emit('muted')
|
this.$emit('muted')
|
||||||
this.hide()
|
this.hide()
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,23 +40,25 @@ 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 = {
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
highlight: null,
|
highlight: null,
|
||||||
expanded: false,
|
expanded: false,
|
||||||
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
|
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
|
||||||
statusContentPropertiesObject: {},
|
statusContentPropertiesObject: {},
|
||||||
inlineDivePosition: null,
|
inlineDivePosition: null,
|
||||||
loadStatusError: null
|
loadStatusError: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
|
|
@ -69,76 +68,80 @@ const conversation = {
|
||||||
'pinnedStatusIdsObject',
|
'pinnedStatusIdsObject',
|
||||||
'inProfile',
|
'inProfile',
|
||||||
'profileUserId',
|
'profileUserId',
|
||||||
'virtualHidden'
|
'virtualHidden',
|
||||||
],
|
],
|
||||||
created () {
|
created() {
|
||||||
if (this.isPage) {
|
if (this.isPage) {
|
||||||
this.fetchConversation()
|
this.fetchConversation()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
maxDepthToShowByDefault () {
|
maxDepthToShowByDefault() {
|
||||||
// maxDepthInThread = max number of depths that is *visible*
|
// maxDepthInThread = max number of depths that is *visible*
|
||||||
// since our depth starts with 0 and "showing" means "showing children"
|
// since our depth starts with 0 and "showing" means "showing children"
|
||||||
// there is a -2 here
|
// there is a -2 here
|
||||||
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
|
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
|
||||||
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
|
||||||
},
|
},
|
||||||
isTreeView () {
|
isTreeView() {
|
||||||
return !this.isLinearView
|
return !this.isLinearView
|
||||||
},
|
},
|
||||||
treeViewIsSimple () {
|
treeViewIsSimple() {
|
||||||
return !this.$store.getters.mergedConfig.conversationTreeAdvanced
|
return !this.$store.getters.mergedConfig.conversationTreeAdvanced
|
||||||
},
|
},
|
||||||
isLinearView () {
|
isLinearView() {
|
||||||
return this.displayStyle === 'linear'
|
return this.displayStyle === 'linear'
|
||||||
},
|
},
|
||||||
shouldFadeAncestors () {
|
shouldFadeAncestors() {
|
||||||
return this.$store.getters.mergedConfig.conversationTreeFadeAncestors
|
return this.$store.getters.mergedConfig.conversationTreeFadeAncestors
|
||||||
},
|
},
|
||||||
otherRepliesButtonPosition () {
|
otherRepliesButtonPosition() {
|
||||||
return this.$store.getters.mergedConfig.conversationOtherRepliesButton
|
return this.$store.getters.mergedConfig.conversationOtherRepliesButton
|
||||||
},
|
},
|
||||||
showOtherRepliesButtonBelowStatus () {
|
showOtherRepliesButtonBelowStatus() {
|
||||||
return this.otherRepliesButtonPosition === 'below'
|
return this.otherRepliesButtonPosition === 'below'
|
||||||
},
|
},
|
||||||
showOtherRepliesButtonInsideStatus () {
|
showOtherRepliesButtonInsideStatus() {
|
||||||
return this.otherRepliesButtonPosition === 'inside'
|
return this.otherRepliesButtonPosition === 'inside'
|
||||||
},
|
},
|
||||||
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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hideStatus () {
|
hideStatus() {
|
||||||
return this.virtualHidden && this.suspendable
|
return this.virtualHidden && this.suspendable
|
||||||
},
|
},
|
||||||
status () {
|
status() {
|
||||||
return this.$store.state.statuses.allStatusesObject[this.statusId]
|
return this.$store.state.statuses.allStatusesObject[this.statusId]
|
||||||
},
|
},
|
||||||
originalStatusId () {
|
originalStatusId() {
|
||||||
if (this.status.retweeted_status) {
|
if (this.status.retweeted_status) {
|
||||||
return this.status.retweeted_status.id
|
return this.status.retweeted_status.id
|
||||||
} else {
|
} else {
|
||||||
return this.statusId
|
return this.statusId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
conversationId () {
|
conversationId() {
|
||||||
return this.getConversationId(this.statusId)
|
return this.getConversationId(this.statusId)
|
||||||
},
|
},
|
||||||
conversation () {
|
conversation() {
|
||||||
if (!this.status) {
|
if (!this.status) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -155,144 +160,188 @@ const conversation = {
|
||||||
|
|
||||||
return sortAndFilterConversation(conversation, this.status)
|
return sortAndFilterConversation(conversation, this.status)
|
||||||
},
|
},
|
||||||
statusMap () {
|
statusMap() {
|
||||||
return this.conversation.reduce((res, s) => {
|
return this.conversation.reduce((res, s) => {
|
||||||
res[s.id] = s
|
res[s.id] = s
|
||||||
return res
|
return res
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
threadTree () {
|
threadTree() {
|
||||||
const reverseLookupTable = this.conversation.reduce((table, status, index) => {
|
const reverseLookupTable = this.conversation.reduce(
|
||||||
table[status.id] = index
|
(table, status, index) => {
|
||||||
return table
|
table[status.id] = index
|
||||||
}, {})
|
return table
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
const threads = this.conversation.reduce((a, cur) => {
|
const threads = this.conversation.reduce(
|
||||||
const id = cur.id
|
(a, cur) => {
|
||||||
a.forest[id] = this.getReplies(id)
|
const id = cur.id
|
||||||
.map(s => s.id)
|
a.forest[id] = this.getReplies(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 = {}) =>
|
||||||
if (processed[id]) {
|
topLevel
|
||||||
return []
|
.map((id) => {
|
||||||
}
|
if (processed[id]) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
processed[id] = true
|
processed[id] = true
|
||||||
return [{
|
return [
|
||||||
status: this.conversation[reverseLookupTable[id]],
|
{
|
||||||
id,
|
status: this.conversation[reverseLookupTable[id]],
|
||||||
depth
|
id,
|
||||||
}, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), [])
|
depth,
|
||||||
}).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
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
totalReplyCount () {
|
totalReplyCount() {
|
||||||
const sizes = {}
|
const sizes = {}
|
||||||
const subTreeSizeFor = (id) => {
|
const subTreeSizeFor = (id) => {
|
||||||
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
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
totalReplyDepth () {
|
totalReplyDepth() {
|
||||||
const depths = {}
|
const depths = {}
|
||||||
const subTreeDepthFor = (id) => {
|
const subTreeDepthFor = (id) => {
|
||||||
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
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
depths () {
|
depths() {
|
||||||
return this.threadTree.reduce((a, k) => {
|
return this.threadTree.reduce((a, k) => {
|
||||||
a[k.id] = k.depth
|
a[k.id] = k.depth
|
||||||
return a
|
return a
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
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() {
|
||||||
return this.topLevel.length - 1
|
return this.topLevel.length - 1
|
||||||
},
|
},
|
||||||
showingTopLevel () {
|
showingTopLevel() {
|
||||||
if (this.canDive && this.diveRoot) {
|
if (this.canDive && this.diveRoot) {
|
||||||
return [this.statusMap[this.diveRoot]]
|
return [this.statusMap[this.diveRoot]]
|
||||||
}
|
}
|
||||||
return this.topLevel
|
return this.topLevel
|
||||||
},
|
},
|
||||||
diveRoot () {
|
diveRoot() {
|
||||||
const statusId = this.inlineDivePosition || this.statusId
|
const statusId = this.inlineDivePosition || this.statusId
|
||||||
const isTopLevel = !this.parentOf(statusId)
|
const isTopLevel = !this.parentOf(statusId)
|
||||||
return isTopLevel ? null : statusId
|
return isTopLevel ? null : statusId
|
||||||
},
|
},
|
||||||
diveDepth () {
|
diveDepth() {
|
||||||
return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
|
return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
|
||||||
},
|
},
|
||||||
diveMode () {
|
diveMode() {
|
||||||
return this.canDive && !!this.diveRoot
|
return this.canDive && !!this.diveRoot
|
||||||
},
|
},
|
||||||
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(
|
||||||
if (irid) {
|
this.conversation,
|
||||||
result[irid] = result[irid] || []
|
(result, { id, in_reply_to_status_id: irid }) => {
|
||||||
result[irid].push({
|
if (irid) {
|
||||||
name: `#${i}`,
|
result[irid] = result[irid] || []
|
||||||
id
|
result[irid].push({
|
||||||
})
|
name: `#${i}`,
|
||||||
}
|
id,
|
||||||
i++
|
})
|
||||||
return result
|
}
|
||||||
}, {})
|
i++
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
)
|
||||||
},
|
},
|
||||||
isExpanded () {
|
isExpanded() {
|
||||||
return !!(this.expanded || this.isPage)
|
return !!(this.expanded || this.isPage)
|
||||||
},
|
},
|
||||||
hiddenStyle () {
|
hiddenStyle() {
|
||||||
const height = (this.status && this.status.virtualHeight) || '120px'
|
const height = (this.status && this.status.virtualHeight) || '120px'
|
||||||
return this.virtualHidden ? { height } : {}
|
return this.virtualHidden ? { height } : {}
|
||||||
},
|
},
|
||||||
threadDisplayStatus () {
|
threadDisplayStatus() {
|
||||||
return this.conversation.reduce((a, k) => {
|
return this.conversation.reduce((a, k) => {
|
||||||
const id = k.id
|
const id = k.id
|
||||||
const depth = this.depths[id]
|
const depth = this.depths[id]
|
||||||
|
|
@ -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'
|
||||||
|
|
@ -311,7 +360,7 @@ const conversation = {
|
||||||
return a
|
return a
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
statusContentProperties () {
|
statusContentProperties() {
|
||||||
return this.conversation.reduce((a, k) => {
|
return this.conversation.reduce((a, k) => {
|
||||||
const id = k.id
|
const id = k.id
|
||||||
const props = (() => {
|
const props = (() => {
|
||||||
|
|
@ -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
|
||||||
|
|
@ -336,54 +385,59 @@ const conversation = {
|
||||||
return a
|
return a
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
canDive () {
|
canDive() {
|
||||||
return this.isTreeView && this.isExpanded
|
return this.isTreeView && this.isExpanded
|
||||||
},
|
},
|
||||||
maybeHighlight () {
|
maybeHighlight() {
|
||||||
return this.isExpanded ? this.highlight : null
|
return this.isExpanded ? this.highlight : null
|
||||||
},
|
},
|
||||||
...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()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
expanded (value) {
|
expanded(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
this.fetchConversation()
|
this.fetchConversation()
|
||||||
} else {
|
} else {
|
||||||
this.resetDisplayState()
|
this.resetDisplayState()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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()
|
||||||
|
|
@ -401,16 +456,16 @@ 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] || []
|
||||||
},
|
},
|
||||||
getHighlight () {
|
getHighlight() {
|
||||||
return this.isExpanded ? this.highlight : null
|
return this.isExpanded ? this.highlight : null
|
||||||
},
|
},
|
||||||
setHighlight (id) {
|
setHighlight(id) {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
this.highlight = id
|
this.highlight = id
|
||||||
|
|
||||||
|
|
@ -421,44 +476,54 @@ const conversation = {
|
||||||
this.$store.dispatch('fetchFavsAndRepeats', id)
|
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||||
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
||||||
},
|
},
|
||||||
toggleExpanded () {
|
toggleExpanded() {
|
||||||
this.expanded = !this.expanded
|
this.expanded = !this.expanded
|
||||||
},
|
},
|
||||||
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) {
|
||||||
const curStatus = this.threadDisplayStatus[id]
|
const curStatus = this.threadDisplayStatus[id]
|
||||||
const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing'
|
const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing'
|
||||||
this.setThreadDisplay(id, nextStatus)
|
this.setThreadDisplay(id, nextStatus)
|
||||||
},
|
},
|
||||||
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')
|
||||||
},
|
},
|
||||||
setStatusContentProperty (id, name, value) {
|
setStatusContentProperty(id, name, value) {
|
||||||
this.statusContentPropertiesObject = {
|
this.statusContentPropertiesObject = {
|
||||||
...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
|
||||||
let parent = this.parentOf(cur)
|
let parent = this.parentOf(cur)
|
||||||
while (cur) {
|
while (cur) {
|
||||||
|
|
@ -472,18 +537,20 @@ const conversation = {
|
||||||
// nothing found, fall back to toplevel
|
// nothing found, fall back to toplevel
|
||||||
return this.topLevel[0] ? this.topLevel[0].id : undefined
|
return this.topLevel[0] ? this.topLevel[0].id : undefined
|
||||||
},
|
},
|
||||||
diveIntoStatus (id) {
|
diveIntoStatus(id) {
|
||||||
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() {
|
||||||
this.inlineDivePosition = null
|
this.inlineDivePosition = null
|
||||||
this.setHighlight(this.statusId)
|
this.setHighlight(this.statusId)
|
||||||
},
|
},
|
||||||
tryScrollTo (id) {
|
tryScrollTo(id) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -512,13 +579,13 @@ const conversation = {
|
||||||
this.setHighlight(id)
|
this.setHighlight(id)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
goToCurrent () {
|
goToCurrent() {
|
||||||
this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
|
this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
|
||||||
},
|
},
|
||||||
statusById (id) {
|
statusById(id) {
|
||||||
return this.statusMap[id]
|
return this.statusMap[id]
|
||||||
},
|
},
|
||||||
parentOf (id) {
|
parentOf(id) {
|
||||||
const status = this.statusById(id)
|
const status = this.statusById(id)
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return undefined
|
return undefined
|
||||||
|
|
@ -529,11 +596,11 @@ const conversation = {
|
||||||
}
|
}
|
||||||
return parentId
|
return parentId
|
||||||
},
|
},
|
||||||
parentOrSelf (id) {
|
parentOrSelf(id) {
|
||||||
return this.parentOf(id) || id
|
return this.parentOf(id) || id
|
||||||
},
|
},
|
||||||
// Ancestors of some status, from top to bottom
|
// Ancestors of some status, from top to bottom
|
||||||
ancestorsOf (id) {
|
ancestorsOf(id) {
|
||||||
const ancestors = []
|
const ancestors = []
|
||||||
let cur = this.parentOf(id)
|
let cur = this.parentOf(id)
|
||||||
while (cur) {
|
while (cur) {
|
||||||
|
|
@ -542,7 +609,7 @@ const conversation = {
|
||||||
}
|
}
|
||||||
return ancestors
|
return ancestors
|
||||||
},
|
},
|
||||||
topLevelAncestorOrSelfId (id) {
|
topLevelAncestorOrSelfId(id) {
|
||||||
let cur = id
|
let cur = id
|
||||||
let parent = this.parentOf(id)
|
let parent = this.parentOf(id)
|
||||||
while (parent) {
|
while (parent) {
|
||||||
|
|
@ -551,11 +618,11 @@ const conversation = {
|
||||||
}
|
}
|
||||||
return cur
|
return cur
|
||||||
},
|
},
|
||||||
resetDisplayState () {
|
resetDisplayState() {
|
||||||
this.undive()
|
this.undive()
|
||||||
this.threadDisplayStatusObject = {}
|
this.threadDisplayStatusObject = {}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default conversation
|
export default conversation
|
||||||
|
|
|
||||||
|
|
@ -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,91 +29,109 @@ 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() {
|
||||||
logoStyle () {
|
return this.supportsMask && this.$store.state.instance.logoMask
|
||||||
|
},
|
||||||
|
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`,
|
{
|
||||||
opacity: this.searchBarHidden ? 1 : 0
|
margin: `${this.$store.state.instance.logoMargin} 0`,
|
||||||
}, this.enableMask
|
opacity: this.searchBarHidden ? 1 : 0,
|
||||||
? {}
|
},
|
||||||
: {
|
this.enableMask
|
||||||
'background-color': this.enableMask ? '' : 'transparent'
|
? {}
|
||||||
})
|
: {
|
||||||
|
'background-color': this.enableMask ? '' : 'transparent',
|
||||||
|
},
|
||||||
|
)
|
||||||
},
|
},
|
||||||
logo () { return this.$store.state.instance.logo },
|
logo() {
|
||||||
sitename () { return this.$store.state.instance.name },
|
return this.$store.state.instance.logo
|
||||||
hideSitename () { return this.$store.state.instance.hideSitename },
|
},
|
||||||
logoLeft () { return this.$store.state.instance.logoLeft },
|
sitename() {
|
||||||
currentUser () { return this.$store.state.users.currentUser },
|
return this.$store.state.instance.name
|
||||||
privateMode () { return this.$store.state.instance.private },
|
},
|
||||||
shouldConfirmLogout () {
|
hideSitename() {
|
||||||
|
return this.$store.state.instance.hideSitename
|
||||||
|
},
|
||||||
|
logoLeft() {
|
||||||
|
return this.$store.state.instance.logoLeft
|
||||||
|
},
|
||||||
|
currentUser() {
|
||||||
|
return this.$store.state.users.currentUser
|
||||||
|
},
|
||||||
|
privateMode() {
|
||||||
|
return this.$store.state.instance.private
|
||||||
|
},
|
||||||
|
shouldConfirmLogout() {
|
||||||
return this.$store.getters.mergedConfig.modalOnLogout
|
return this.$store.getters.mergedConfig.modalOnLogout
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
scrollToTop () {
|
scrollToTop() {
|
||||||
window.scrollTo(0, 0)
|
window.scrollTo(0, 0)
|
||||||
},
|
},
|
||||||
showConfirmLogout () {
|
showConfirmLogout() {
|
||||||
this.showingConfirmLogout = true
|
this.showingConfirmLogout = true
|
||||||
},
|
},
|
||||||
hideConfirmLogout () {
|
hideConfirmLogout() {
|
||||||
this.showingConfirmLogout = false
|
this.showingConfirmLogout = false
|
||||||
},
|
},
|
||||||
logout () {
|
logout() {
|
||||||
if (!this.shouldConfirmLogout) {
|
if (!this.shouldConfirmLogout) {
|
||||||
this.doLogout()
|
this.doLogout()
|
||||||
} else {
|
} else {
|
||||||
this.showConfirmLogout()
|
this.showConfirmLogout()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
doLogout () {
|
doLogout() {
|
||||||
this.$router.replace('/main/public')
|
this.$router.replace('/main/public')
|
||||||
this.$store.dispatch('logout')
|
this.$store.dispatch('logout')
|
||||||
this.hideConfirmLogout()
|
this.hideConfirmLogout()
|
||||||
},
|
},
|
||||||
onSearchBarToggled (hidden) {
|
onSearchBarToggled(hidden) {
|
||||||
this.searchBarHidden = hidden
|
this.searchBarHidden = hidden
|
||||||
},
|
},
|
||||||
openSettingsModal () {
|
openSettingsModal() {
|
||||||
useInterfaceStore().openSettingsModal('user')
|
useInterfaceStore().openSettingsModal('user')
|
||||||
},
|
},
|
||||||
openAdminModal () {
|
openAdminModal() {
|
||||||
useInterfaceStore().openSettingsModal('admin')
|
useInterfaceStore().openSettingsModal('admin')
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ import Timeline from '../timeline/timeline.vue'
|
||||||
|
|
||||||
const DMs = {
|
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
|
||||||
|
|
|
||||||
|
|
@ -3,24 +3,24 @@ import ProgressButton from '../progress_button/progress_button.vue'
|
||||||
const DomainMuteCard = {
|
const DomainMuteCard = {
|
||||||
props: ['domain'],
|
props: ['domain'],
|
||||||
components: {
|
components: {
|
||||||
ProgressButton
|
ProgressButton,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
user () {
|
user() {
|
||||||
return this.$store.state.users.currentUser
|
return this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
muted () {
|
muted() {
|
||||||
return this.user.domainMutes.includes(this.domain)
|
return this.user.domainMutes.includes(this.domain)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
unmuteDomain () {
|
unmuteDomain() {
|
||||||
return this.$store.dispatch('unmuteDomain', this.domain)
|
return this.$store.dispatch('unmuteDomain', this.domain)
|
||||||
},
|
},
|
||||||
muteDomain () {
|
muteDomain() {
|
||||||
return this.$store.dispatch('muteDomain', this.domain)
|
return this.$store.dispatch('muteDomain', this.domain)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DomainMuteCard
|
export default DomainMuteCard
|
||||||
|
|
|
||||||
|
|
@ -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,23 +17,23 @@ 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: {
|
||||||
relAttrs () {
|
relAttrs() {
|
||||||
if (this.draft.type === 'edit') {
|
if (this.draft.type === 'edit') {
|
||||||
return { statusId: this.draft.refId }
|
return { statusId: this.draft.refId }
|
||||||
} else if (this.draft.type === 'reply') {
|
} else if (this.draft.type === 'reply') {
|
||||||
|
|
@ -45,24 +42,24 @@ const Draft = {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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
|
||||||
},
|
},
|
||||||
nsfwClickthrough () {
|
nsfwClickthrough() {
|
||||||
if (!this.draft.nsfw) {
|
if (!this.draft.nsfw) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -70,35 +67,34 @@ const Draft = {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
editing (newVal) {
|
editing(newVal) {
|
||||||
if (newVal) return
|
if (newVal) return
|
||||||
if (this.safeToSave) {
|
if (this.safeToSave) {
|
||||||
this.$store.dispatch('addOrSaveDraft', { draft: this.draft })
|
this.$store.dispatch('addOrSaveDraft', { draft: this.draft })
|
||||||
} else {
|
} else {
|
||||||
this.$store.dispatch('addOrSaveDraft', { draft: this.referenceDraft })
|
this.$store.dispatch('addOrSaveDraft', { draft: this.referenceDraft })
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleEditing () {
|
toggleEditing() {
|
||||||
this.editing = !this.editing
|
this.editing = !this.editing
|
||||||
},
|
},
|
||||||
abandon () {
|
abandon() {
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,29 @@
|
||||||
import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
|
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) {
|
||||||
return 'save'
|
return 'save'
|
||||||
} else {
|
} else {
|
||||||
return this.$store.getters.mergedConfig.unsavedPostAction
|
return this.$store.getters.mergedConfig.unsavedPostAction
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
shouldConfirm () {
|
shouldConfirm() {
|
||||||
return this.action === 'confirm'
|
return this.action === 'confirm'
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
requestClose () {
|
requestClose() {
|
||||||
if (this.shouldConfirm) {
|
if (this.shouldConfirm) {
|
||||||
this.showing = true
|
this.showing = true
|
||||||
} else if (this.action === 'save') {
|
} else if (this.action === 'save') {
|
||||||
|
|
@ -35,18 +32,18 @@ const DraftCloser = {
|
||||||
this.discard()
|
this.discard()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
save () {
|
save() {
|
||||||
this.$emit('save')
|
this.$emit('save')
|
||||||
this.showing = false
|
this.showing = false
|
||||||
},
|
},
|
||||||
discard () {
|
discard() {
|
||||||
this.$emit('discard')
|
this.$emit('discard')
|
||||||
this.showing = false
|
this.showing = false
|
||||||
},
|
},
|
||||||
cancel () {
|
cancel() {
|
||||||
this.showing = false
|
this.showing = false
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DraftCloser
|
export default DraftCloser
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
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() {
|
||||||
this.$refs.postStatusForm.requestClose()
|
this.$refs.postStatusForm.requestClose()
|
||||||
},
|
},
|
||||||
doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) {
|
doEditStatus({ status, spoilerText, sensitive, media, contentType, poll }) {
|
||||||
const params = {
|
const params = {
|
||||||
store: this.$store,
|
store: this.$store,
|
||||||
statusId: this.params.statusId,
|
statusId: this.params.statusId,
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,35 @@
|
||||||
|
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: {
|
||||||
isLoggedIn () {
|
isLoggedIn() {
|
||||||
return !!this.$store.state.users.currentUser
|
return !!this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
modalActivated () {
|
modalActivated() {
|
||||||
return useEditStatusStore().modalActivated
|
return useEditStatusStore().modalActivated
|
||||||
},
|
},
|
||||||
isFormVisible () {
|
isFormVisible() {
|
||||||
return this.isLoggedIn && !this.resettingForm && this.modalActivated
|
return this.isLoggedIn && !this.resettingForm && this.modalActivated
|
||||||
},
|
},
|
||||||
params () {
|
params() {
|
||||||
return useEditStatusStore().params || {}
|
return useEditStatusStore().params || {}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
params (newVal, oldVal) {
|
params(newVal, oldVal) {
|
||||||
if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) {
|
if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) {
|
||||||
this.resettingForm = true
|
this.resettingForm = true
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
|
@ -36,20 +37,22 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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,15 +99,15 @@ 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 {
|
||||||
randomSeed: genRandomSeed(),
|
randomSeed: genRandomSeed(),
|
||||||
input: undefined,
|
input: undefined,
|
||||||
|
|
@ -122,58 +120,65 @@ 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() {
|
||||||
return this.$store.getters.mergedConfig.padEmoji
|
return this.$store.getters.mergedConfig.padEmoji
|
||||||
},
|
},
|
||||||
defaultCandidateIndex () {
|
defaultCandidateIndex() {
|
||||||
return this.$store.getters.mergedConfig.autocompleteSelect ? 0 : -1
|
return this.$store.getters.mergedConfig.autocompleteSelect ? 0 : -1
|
||||||
},
|
},
|
||||||
preText () {
|
preText() {
|
||||||
return this.modelValue.slice(0, this.caret)
|
return this.modelValue.slice(0, this.caret)
|
||||||
},
|
},
|
||||||
postText () {
|
postText() {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
@ -205,16 +210,18 @@ const EmojiInput = {
|
||||||
return emoji.displayText
|
return emoji.displayText
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
suggestionListId () {
|
suggestionListId() {
|
||||||
return `suggestions-${this.randomSeed}`
|
return `suggestions-${this.randomSeed}`
|
||||||
},
|
},
|
||||||
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
|
||||||
|
|
@ -243,7 +250,7 @@ const EmojiInput = {
|
||||||
input.addEventListener('input', this.onInput)
|
input.addEventListener('input', this.onInput)
|
||||||
input.addEventListener('scroll', this.onInputScroll)
|
input.addEventListener('scroll', this.onInputScroll)
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted() {
|
||||||
const { input } = this
|
const { input } = this
|
||||||
if (input) {
|
if (input) {
|
||||||
input.removeEventListener('blur', this.onBlur)
|
input.removeEventListener('blur', this.onBlur)
|
||||||
|
|
@ -273,36 +280,40 @@ 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)
|
||||||
},
|
},
|
||||||
triggerShowPicker () {
|
triggerShowPicker() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.picker.showPicker()
|
this.$refs.picker.showPicker()
|
||||||
this.scrollIntoView()
|
this.scrollIntoView()
|
||||||
|
|
@ -315,7 +326,7 @@ const EmojiInput = {
|
||||||
this.disableClickOutside = false
|
this.disableClickOutside = false
|
||||||
}, 0)
|
}, 0)
|
||||||
},
|
},
|
||||||
togglePicker () {
|
togglePicker() {
|
||||||
this.input.focus()
|
this.input.focus()
|
||||||
if (!this.pickerShown) {
|
if (!this.pickerShown) {
|
||||||
this.scrollIntoView()
|
this.scrollIntoView()
|
||||||
|
|
@ -325,12 +336,16 @@ const EmojiInput = {
|
||||||
this.$refs.picker.hidePicker()
|
this.$refs.picker.hidePicker()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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
|
||||||
},
|
},
|
||||||
insert ({ insertion, keepOpen, surroundingSpace = true }) {
|
insert({ insertion, keepOpen, surroundingSpace = true }) {
|
||||||
const before = this.modelValue.substring(0, this.caret) || ''
|
const before = this.modelValue.substring(0, this.caret) || ''
|
||||||
const after = this.modelValue.substring(this.caret) || ''
|
const after = this.modelValue.substring(this.caret) || ''
|
||||||
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
@ -372,13 +393,20 @@ const EmojiInput = {
|
||||||
this.caret = position
|
this.caret = position
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
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
|
||||||
|
|
@ -393,7 +421,7 @@ const EmojiInput = {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cycleBackward (e) {
|
cycleBackward(e) {
|
||||||
const len = this.suggestions.length || 0
|
const len = this.suggestions.length || 0
|
||||||
|
|
||||||
this.highlighted -= 1
|
this.highlighted -= 1
|
||||||
|
|
@ -406,7 +434,7 @@ const EmojiInput = {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cycleForward (e) {
|
cycleForward(e) {
|
||||||
const len = this.suggestions.length || 0
|
const len = this.suggestions.length || 0
|
||||||
|
|
||||||
this.highlighted += 1
|
this.highlighted += 1
|
||||||
|
|
@ -418,26 +446,28 @@ const EmojiInput = {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollIntoView () {
|
scrollIntoView() {
|
||||||
const rootRef = this.$refs.picker.$el
|
const rootRef = this.$refs.picker.$el
|
||||||
/* Scroller is either `window` (replies in TL), sidebar (main post form,
|
/* Scroller is either `window` (replies in TL), sidebar (main post form,
|
||||||
* 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('.post-form-modal-view') ||
|
this.$el.closest('.sidebar-scroller') ||
|
||||||
window
|
this.$el.closest('.post-form-modal-view') ||
|
||||||
const currentScroll = scrollerRef === window
|
window
|
||||||
? scrollerRef.scrollY
|
const currentScroll =
|
||||||
: scrollerRef.scrollTop
|
scrollerRef === window ? scrollerRef.scrollY : scrollerRef.scrollTop
|
||||||
const scrollerHeight = scrollerRef === window
|
const scrollerHeight =
|
||||||
? scrollerRef.innerHeight
|
scrollerRef === window
|
||||||
: scrollerRef.offsetHeight
|
? scrollerRef.innerHeight
|
||||||
|
: 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
|
||||||
|
|
@ -459,13 +489,13 @@ const EmojiInput = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onPickerShown () {
|
onPickerShown() {
|
||||||
this.pickerShown = true
|
this.pickerShown = true
|
||||||
},
|
},
|
||||||
onPickerClosed () {
|
onPickerClosed() {
|
||||||
this.pickerShown = false
|
this.pickerShown = false
|
||||||
},
|
},
|
||||||
onBlur (e) {
|
onBlur(e) {
|
||||||
// Clicking on any suggestion removes focus from autocomplete,
|
// Clicking on any suggestion removes focus from autocomplete,
|
||||||
// preventing click handler ever executing.
|
// preventing click handler ever executing.
|
||||||
this.blurTimeout = setTimeout(() => {
|
this.blurTimeout = setTimeout(() => {
|
||||||
|
|
@ -473,10 +503,10 @@ const EmojiInput = {
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
}, 200)
|
}, 200)
|
||||||
},
|
},
|
||||||
onClick (e, suggestion) {
|
onClick(e, suggestion) {
|
||||||
this.replaceText(e, suggestion)
|
this.replaceText(e, suggestion)
|
||||||
},
|
},
|
||||||
onFocus (e) {
|
onFocus(e) {
|
||||||
if (this.blurTimeout) {
|
if (this.blurTimeout) {
|
||||||
clearTimeout(this.blurTimeout)
|
clearTimeout(this.blurTimeout)
|
||||||
this.blurTimeout = null
|
this.blurTimeout = null
|
||||||
|
|
@ -486,7 +516,7 @@ const EmojiInput = {
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
this.temporarilyHideSuggestions = false
|
this.temporarilyHideSuggestions = false
|
||||||
},
|
},
|
||||||
onKeyUp (e) {
|
onKeyUp(e) {
|
||||||
const { key } = e
|
const { key } = e
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
|
|
||||||
|
|
@ -498,10 +528,10 @@ const EmojiInput = {
|
||||||
this.temporarilyHideSuggestions = false
|
this.temporarilyHideSuggestions = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPaste (e) {
|
onPaste(e) {
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
},
|
},
|
||||||
onKeyDown (e) {
|
onKeyDown(e) {
|
||||||
const { ctrlKey, shiftKey, key } = e
|
const { ctrlKey, shiftKey, key } = e
|
||||||
if (this.newlineOnCtrlEnter && ctrlKey && key === 'Enter') {
|
if (this.newlineOnCtrlEnter && ctrlKey && key === 'Enter') {
|
||||||
this.insert({ insertion: '\n', surroundingSpace: false })
|
this.insert({ insertion: '\n', surroundingSpace: false })
|
||||||
|
|
@ -545,30 +575,30 @@ const EmojiInput = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onInput (e) {
|
onInput(e) {
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
this.$emit('update:modelValue', e.target.value)
|
this.$emit('update:modelValue', e.target.value)
|
||||||
},
|
},
|
||||||
onStickerUploaded (e) {
|
onStickerUploaded(e) {
|
||||||
this.$emit('sticker-uploaded', e)
|
this.$emit('sticker-uploaded', e)
|
||||||
},
|
},
|
||||||
onStickerUploadFailed (e) {
|
onStickerUploadFailed(e) {
|
||||||
this.$emit('sticker-upload-Failed', e)
|
this.$emit('sticker-upload-Failed', e)
|
||||||
},
|
},
|
||||||
setCaret ({ target: { selectionStart } }) {
|
setCaret({ target: { selectionStart } }) {
|
||||||
this.caret = selectionStart
|
this.caret = selectionStart
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.suggestorPopover.updateStyles()
|
this.$refs.suggestorPopover.updateStyles()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
autoCompleteItemLabel (suggestion) {
|
autoCompleteItemLabel(suggestion) {
|
||||||
if (suggestion.user) {
|
if (suggestion.user) {
|
||||||
return suggestion.displayText + ' ' + suggestion.detailText
|
return suggestion.displayText + ' ' + suggestion.detailText
|
||||||
} else {
|
} else {
|
||||||
return this.maybeLocalizedEmojiName(suggestion)
|
return this.maybeLocalizedEmojiName(suggestion)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EmojiInput
|
export default EmojiInput
|
||||||
|
|
|
||||||
|
|
@ -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,37 +105,42 @@ 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) ||
|
||||||
let aScore = 0
|
user.name.toLowerCase().startsWith(noPrefix)),
|
||||||
let bScore = 0
|
)
|
||||||
|
.slice(0, 20)
|
||||||
|
.sort((a, b) => {
|
||||||
|
let aScore = 0
|
||||||
|
let bScore = 0
|
||||||
|
|
||||||
// Matches on screen name (i.e. user@instance) makes a priority
|
// Matches on screen name (i.e. user@instance) makes a priority
|
||||||
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||||
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||||
|
|
||||||
// Matches on name takes second priority
|
// Matches on name takes second priority
|
||||||
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||||
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||||
|
|
||||||
const diff = (bScore - aScore) * 10
|
const diff = (bScore - aScore) * 10
|
||||||
|
|
||||||
// Then sort alphabetically
|
// Then sort alphabetically
|
||||||
const nameAlphabetically = a.name > b.name ? 1 : -1
|
const nameAlphabetically = a.name > b.name ? 1 : -1
|
||||||
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) => ({
|
})
|
||||||
user,
|
.map((user) => ({
|
||||||
displayText: user.screen_name_ui,
|
user,
|
||||||
detailText: user.name,
|
displayText: user.screen_name_ui,
|
||||||
imageUrl: user.profile_image_url_original,
|
detailText: user.name,
|
||||||
replacement: '@' + user.screen_name + ' '
|
imageUrl: user.profile_image_url_original,
|
||||||
}))
|
replacement: '@' + user.screen_name + ' ',
|
||||||
|
}))
|
||||||
|
|
||||||
suggestions = newSuggestions || []
|
suggestions = newSuggestions || []
|
||||||
return suggestions
|
return suggestions
|
||||||
|
|
|
||||||
|
|
@ -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,20 +101,20 @@ 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 {
|
||||||
keyword: '',
|
keyword: '',
|
||||||
activeGroup: 'custom',
|
activeGroup: 'custom',
|
||||||
|
|
@ -125,20 +129,22 @@ 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) {
|
||||||
e.currentTarget.scrollLeft += e.deltaY + e.deltaX
|
e.currentTarget.scrollLeft += e.deltaY + e.deltaX
|
||||||
},
|
},
|
||||||
updateEmojiSize () {
|
updateEmojiSize() {
|
||||||
const css = window.getComputedStyle(this.$refs.popover.$el)
|
const css = window.getComputedStyle(this.$refs.popover.$el)
|
||||||
const fontSize = css.getPropertyValue('font-size') || '1rem'
|
const fontSize = css.getPropertyValue('font-size') || '1rem'
|
||||||
const emojiSize = css.getPropertyValue('--emojiSize') || '2.2rem'
|
const emojiSize = css.getPropertyValue('--emojiSize') || '2.2rem'
|
||||||
|
|
@ -163,56 +169,68 @@ 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() {
|
||||||
this.$refs.popover.showPopover()
|
this.$refs.popover.showPopover()
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.onShowing()
|
this.onShowing()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
hidePicker () {
|
hidePicker() {
|
||||||
this.$refs.popover.hidePopover()
|
this.$refs.popover.hidePopover()
|
||||||
},
|
},
|
||||||
setAnchorEl (el) {
|
setAnchorEl(el) {
|
||||||
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')
|
||||||
},
|
},
|
||||||
onPopoverClosed () {
|
onPopoverClosed() {
|
||||||
this.$emit('close')
|
this.$emit('close')
|
||||||
},
|
},
|
||||||
onStickerUploaded (e) {
|
onStickerUploaded(e) {
|
||||||
this.$emit('sticker-uploaded', e)
|
this.$emit('sticker-uploaded', e)
|
||||||
},
|
},
|
||||||
onStickerUploadFailed (e) {
|
onStickerUploadFailed(e) {
|
||||||
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
|
||||||
this.scrolledGroup(target, visibleStartIndex, visibleEndIndex)
|
this.scrolledGroup(target, visibleStartIndex, visibleEndIndex)
|
||||||
},
|
},
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
@ -220,7 +238,7 @@ const EmojiPicker = {
|
||||||
this.scrollHeader()
|
this.scrollHeader()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
scrollHeader () {
|
scrollHeader() {
|
||||||
// Scroll the active tab's header into view
|
// Scroll the active tab's header into view
|
||||||
const headerRef = this.groupRefs['group-header-' + this.activeGroup]
|
const headerRef = this.groupRefs['group-header-' + this.activeGroup]
|
||||||
const left = headerRef.offsetLeft
|
const left = headerRef.offsetLeft
|
||||||
|
|
@ -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) {
|
||||||
|
|
@ -237,12 +257,12 @@ const EmojiPicker = {
|
||||||
setScroll(right + margin - headerCont.clientWidth)
|
setScroll(right + margin - headerCont.clientWidth)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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) {
|
||||||
if (target.scrollTop <= 5) {
|
if (target.scrollTop <= 5) {
|
||||||
this.groupsScrolledClass = 'scrolled-top'
|
this.groupsScrolledClass = 'scrolled-top'
|
||||||
} else if (target.scrollTop >= target.scrollTopMax - 5) {
|
} else if (target.scrollTop >= target.scrollTopMax - 5) {
|
||||||
|
|
@ -251,16 +271,21 @@ const EmojiPicker = {
|
||||||
this.groupsScrolledClass = 'scrolled-middle'
|
this.groupsScrolledClass = 'scrolled-middle'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleStickers () {
|
toggleStickers() {
|
||||||
this.showingStickers = !this.showingStickers
|
this.showingStickers = !this.showingStickers
|
||||||
},
|
},
|
||||||
setShowStickers (value) {
|
setShowStickers(value) {
|
||||||
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
|
||||||
this.updateEmojiSize()
|
this.updateEmojiSize()
|
||||||
this.recalculateItemPerRow()
|
this.recalculateItemPerRow()
|
||||||
|
|
@ -277,59 +302,59 @@ 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(() => {
|
||||||
if (!this.$refs['emoji-groups']) {
|
if (!this.$refs['emoji-groups']) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.width = this.$refs['emoji-groups'].$el.clientWidth
|
this.width = this.$refs['emoji-groups'].$el.clientWidth
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
keyword () {
|
keyword() {
|
||||||
this.onScroll()
|
this.onScroll()
|
||||||
this.debouncedHandleKeywordChange()
|
this.debouncedHandleKeywordChange()
|
||||||
},
|
},
|
||||||
allCustomGroups () {
|
allCustomGroups() {
|
||||||
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
minItemSize () {
|
minItemSize() {
|
||||||
return this.emojiSize
|
return this.emojiSize
|
||||||
},
|
},
|
||||||
// used to watch it
|
// used to watch it
|
||||||
fontSize () {
|
fontSize() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.updateEmojiSize()
|
this.updateEmojiSize()
|
||||||
})
|
})
|
||||||
return this.$store.getters.mergedConfig.fontSize
|
return this.$store.getters.mergedConfig.fontSize
|
||||||
},
|
},
|
||||||
emojiHeight () {
|
emojiHeight() {
|
||||||
return this.emojiSize
|
return this.emojiSize
|
||||||
},
|
},
|
||||||
itemPerRow () {
|
itemPerRow() {
|
||||||
return this.width ? Math.floor(this.width / this.emojiSize) : 6
|
return this.width ? Math.floor(this.width / this.emojiSize) : 6
|
||||||
},
|
},
|
||||||
activeGroupView () {
|
activeGroupView() {
|
||||||
return this.showingStickers ? '' : this.activeGroup
|
return this.showingStickers ? '' : this.activeGroup
|
||||||
},
|
},
|
||||||
stickersAvailable () {
|
stickersAvailable() {
|
||||||
if (this.$store.state.instance.stickers) {
|
if (this.$store.state.instance.stickers) {
|
||||||
return this.$store.state.instance.stickers.length > 0
|
return this.$store.state.instance.stickers.length > 0
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
},
|
},
|
||||||
allCustomGroups () {
|
allCustomGroups() {
|
||||||
if (this.hideCustomEmoji || this.hideCustomEmojiInPicker) {
|
if (this.hideCustomEmoji || this.hideCustomEmojiInPicker) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
@ -339,46 +364,49 @@ const EmojiPicker = {
|
||||||
}
|
}
|
||||||
return emojis
|
return emojis
|
||||||
},
|
},
|
||||||
defaultGroup () {
|
defaultGroup() {
|
||||||
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() {
|
||||||
return Object.entries(this.allCustomGroups)
|
return Object.entries(this.allCustomGroups)
|
||||||
.map(([, v]) => v)
|
.map(([, v]) => v)
|
||||||
.concat(this.unicodeEmojiGroups)
|
.concat(this.unicodeEmojiGroups)
|
||||||
},
|
},
|
||||||
stickerPickerEnabled () {
|
stickerPickerEnabled() {
|
||||||
return (this.$store.state.instance.stickers || []).length !== 0
|
return (this.$store.state.instance.stickers || []).length !== 0
|
||||||
},
|
},
|
||||||
debouncedHandleKeywordChange () {
|
debouncedHandleKeywordChange() {
|
||||||
return debounce(() => {
|
return debounce(() => {
|
||||||
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
||||||
}, 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
|
||||||
}
|
}
|
||||||
|
|
@ -396,10 +424,10 @@ const EmojiPicker = {
|
||||||
return emoji.displayText
|
return emoji.displayText
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isInModal () {
|
isInModal() {
|
||||||
return this.popoversZLayer === 'modals'
|
return this.popoversZLayer === 'modals'
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EmojiPicker
|
export default EmojiPicker
|
||||||
|
|
|
||||||
|
|
@ -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,57 +14,62 @@ const EmojiReactions = {
|
||||||
components: {
|
components: {
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
UserListPopover,
|
UserListPopover,
|
||||||
StillImage
|
StillImage,
|
||||||
},
|
},
|
||||||
props: ['status'],
|
props: ['status'],
|
||||||
data: () => ({
|
data: () => ({
|
||||||
showAll: false
|
showAll: false,
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
tooManyReactions () {
|
tooManyReactions() {
|
||||||
return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF
|
return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF
|
||||||
},
|
},
|
||||||
emojiReactions () {
|
emojiReactions() {
|
||||||
return this.showAll
|
return this.showAll
|
||||||
? this.status.emoji_reactions
|
? this.status.emoji_reactions
|
||||||
: this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF)
|
: this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF)
|
||||||
},
|
},
|
||||||
showMoreString () {
|
showMoreString() {
|
||||||
return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}`
|
return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}`
|
||||||
},
|
},
|
||||||
accountsForEmoji () {
|
accountsForEmoji() {
|
||||||
return this.status.emoji_reactions.reduce((acc, reaction) => {
|
return this.status.emoji_reactions.reduce((acc, reaction) => {
|
||||||
acc[reaction.name] = reaction.accounts || []
|
acc[reaction.name] = reaction.accounts || []
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
loggedIn () {
|
loggedIn() {
|
||||||
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) {
|
||||||
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
|
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
|
||||||
},
|
},
|
||||||
unreact (emoji) {
|
unreact(emoji) {
|
||||||
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
|
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
|
||||||
},
|
},
|
||||||
async emojiOnClick (emoji) {
|
async emojiOnClick(emoji) {
|
||||||
if (!this.loggedIn) return
|
if (!this.loggedIn) return
|
||||||
|
|
||||||
await this.fetchEmojiReactionsByIfMissing()
|
await this.fetchEmojiReactionsByIfMissing()
|
||||||
|
|
@ -81,19 +79,23 @@ const EmojiReactions = {
|
||||||
this.reactWith(emoji)
|
this.reactWith(emoji)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
counterTriggerAttrs (reaction) {
|
counterTriggerAttrs(reaction) {
|
||||||
return {
|
return {
|
||||||
class: [
|
class: [
|
||||||
'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
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
fileToDownload.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content))
|
'href',
|
||||||
fileToDownload.setAttribute('download', this.filename)
|
'data:text/plain;charset=utf-8,' + encodeURIComponent(content),
|
||||||
fileToDownload.style.display = 'none'
|
)
|
||||||
document.body.appendChild(fileToDownload)
|
fileToDownload.setAttribute('download', this.filename)
|
||||||
fileToDownload.click()
|
fileToDownload.style.display = 'none'
|
||||||
document.body.removeChild(fileToDownload)
|
document.body.appendChild(fileToDownload)
|
||||||
// Add delay before hiding processing state since browser takes some time to handle file download
|
fileToDownload.click()
|
||||||
setTimeout(() => { this.processing = false }, 2000)
|
document.body.removeChild(fileToDownload)
|
||||||
})
|
// Add delay before hiding processing state since browser takes some time to handle file download
|
||||||
}
|
setTimeout(() => {
|
||||||
}
|
this.processing = false
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Exporter
|
export default Exporter
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,54 @@
|
||||||
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'],
|
||||||
data () {
|
data() {
|
||||||
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: {
|
||||||
openPlayer () {
|
openPlayer() {
|
||||||
if (this.player) return // prevent double-loading, or re-loading on failure
|
if (this.player) return // prevent double-loading, or re-loading on failure
|
||||||
this.player = 'hidden'
|
this.player = 'hidden'
|
||||||
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
|
||||||
this.player = true
|
.load(this.src)
|
||||||
}).catch((e) => {
|
.then(() => {
|
||||||
console.error('Error loading ruffle', e)
|
this.player = true
|
||||||
this.player = 'error'
|
})
|
||||||
})
|
.catch((e) => {
|
||||||
|
console.error('Error loading ruffle', e)
|
||||||
|
this.player = 'error'
|
||||||
|
})
|
||||||
this.ruffleInstance = player
|
this.ruffleInstance = player
|
||||||
this.$emit('playerOpened')
|
this.$emit('playerOpened')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
closePlayer () {
|
closePlayer() {
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,27 @@
|
||||||
|
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: {
|
||||||
shouldConfirmUnfollow () {
|
shouldConfirmUnfollow() {
|
||||||
return this.$store.getters.mergedConfig.modalOnUnfollow
|
return this.$store.getters.mergedConfig.modalOnUnfollow
|
||||||
},
|
},
|
||||||
isPressed () {
|
isPressed() {
|
||||||
return this.inProgress || this.relationship.following
|
return this.inProgress || this.relationship.following
|
||||||
},
|
},
|
||||||
title () {
|
title() {
|
||||||
if (this.inProgress || this.relationship.following) {
|
if (this.inProgress || this.relationship.following) {
|
||||||
return this.$t('user_card.follow_unfollow')
|
return this.$t('user_card.follow_unfollow')
|
||||||
} else if (this.relationship.requested) {
|
} else if (this.relationship.requested) {
|
||||||
|
|
@ -27,7 +30,7 @@ export default {
|
||||||
return this.$t('user_card.follow')
|
return this.$t('user_card.follow')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label () {
|
label() {
|
||||||
if (this.inProgress) {
|
if (this.inProgress) {
|
||||||
return this.$t('user_card.follow_progress')
|
return this.$t('user_card.follow_progress')
|
||||||
} else if (this.relationship.following) {
|
} else if (this.relationship.following) {
|
||||||
|
|
@ -38,42 +41,47 @@ export default {
|
||||||
return this.$t('user_card.follow')
|
return this.$t('user_card.follow')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
disabled () {
|
disabled() {
|
||||||
return this.inProgress || this.user.deactivated
|
return this.inProgress || this.user.deactivated
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showConfirmUnfollow () {
|
showConfirmUnfollow() {
|
||||||
this.showingConfirmUnfollow = true
|
this.showingConfirmUnfollow = true
|
||||||
},
|
},
|
||||||
hideConfirmUnfollow () {
|
hideConfirmUnfollow() {
|
||||||
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
|
||||||
requestFollow(this.relationship.id, this.$store).then(() => {
|
requestFollow(this.relationship.id, this.$store).then(() => {
|
||||||
this.inProgress = false
|
this.inProgress = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
unfollow () {
|
unfollow() {
|
||||||
if (this.shouldConfirmUnfollow) {
|
if (this.shouldConfirmUnfollow) {
|
||||||
this.showConfirmUnfollow()
|
this.showConfirmUnfollow()
|
||||||
} else {
|
} else {
|
||||||
this.doUnfollow()
|
this.doUnfollow()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
doUnfollow () {
|
doUnfollow() {
|
||||||
const store = this.$store
|
const store = this.$store
|
||||||
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()
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,27 @@
|
||||||
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() {
|
||||||
return this.$store.state.users.currentUser.id === this.user.id
|
return this.$store.state.users.currentUser.id === this.user.id
|
||||||
},
|
},
|
||||||
loggedIn () {
|
loggedIn() {
|
||||||
return this.$store.state.users.currentUser
|
return this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
relationship () {
|
relationship() {
|
||||||
return this.$store.getters.relationship(this.user.id)
|
return this.$store.getters.relationship(this.user.id)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FollowCard
|
export default FollowCard
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,48 @@
|
||||||
|
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
|
||||||
},
|
},
|
||||||
showApproveConfirmDialog () {
|
showApproveConfirmDialog() {
|
||||||
this.showingApproveConfirmDialog = true
|
this.showingApproveConfirmDialog = true
|
||||||
},
|
},
|
||||||
hideApproveConfirmDialog () {
|
hideApproveConfirmDialog() {
|
||||||
this.showingApproveConfirmDialog = false
|
this.showingApproveConfirmDialog = false
|
||||||
},
|
},
|
||||||
showDenyConfirmDialog () {
|
showDenyConfirmDialog() {
|
||||||
this.showingDenyConfirmDialog = true
|
this.showingDenyConfirmDialog = true
|
||||||
},
|
},
|
||||||
hideDenyConfirmDialog () {
|
hideDenyConfirmDialog() {
|
||||||
this.showingDenyConfirmDialog = false
|
this.showingDenyConfirmDialog = false
|
||||||
},
|
},
|
||||||
approveUser () {
|
approveUser() {
|
||||||
if (this.shouldConfirmApprove) {
|
if (this.shouldConfirmApprove) {
|
||||||
this.showApproveConfirmDialog()
|
this.showApproveConfirmDialog()
|
||||||
} else {
|
} else {
|
||||||
this.doApprove()
|
this.doApprove()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
doApprove () {
|
doApprove() {
|
||||||
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
|
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
|
||||||
this.$store.dispatch('removeFollowRequest', this.user)
|
this.$store.dispatch('removeFollowRequest', this.user)
|
||||||
|
|
||||||
|
|
@ -48,40 +50,41 @@ 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()
|
||||||
},
|
},
|
||||||
denyUser () {
|
denyUser() {
|
||||||
if (this.shouldConfirmDeny) {
|
if (this.shouldConfirmDeny) {
|
||||||
this.showDenyConfirmDialog()
|
this.showDenyConfirmDialog()
|
||||||
} else {
|
} else {
|
||||||
this.doDeny()
|
this.doDeny()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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() {
|
||||||
return this.$store.getters.mergedConfig
|
return this.$store.getters.mergedConfig
|
||||||
},
|
},
|
||||||
shouldConfirmApprove () {
|
shouldConfirmApprove() {
|
||||||
return this.mergedConfig.modalOnApproveFollow
|
return this.mergedConfig.modalOnApproveFollow
|
||||||
},
|
},
|
||||||
shouldConfirmDeny () {
|
shouldConfirmDeny() {
|
||||||
return this.mergedConfig.modalOnDenyFollow
|
return this.mergedConfig.modalOnDenyFollow
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FollowRequestCard
|
export default FollowRequestCard
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,29 @@
|
||||||
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()
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
manualEntry: false,
|
manualEntry: false,
|
||||||
availableOptions: [
|
availableOptions: [
|
||||||
|
|
@ -37,24 +31,24 @@ 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() {
|
||||||
return typeof this.modelValue !== 'undefined'
|
return typeof this.modelValue !== 'undefined'
|
||||||
},
|
},
|
||||||
localFontsList () {
|
localFontsList() {
|
||||||
return useInterfaceStore().localFonts
|
return useInterfaceStore().localFonts
|
||||||
},
|
},
|
||||||
localFontsSize () {
|
localFontsSize() {
|
||||||
return useInterfaceStore().localFonts?.length
|
return useInterfaceStore().localFonts?.length
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,52 +18,71 @@ const Gallery = {
|
||||||
'shiftUpAttachment',
|
'shiftUpAttachment',
|
||||||
'shiftDnAttachment',
|
'shiftDnAttachment',
|
||||||
'editAttachment',
|
'editAttachment',
|
||||||
'grid'
|
'grid',
|
||||||
],
|
],
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
sizes: {},
|
sizes: {},
|
||||||
hidingLong: true
|
hidingLong: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: { Attachment },
|
components: { Attachment },
|
||||||
computed: {
|
computed: {
|
||||||
rows () {
|
rows() {
|
||||||
if (!this.attachments) {
|
if (!this.attachments) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
const attachments = this.limit > 0
|
const attachments =
|
||||||
? this.attachments.slice(0, this.limit)
|
this.limit > 0
|
||||||
: this.attachments
|
? this.attachments.slice(0, this.limit)
|
||||||
|
: 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
|
||||||
if (attachment.mimetype.includes('audio')) {
|
.reduce(
|
||||||
return [...acc, { audio: true, items: [attachment] }, { items: [] }]
|
(acc, attachment, i) => {
|
||||||
}
|
if (attachment.mimetype.includes('audio')) {
|
||||||
if (!(
|
return [
|
||||||
attachment.mimetype.includes('image') ||
|
...acc,
|
||||||
attachment.mimetype.includes('video') ||
|
{ audio: true, items: [attachment] },
|
||||||
attachment.mimetype.includes('flash')
|
{ items: [] },
|
||||||
)) {
|
]
|
||||||
return [...acc, { minimal: true, items: [attachment] }, { items: [] }]
|
}
|
||||||
}
|
if (
|
||||||
const maxPerRow = 3
|
!(
|
||||||
const attachmentsRemaining = this.attachments.length - i + 1
|
attachment.mimetype.includes('image') ||
|
||||||
const currentRow = acc[acc.length - 1].items
|
attachment.mimetype.includes('video') ||
|
||||||
currentRow.push(attachment)
|
attachment.mimetype.includes('flash')
|
||||||
if (currentRow.length >= maxPerRow && attachmentsRemaining > maxPerRow) {
|
)
|
||||||
return [...acc, { items: [] }]
|
) {
|
||||||
} else {
|
return [
|
||||||
return acc
|
...acc,
|
||||||
}
|
{ minimal: true, items: [attachment] },
|
||||||
}, [{ items: [] }]).filter(_ => _.items.length > 0)
|
{ items: [] },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const maxPerRow = 3
|
||||||
|
const attachmentsRemaining = this.attachments.length - i + 1
|
||||||
|
const currentRow = acc[acc.length - 1].items
|
||||||
|
currentRow.push(attachment)
|
||||||
|
if (
|
||||||
|
currentRow.length >= maxPerRow &&
|
||||||
|
attachmentsRemaining > maxPerRow
|
||||||
|
) {
|
||||||
|
return [...acc, { items: [] }]
|
||||||
|
} else {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[{ items: [] }],
|
||||||
|
)
|
||||||
|
.filter((_) => _.items.length > 0)
|
||||||
return rows
|
return rows
|
||||||
},
|
},
|
||||||
attachmentsDimensionalScore () {
|
attachmentsDimensionalScore() {
|
||||||
return this.rows.reduce((acc, row) => {
|
return this.rows.reduce((acc, row) => {
|
||||||
let size = 0
|
let size = 0
|
||||||
if (row.minimal) {
|
if (row.minimal) {
|
||||||
|
|
@ -75,7 +95,7 @@ const Gallery = {
|
||||||
return acc + size
|
return acc + size
|
||||||
}, 0)
|
}, 0)
|
||||||
},
|
},
|
||||||
tooManyAttachments () {
|
tooManyAttachments() {
|
||||||
if (this.editable || this.size === 'small') {
|
if (this.editable || this.size === 'small') {
|
||||||
return false
|
return false
|
||||||
} else if (this.size === 'hide') {
|
} else if (this.size === 'hide') {
|
||||||
|
|
@ -83,38 +103,38 @@ const Gallery = {
|
||||||
} else {
|
} else {
|
||||||
return this.attachmentsDimensionalScore > 1
|
return this.attachmentsDimensionalScore > 1
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onNaturalSizeLoad ({ id, width, height }) {
|
onNaturalSizeLoad({ id, width, height }) {
|
||||||
set(this.sizes, id, { width, height })
|
set(this.sizes, id, { width, height })
|
||||||
},
|
},
|
||||||
rowStyle (row) {
|
rowStyle(row) {
|
||||||
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) {
|
||||||
const size = this.sizes[id]
|
const size = this.sizes[id]
|
||||||
return size ? size.width / size.height : 1
|
return size ? size.width / size.height : 1
|
||||||
},
|
},
|
||||||
toggleHidingLong (event) {
|
toggleHidingLong(event) {
|
||||||
this.hidingLong = event
|
this.hidingLong = event
|
||||||
},
|
},
|
||||||
openGallery () {
|
openGallery() {
|
||||||
useMediaViewerStore().setMedia(this.attachments)
|
useMediaViewerStore().setMedia(this.attachments)
|
||||||
useMediaViewerStore().setCurrentMedia(this.attachments[0])
|
useMediaViewerStore().setCurrentMedia(this.attachments[0])
|
||||||
},
|
},
|
||||||
onMedia () {
|
onMedia() {
|
||||||
useMediaViewerStore().setMedia(this.attachments)
|
useMediaViewerStore().setMedia(this.attachments)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Gallery
|
export default Gallery
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -5,20 +5,20 @@ 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() {
|
||||||
const tag = this.tag || extractTagFromUrl(this.url)
|
const tag = this.tag || extractTagFromUrl(this.url)
|
||||||
if (tag) {
|
if (tag) {
|
||||||
const link = this.generateTagLink(tag)
|
const link = this.generateTagLink(tag)
|
||||||
|
|
@ -27,10 +27,10 @@ const HashtagLink = {
|
||||||
window.open(this.url, '_blank')
|
window.open(this.url, '_blank')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
generateTagLink (tag) {
|
generateTagLink(tag) {
|
||||||
return `/tag/${tag}`
|
return `/tag/${tag}`
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HashtagLink
|
export default HashtagLink
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: [
|
||||||
|
|
@ -31,12 +28,12 @@ const ImageCropper = {
|
||||||
'close', // cropper is closed
|
'close', // cropper is closed
|
||||||
],
|
],
|
||||||
methods: {
|
methods: {
|
||||||
destroy () {
|
destroy() {
|
||||||
this.$refs.input.value = ''
|
this.$refs.input.value = ''
|
||||||
this.dataUrl = undefined
|
this.dataUrl = undefined
|
||||||
this.$emit('close')
|
this.$emit('close')
|
||||||
},
|
},
|
||||||
submit (cropping = true) {
|
submit(cropping = true) {
|
||||||
let cropperPromise
|
let cropperPromise
|
||||||
if (cropping) {
|
if (cropping) {
|
||||||
cropperPromise = this.$refs.cropperSelection.$toCanvas()
|
cropperPromise = this.$refs.cropperSelection.$toCanvas()
|
||||||
|
|
@ -44,14 +41,14 @@ 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 })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
pickImage () {
|
pickImage() {
|
||||||
this.$refs.input.click()
|
this.$refs.input.click()
|
||||||
},
|
},
|
||||||
readFile () {
|
readFile() {
|
||||||
const fileInput = this.$refs.input
|
const fileInput = this.$refs.input
|
||||||
if (fileInput.files != null && fileInput.files[0] != null) {
|
if (fileInput.files != null && fileInput.files[0] != null) {
|
||||||
this.file = fileInput.files[0]
|
this.file = fileInput.files[0]
|
||||||
|
|
@ -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,11 +81,11 @@ 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
|
||||||
fileInput.addEventListener('change', this.readFile)
|
fileInput.addEventListener('change', this.readFile)
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,49 @@
|
||||||
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: {
|
||||||
change () {
|
change() {
|
||||||
this.file = this.$refs.input.files[0]
|
this.file = this.$refs.input.files[0]
|
||||||
},
|
},
|
||||||
submit () {
|
submit() {
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
{
|
||||||
y: 0,
|
x: 0,
|
||||||
blur: 2,
|
y: 0,
|
||||||
spread: 0,
|
blur: 2,
|
||||||
color: '#000000',
|
spread: 0,
|
||||||
alpha: 1
|
color: '#000000',
|
||||||
}, '--defaultInputBevel']
|
alpha: 1,
|
||||||
}
|
},
|
||||||
|
'--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',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
const InstanceSpecificPanel = {
|
const InstanceSpecificPanel = {
|
||||||
computed: {
|
computed: {
|
||||||
instanceSpecificPanelContent () {
|
instanceSpecificPanelContent() {
|
||||||
return this.$store.state.instance.instanceSpecificPanelContent
|
return this.$store.state.instance.instanceSpecificPanelContent
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InstanceSpecificPanel
|
export default InstanceSpecificPanel
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,63 @@
|
||||||
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: {
|
||||||
languages () {
|
languages() {
|
||||||
return localeService.languages
|
return localeService.languages
|
||||||
},
|
},
|
||||||
uniqueId () {
|
uniqueId() {
|
||||||
return uuidv4()
|
return uuidv4()
|
||||||
},
|
},
|
||||||
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: {
|
||||||
getLanguageName (code) {
|
getLanguageName(code) {
|
||||||
return localeService.getLanguageName(code)
|
return localeService.getLanguageName(code)
|
||||||
},
|
},
|
||||||
addLanguage () {
|
addLanguage() {
|
||||||
this.controlledLanguage = [...this.controlledLanguage, '']
|
this.controlledLanguage = [...this.controlledLanguage, '']
|
||||||
},
|
},
|
||||||
setLanguageAt (index, val) {
|
setLanguageAt(index, val) {
|
||||||
const lang = [...this.controlledLanguage]
|
const lang = [...this.controlledLanguage]
|
||||||
lang[index] = val
|
lang[index] = val
|
||||||
this.controlledLanguage = lang
|
this.controlledLanguage = lang
|
||||||
},
|
},
|
||||||
removeLanguageAt (index) {
|
removeLanguageAt(index) {
|
||||||
const lang = [...this.controlledLanguage]
|
const lang = [...this.controlledLanguage]
|
||||||
lang.splice(index, 1)
|
lang.splice(index, 1)
|
||||||
this.controlledLanguage = lang
|
this.controlledLanguage = lang
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,37 +2,31 @@ import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
const LinkPreview = {
|
const LinkPreview = {
|
||||||
name: 'LinkPreview',
|
name: 'LinkPreview',
|
||||||
props: [
|
props: ['card', 'size', 'nsfw'],
|
||||||
'card',
|
data() {
|
||||||
'size',
|
|
||||||
'nsfw'
|
|
||||||
],
|
|
||||||
data () {
|
|
||||||
return {
|
return {
|
||||||
imageLoaded: false
|
imageLoaded: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
useImage () {
|
useImage() {
|
||||||
// Currently BE shoudn't give cards if tagged NSFW, this is a bit paranoid
|
// Currently BE shoudn't give cards if tagged NSFW, this is a bit paranoid
|
||||||
// as it makes sure to hide the image if somehow NSFW tagged preview can
|
// as it makes sure to hide the image if somehow NSFW tagged preview can
|
||||||
// exist.
|
// exist.
|
||||||
return this.card.image && !this.censored && this.size !== 'hide'
|
return this.card.image && !this.censored && this.size !== 'hide'
|
||||||
},
|
},
|
||||||
censored () {
|
censored() {
|
||||||
return this.nsfw && this.hideNsfwConfig
|
return this.nsfw && this.hideNsfwConfig
|
||||||
},
|
},
|
||||||
useDescription () {
|
useDescription() {
|
||||||
return this.card.description && /\S/.test(this.card.description)
|
return this.card.description && /\S/.test(this.card.description)
|
||||||
},
|
},
|
||||||
hideNsfwConfig () {
|
hideNsfwConfig() {
|
||||||
return this.mergedConfig.hideNsfw
|
return this.mergedConfig.hideNsfw
|
||||||
},
|
},
|
||||||
...mapGetters([
|
...mapGetters(['mergedConfig']),
|
||||||
'mergedConfig'
|
|
||||||
])
|
|
||||||
},
|
},
|
||||||
created () {
|
created() {
|
||||||
if (this.useImage) {
|
if (this.useImage) {
|
||||||
const newImg = new Image()
|
const newImg = new Image()
|
||||||
newImg.onload = () => {
|
newImg.onload = () => {
|
||||||
|
|
@ -40,7 +34,7 @@ const LinkPreview = {
|
||||||
}
|
}
|
||||||
newImg.src = this.card.image
|
newImg.src = this.card.image
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LinkPreview
|
export default LinkPreview
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -2,27 +2,27 @@ import { useListsStore } from 'src/stores/lists'
|
||||||
import ListsCard from '../lists_card/lists_card.vue'
|
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() {
|
||||||
this.isNew = false
|
this.isNew = false
|
||||||
},
|
},
|
||||||
newList () {
|
newList() {
|
||||||
this.isNew = true
|
this.isNew = true
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Lists
|
export default Lists
|
||||||
|
|
|
||||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue