Merge branch 'more-fixes' into mfm
This commit is contained in:
commit
5ca430a1c0
358 changed files with 11941 additions and 10638 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
# This file is a template, and might need editing before it works on your project.
|
# This file is a template, and might need editing before it works on your project.
|
||||||
# Official framework image. Look for the different tagged releases at:
|
# Official framework image. Look for the different tagged releases at:
|
||||||
# https://hub.docker.com/r/library/node/tags/
|
# https://hub.docker.com/r/library/node/tags/
|
||||||
image: node:18
|
image: node:20
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- check-changelog
|
- check-changelog
|
||||||
|
|
@ -73,7 +73,7 @@ test:
|
||||||
|
|
||||||
e2e-pleroma:
|
e2e-pleroma:
|
||||||
stage: test
|
stage: test
|
||||||
image: mcr.microsoft.com/playwright:v1.57.0-jammy
|
image: mcr.microsoft.com/playwright:v1.61.0-jammy
|
||||||
services:
|
services:
|
||||||
- name: postgres:15-alpine
|
- name: postgres:15-alpine
|
||||||
alias: db
|
alias: db
|
||||||
|
|
@ -157,7 +157,7 @@ e2e-pleroma:
|
||||||
variables:
|
variables:
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
||||||
FF_NETWORK_PER_BUILD: "true"
|
FF_NETWORK_PER_BUILD: "true"
|
||||||
PLEROMA_IMAGE: git.pleroma.social:5050/pleroma/pleroma:stable
|
PLEROMA_IMAGE: git.pleroma.social/pleroma/pleroma:stable
|
||||||
POSTGRES_USER: pleroma
|
POSTGRES_USER: pleroma
|
||||||
POSTGRES_PASSWORD: pleroma
|
POSTGRES_PASSWORD: pleroma
|
||||||
POSTGRES_DB: pleroma
|
POSTGRES_DB: pleroma
|
||||||
|
|
@ -175,7 +175,7 @@ e2e-pleroma:
|
||||||
NOTIFY_EMAIL: $E2E_ADMIN_EMAIL
|
NOTIFY_EMAIL: $E2E_ADMIN_EMAIL
|
||||||
VITE_PROXY_TARGET: http://pleroma:4000
|
VITE_PROXY_TARGET: http://pleroma:4000
|
||||||
VITE_PROXY_ORIGIN: http://localhost:4000
|
VITE_PROXY_ORIGIN: http://localhost:4000
|
||||||
E2E_BASE_URL: http://localhost:8080
|
E2E_BASE_URL: http://localhost:8099
|
||||||
script:
|
script:
|
||||||
- npm install -g yarn@1.22.22
|
- npm install -g yarn@1.22.22
|
||||||
- yarn --frozen-lockfile
|
- yarn --frozen-lockfile
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
18.20.8
|
20.19.0
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ labels:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
build:
|
build:
|
||||||
image: docker.io/node:18-alpine
|
image: docker.io/node:20-alpine
|
||||||
commands:
|
commands:
|
||||||
- apk add --no-cache zip git
|
- apk add --no-cache zip git
|
||||||
- yarn --frozen-lockfile
|
- yarn --frozen-lockfile
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ when:
|
||||||
steps:
|
steps:
|
||||||
install-depends:
|
install-depends:
|
||||||
image: &node-image
|
image: &node-image
|
||||||
docker.io/node:18-alpine
|
docker.io/node:20-alpine
|
||||||
commands:
|
commands:
|
||||||
- yarn --frozen-lockfile
|
- yarn --frozen-lockfile
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,12 @@ variables:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
test:
|
test:
|
||||||
image: mcr.microsoft.com/playwright:v1.57.0-jammy
|
image: mcr.microsoft.com/playwright:v1.61.0-jammy
|
||||||
entrypoint: *script_file_entrypoint
|
entrypoint: *script_file_entrypoint
|
||||||
environment:
|
environment:
|
||||||
APT_CACHE_DIR: apt-cache
|
APT_CACHE_DIR: apt-cache
|
||||||
DEBIAN_FRONTEND: noninteractive
|
DEBIAN_FRONTEND: noninteractive
|
||||||
E2E_BASE_URL: http://localhost:8080
|
E2E_BASE_URL: http://localhost:8099
|
||||||
FF_NETWORK_PER_BUILD: "true"
|
FF_NETWORK_PER_BUILD: "true"
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
||||||
VITE_PROXY_ORIGIN: "http://pleroma:4000"
|
VITE_PROXY_ORIGIN: "http://pleroma:4000"
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ variables:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
test:
|
test:
|
||||||
image: mcr.microsoft.com/playwright:v1.57.0-jammy
|
image: mcr.microsoft.com/playwright:v1.61.0-jammy
|
||||||
environment:
|
environment:
|
||||||
APT_CACHE_DIR: apt-cache
|
APT_CACHE_DIR: apt-cache
|
||||||
DEBIAN_FRONTEND: noninteractive
|
DEBIAN_FRONTEND: noninteractive
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@
|
||||||
"noUnusedLabels": "error",
|
"noUnusedLabels": "error",
|
||||||
"noUnusedPrivateClassMembers": "error",
|
"noUnusedPrivateClassMembers": "error",
|
||||||
"noUnusedVariables": "error",
|
"noUnusedVariables": "error",
|
||||||
|
"noUnusedImports": "error",
|
||||||
"useIsNan": "error",
|
"useIsNan": "error",
|
||||||
"useValidForDirection": "error",
|
"useValidForDirection": "error",
|
||||||
"useValidTypeof": "error",
|
"useValidTypeof": "error",
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ export default function () {
|
||||||
const warning = warnings[i]
|
const warning = warnings[i]
|
||||||
console.warn(' ' + warning)
|
console.warn(' ' + warning)
|
||||||
}
|
}
|
||||||
console.warn()
|
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
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 * as esbuild from 'esbuild'
|
import { exactRegex } from '@rolldown/pluginutils'
|
||||||
import { build } from 'vite'
|
import { build } from 'vite'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -15,106 +14,6 @@ const getSWMessagesAsText = async () => {
|
||||||
}
|
}
|
||||||
const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
||||||
|
|
||||||
const swEnvName = 'virtual:pleroma-fe/service_worker_env'
|
|
||||||
const swEnvNameResolved = '\0' + swEnvName
|
|
||||||
const getDevSwEnv = () => `self.serviceWorkerOption = { assets: [] };`
|
|
||||||
const getProdSwEnv = ({ assets }) =>
|
|
||||||
`self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };`
|
|
||||||
|
|
||||||
export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => {
|
|
||||||
const swFullSrc = resolve(projectRoot, swSrc)
|
|
||||||
const esbuildAlias = {}
|
|
||||||
Object.entries(alias).forEach(([source, dest]) => {
|
|
||||||
esbuildAlias[source] = dest.startsWith('/') ? projectRoot + dest : dest
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: 'dev-sw-plugin',
|
|
||||||
apply: 'serve',
|
|
||||||
configResolved() {
|
|
||||||
/* no-op */
|
|
||||||
},
|
|
||||||
resolveId(id) {
|
|
||||||
const name = id.startsWith('/') ? id.slice(1) : id
|
|
||||||
if (name === swDest) {
|
|
||||||
return swFullSrc
|
|
||||||
} else if (name === swEnvName) {
|
|
||||||
return swEnvNameResolved
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
async load(id) {
|
|
||||||
if (id === swFullSrc) {
|
|
||||||
return readFile(swFullSrc, 'utf-8')
|
|
||||||
} else if (id === swEnvNameResolved) {
|
|
||||||
return getDevSwEnv()
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* vite does not bundle the service worker
|
|
||||||
* during dev, and firefox does not support ESM as service worker
|
|
||||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=1360870
|
|
||||||
*/
|
|
||||||
async transform(code, id) {
|
|
||||||
if (id === swFullSrc && transformSW) {
|
|
||||||
const res = await esbuild.build({
|
|
||||||
entryPoints: [swSrc],
|
|
||||||
bundle: true,
|
|
||||||
write: false,
|
|
||||||
outfile: 'sw-pleroma.js',
|
|
||||||
alias: esbuildAlias,
|
|
||||||
plugins: [
|
|
||||||
{
|
|
||||||
name: 'vite-like-root-resolve',
|
|
||||||
setup(b) {
|
|
||||||
b.onResolve({ filter: new RegExp(/^\//) }, (args) => ({
|
|
||||||
path: resolve(projectRoot, args.path.slice(1)),
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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(),
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
const text = res.outputFiles[0].text
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Idea taken from
|
// Idea taken from
|
||||||
// https://github.com/vite-pwa/vite-plugin-pwa/blob/main/src/plugins/build.ts
|
// https://github.com/vite-pwa/vite-plugin-pwa/blob/main/src/plugins/build.ts
|
||||||
// rollup does not support compiling to iife if we want to code-split;
|
// rollup does not support compiling to iife if we want to code-split;
|
||||||
|
|
@ -122,12 +21,17 @@ export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => {
|
||||||
// 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 = ({ swSrc, swDest }) => {
|
export const buildSwPlugin = ({ swSrc, swDest }) => {
|
||||||
|
const swFullSrc = resolve(projectRoot, swSrc)
|
||||||
|
const swEnvName = 'virtual:pleroma-fe/service_worker_env'
|
||||||
|
const swEnvNameResolved = '\0' + swEnvName
|
||||||
|
|
||||||
let config
|
let config
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'build-sw-plugin',
|
name: 'build-sw-plugin',
|
||||||
enforce: 'post',
|
enforce: 'post',
|
||||||
apply: 'build',
|
|
||||||
configResolved(resolvedConfig) {
|
configResolved(resolvedConfig) {
|
||||||
|
resolvedConfig
|
||||||
config = {
|
config = {
|
||||||
define: resolvedConfig.define,
|
define: resolvedConfig.define,
|
||||||
resolve: resolvedConfig.resolve,
|
resolve: resolvedConfig.resolve,
|
||||||
|
|
@ -135,15 +39,16 @@ export const buildSwPlugin = ({ swSrc, swDest }) => {
|
||||||
publicDir: false,
|
publicDir: false,
|
||||||
build: {
|
build: {
|
||||||
...resolvedConfig.build,
|
...resolvedConfig.build,
|
||||||
lib: {
|
|
||||||
entry: swSrc,
|
|
||||||
formats: ['iife'],
|
|
||||||
name: 'sw_pleroma',
|
|
||||||
},
|
|
||||||
emptyOutDir: false,
|
emptyOutDir: false,
|
||||||
rollupOptions: {
|
rolldownOptions: {
|
||||||
|
input: {
|
||||||
|
main: swSrc,
|
||||||
|
},
|
||||||
|
context: 'self',
|
||||||
output: {
|
output: {
|
||||||
entryFileNames: swDest,
|
entryFileNames: swDest,
|
||||||
|
codeSplitting: false,
|
||||||
|
format: 'iife',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -157,29 +62,64 @@ export const buildSwPlugin = ({ swSrc, swDest }) => {
|
||||||
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) {
|
mode: 'production',
|
||||||
if (id === swEnvName) {
|
resolveId: {
|
||||||
return swEnvNameResolved
|
filter: { id: exactRegex(swEnvName) },
|
||||||
}
|
handler: () => swEnvNameResolved,
|
||||||
return null
|
|
||||||
},
|
},
|
||||||
load(id) {
|
load: {
|
||||||
if (id === swEnvNameResolved) {
|
filter: { id: exactRegex(swEnvNameResolved) },
|
||||||
return getProdSwEnv({ assets })
|
handler() {
|
||||||
}
|
return `self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };`
|
||||||
return null
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
resolveId: {
|
||||||
|
filter: { id: new RegExp(swDest) },
|
||||||
|
handler() {
|
||||||
|
return swFullSrc
|
||||||
|
},
|
||||||
|
},
|
||||||
|
load: {
|
||||||
|
filter: { id: new RegExp(swFullSrc) },
|
||||||
|
async handler() {
|
||||||
|
config.plugins.push({
|
||||||
|
name: 'dummy-sw-env',
|
||||||
|
mode: 'development',
|
||||||
|
resolveId: {
|
||||||
|
filter: { id: exactRegex(swEnvName) },
|
||||||
|
handler: () => swEnvNameResolved,
|
||||||
|
},
|
||||||
|
load: {
|
||||||
|
filter: { id: exactRegex(swEnvNameResolved) },
|
||||||
|
handler: () => 'self.serviceWorkerOption = { assets: [] }',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const swBundle = await build(config)
|
||||||
|
return swBundle.output[0]
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error building ServiceWorker:', e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
closeBundle: {
|
closeBundle: {
|
||||||
order: 'post',
|
order: 'post',
|
||||||
sequential: true,
|
sequential: true,
|
||||||
async handler() {
|
async handler() {
|
||||||
|
if (process.env.VITEST) return
|
||||||
console.info('Building service worker for production')
|
console.info('Building service worker for production')
|
||||||
await build(config)
|
try {
|
||||||
|
await build(config)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error building ServiceWorker:', e)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
0
changelog.d/api-refactor.skip
Normal file
0
changelog.d/api-refactor.skip
Normal file
1
changelog.d/fast.change
Normal file
1
changelog.d/fast.change
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Migrated to Vite 8 and optimized our imports, more stuff is loaded on-demand, reducing the initial load time and transfer size
|
||||||
|
|
@ -4,3 +4,4 @@ reply/quote now is a radio group and wraps, fixes overflow on languages where la
|
||||||
personal note input is now bigger
|
personal note input is now bigger
|
||||||
moved "edit pinned" to the bottom for status action buttons.
|
moved "edit pinned" to the bottom for status action buttons.
|
||||||
dots status action button drops down instead of up to avoid hiding the action buttons
|
dots status action button drops down instead of up to avoid hiding the action buttons
|
||||||
|
improved attachment description (alt text) input and display
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,4 @@ second language input not having header
|
||||||
post form's bottom left buttons not showing their toggled state
|
post form's bottom left buttons not showing their toggled state
|
||||||
some font overrides not working
|
some font overrides not working
|
||||||
popovers opening outside of window's boundaries
|
popovers opening outside of window's boundaries
|
||||||
|
occasional blank page when showing new posts
|
||||||
|
|
|
||||||
0
changelog.d/more-fixes.skip
Normal file
0
changelog.d/more-fixes.skip
Normal file
1
changelog.d/profilebg.add
Normal file
1
changelog.d/profilebg.add
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
displaying other user's backgrounds (if supported by BE)
|
||||||
1
changelog.d/reply-quote-config.fix
Normal file
1
changelog.d/reply-quote-config.fix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Fix reply form crash when quote-reply settings are unavailable
|
||||||
1
changelog.d/user-management.add
Normal file
1
changelog.d/user-management.add
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
User administration + post scope/sensitivity admin change support
|
||||||
|
|
@ -12,7 +12,7 @@ services:
|
||||||
retries: 30
|
retries: 30
|
||||||
|
|
||||||
pleroma:
|
pleroma:
|
||||||
image: ${PLEROMA_IMAGE:-git.pleroma.social:5050/pleroma/pleroma:stable}
|
image: ${PLEROMA_IMAGE:-git.pleroma.social/pleroma/pleroma:stable}
|
||||||
environment:
|
environment:
|
||||||
DB_USER: pleroma
|
DB_USER: pleroma
|
||||||
DB_PASS: pleroma
|
DB_PASS: pleroma
|
||||||
|
|
@ -39,6 +39,8 @@ services:
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 60
|
retries: 60
|
||||||
|
ports:
|
||||||
|
- 4000:4000
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
build:
|
build:
|
||||||
|
|
@ -51,7 +53,7 @@ services:
|
||||||
CI: "1"
|
CI: "1"
|
||||||
VITE_PROXY_TARGET: http://pleroma:4000
|
VITE_PROXY_TARGET: http://pleroma:4000
|
||||||
VITE_PROXY_ORIGIN: http://localhost:4000
|
VITE_PROXY_ORIGIN: http://localhost:4000
|
||||||
E2E_BASE_URL: http://localhost:8080
|
E2E_BASE_URL: http://localhost:8099
|
||||||
E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-admin}
|
E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-admin}
|
||||||
E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin}
|
E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin}
|
||||||
command: ["yarn", "e2e:pw"]
|
command: ["yarn", "e2e:pw"]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
FROM mcr.microsoft.com/playwright:v1.57.0-jammy
|
FROM mcr.microsoft.com/playwright:v1.61.0-jammy
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
|
||||||
27
package.json
27
package.json
|
|
@ -43,7 +43,7 @@
|
||||||
"localforage": "1.10.0",
|
"localforage": "1.10.0",
|
||||||
"parse-link-header": "2.0.0",
|
"parse-link-header": "2.0.0",
|
||||||
"phoenix": "1.8.1",
|
"phoenix": "1.8.1",
|
||||||
"pinia": "^3.0.0",
|
"pinia": "^3.0.4",
|
||||||
"punycode.js": "2.3.1",
|
"punycode.js": "2.3.1",
|
||||||
"qrcode": "1.5.4",
|
"qrcode": "1.5.4",
|
||||||
"querystring-es3": "0.2.1",
|
"querystring-es3": "0.2.1",
|
||||||
|
|
@ -65,10 +65,12 @@
|
||||||
"@biomejs/biome": "2.3.11",
|
"@biomejs/biome": "2.3.11",
|
||||||
"@pinia/testing": "1.0.3",
|
"@pinia/testing": "1.0.3",
|
||||||
"@ungap/event-target": "0.2.4",
|
"@ungap/event-target": "0.2.4",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/devtools": "^0.3.1",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
"@vitejs/plugin-vue": "^6.0.7",
|
||||||
"@vitest/browser": "^3.0.7",
|
"@vitejs/plugin-vue-jsx": "^5.1.5",
|
||||||
"@vitest/ui": "^3.0.7",
|
"@vitest/browser-playwright": "^4.1.7",
|
||||||
|
"@vitest/browser": "^4.1.7",
|
||||||
|
"@vitest/ui": "^4.1.7",
|
||||||
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
|
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
|
||||||
"@vue/babel-plugin-jsx": "1.5.0",
|
"@vue/babel-plugin-jsx": "1.5.0",
|
||||||
"@vue/compiler-sfc": "3.5.22",
|
"@vue/compiler-sfc": "3.5.22",
|
||||||
|
|
@ -94,13 +96,14 @@
|
||||||
"http-proxy-middleware": "3.0.5",
|
"http-proxy-middleware": "3.0.5",
|
||||||
"iso-639-1": "3.1.5",
|
"iso-639-1": "3.1.5",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"msw": "2.10.5",
|
"msw": "2.14.6",
|
||||||
"nightwatch": "3.12.2",
|
"nightwatch": "3.12.2",
|
||||||
"playwright": "1.57.0",
|
"oxc": "^1.0.1",
|
||||||
|
"playwright": "1.61.0",
|
||||||
"postcss": "8.5.6",
|
"postcss": "8.5.6",
|
||||||
"postcss-html": "^1.5.0",
|
"postcss-html": "^1.5.0",
|
||||||
"postcss-scss": "^4.0.6",
|
"postcss-scss": "^4.0.6",
|
||||||
"sass": "1.93.2",
|
"sass-embedded": "^1.100.0",
|
||||||
"selenium-server": "3.141.59",
|
"selenium-server": "3.141.59",
|
||||||
"semver": "7.7.3",
|
"semver": "7.7.3",
|
||||||
"serve-static": "2.2.0",
|
"serve-static": "2.2.0",
|
||||||
|
|
@ -113,10 +116,10 @@
|
||||||
"stylelint-config-recommended-scss": "^14.0.0",
|
"stylelint-config-recommended-scss": "^14.0.0",
|
||||||
"stylelint-config-recommended-vue": "^1.6.0",
|
"stylelint-config-recommended-vue": "^1.6.0",
|
||||||
"stylelint-config-standard": "38.0.0",
|
"stylelint-config-standard": "38.0.0",
|
||||||
"vite": "^6.1.0",
|
"vite": "^8.0.0",
|
||||||
"vite-plugin-eslint2": "^5.0.3",
|
"vite-plugin-eslint2": "^5.1.0",
|
||||||
"vite-plugin-stylelint": "^6.0.0",
|
"vite-plugin-stylelint": "^6.1.0",
|
||||||
"vitest": "^3.0.7",
|
"vitest": "^4.1.7",
|
||||||
"vue-eslint-parser": "10.2.0"
|
"vue-eslint-parser": "10.2.0"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
80
src/App.js
80
src/App.js
|
|
@ -2,22 +2,15 @@ import { throttle } from 'lodash'
|
||||||
import { mapState } from 'pinia'
|
import { mapState } from 'pinia'
|
||||||
import { defineAsyncComponent } from 'vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
import DesktopNav from './components/desktop_nav/desktop_nav.vue'
|
import DesktopNav from 'src/components/desktop_nav/desktop_nav.vue'
|
||||||
import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
|
import FeaturesPanel from 'src/components/features_panel/features_panel.vue'
|
||||||
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
import GlobalError from 'src/components/global_error/global_error.vue'
|
||||||
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
import GlobalNoticeList from 'src/components/global_notice_list/global_notice_list.vue'
|
||||||
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
|
import InstanceSpecificPanel from 'src/components/instance_specific_panel/instance_specific_panel.vue'
|
||||||
import MediaModal from './components/media_modal/media_modal.vue'
|
import MobileNav from 'src/components/mobile_nav/mobile_nav.vue'
|
||||||
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
import MobilePostStatusButton from 'src/components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
|
||||||
import NavPanel from './components/nav_panel/nav_panel.vue'
|
import UserPanel from 'src/components/user_panel/user_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 { getOrCreateServiceWorker } from './services/sw/sw'
|
||||||
import { windowHeight, windowWidth } from './services/window_utils/window_utils'
|
import { windowHeight, windowWidth } from './services/window_utils/window_utils'
|
||||||
|
|
||||||
|
|
@ -29,9 +22,6 @@ import { useInterfaceStore } from 'src/stores/interface.js'
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
import { useShoutStore } from 'src/stores/shout.js'
|
import { useShoutStore } from 'src/stores/shout.js'
|
||||||
|
|
||||||
import messages from 'src/i18n/messages'
|
|
||||||
import localeService from 'src/services/locale/locale.service.js'
|
|
||||||
|
|
||||||
// Helper to unwrap reactive proxies
|
// Helper to unwrap reactive proxies
|
||||||
window.toValue = (x) => JSON.parse(JSON.stringify(x))
|
window.toValue = (x) => JSON.parse(JSON.stringify(x))
|
||||||
|
|
||||||
|
|
@ -41,27 +31,45 @@ export default {
|
||||||
UserPanel,
|
UserPanel,
|
||||||
NavPanel,
|
NavPanel,
|
||||||
Notifications: defineAsyncComponent(
|
Notifications: defineAsyncComponent(
|
||||||
() => import('./components/notifications/notifications.vue'),
|
() => import('src/components/notifications/notifications.vue'),
|
||||||
),
|
),
|
||||||
InstanceSpecificPanel,
|
InstanceSpecificPanel,
|
||||||
FeaturesPanel,
|
FeaturesPanel,
|
||||||
WhoToFollowPanel,
|
WhoToFollowPanel: defineAsyncComponent(
|
||||||
ShoutPanel,
|
() =>
|
||||||
MediaModal,
|
import('src/components/who_to_follow_panel/who_to_follow_panel.vue'),
|
||||||
SideDrawer,
|
),
|
||||||
|
ShoutPanel: defineAsyncComponent(
|
||||||
|
() => import('src/components/shout_panel/shout_panel.vue'),
|
||||||
|
),
|
||||||
|
MediaModal: defineAsyncComponent(
|
||||||
|
() => import('src/components/media_modal/media_modal.vue'),
|
||||||
|
),
|
||||||
MobilePostStatusButton,
|
MobilePostStatusButton,
|
||||||
MobileNav,
|
MobileNav,
|
||||||
DesktopNav,
|
DesktopNav,
|
||||||
SettingsModal: defineAsyncComponent(
|
SettingsModal: defineAsyncComponent(
|
||||||
() => import('./components/settings_modal/settings_modal.vue'),
|
() => import('src/components/settings_modal/settings_modal.vue'),
|
||||||
),
|
),
|
||||||
UpdateNotification: defineAsyncComponent(
|
UpdateNotification: defineAsyncComponent(
|
||||||
() => import('./components/update_notification/update_notification.vue'),
|
() =>
|
||||||
|
import('src/components/update_notification/update_notification.vue'),
|
||||||
),
|
),
|
||||||
UserReportingModal,
|
PostStatusModal: defineAsyncComponent(
|
||||||
PostStatusModal,
|
() => import('src/components/post_status_modal/post_status_modal.vue'),
|
||||||
EditStatusModal,
|
),
|
||||||
StatusHistoryModal,
|
UserReportingModal: defineAsyncComponent(
|
||||||
|
() =>
|
||||||
|
import('src/components/user_reporting_modal/user_reporting_modal.vue'),
|
||||||
|
),
|
||||||
|
EditStatusModal: defineAsyncComponent(
|
||||||
|
() => import('src/components/edit_status_modal/edit_status_modal.vue'),
|
||||||
|
),
|
||||||
|
StatusHistoryModal: defineAsyncComponent(
|
||||||
|
() =>
|
||||||
|
import('src/components/status_history_modal/status_history_modal.vue'),
|
||||||
|
),
|
||||||
|
GlobalError,
|
||||||
GlobalNoticeList,
|
GlobalNoticeList,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
|
@ -149,13 +157,23 @@ export default {
|
||||||
userBackground() {
|
userBackground() {
|
||||||
return this.currentUser.background_image
|
return this.currentUser.background_image
|
||||||
},
|
},
|
||||||
|
foreignProfileBackground() {
|
||||||
|
return (
|
||||||
|
useMergedConfigStore().mergedConfig.allowForeignUserBackground &&
|
||||||
|
useInterfaceStore().foreignProfileBackground
|
||||||
|
)
|
||||||
|
},
|
||||||
instanceBackground() {
|
instanceBackground() {
|
||||||
return useMergedConfigStore().mergedConfig.hideInstanceWallpaper
|
return useMergedConfigStore().mergedConfig.hideInstanceWallpaper
|
||||||
? null
|
? null
|
||||||
: this.instanceBackgroundUrl
|
: this.instanceBackgroundUrl
|
||||||
},
|
},
|
||||||
background() {
|
background() {
|
||||||
return this.userBackground || this.instanceBackground
|
return (
|
||||||
|
this.foreignProfileBackground ||
|
||||||
|
this.userBackground ||
|
||||||
|
this.instanceBackground
|
||||||
|
)
|
||||||
},
|
},
|
||||||
bgStyle() {
|
bgStyle() {
|
||||||
if (this.background) {
|
if (this.background) {
|
||||||
|
|
|
||||||
89
src/App.scss
89
src/App.scss
|
|
@ -50,7 +50,7 @@ body {
|
||||||
// have a cursor/pointer to operate them
|
// have a cursor/pointer to operate them
|
||||||
@media (any-pointer: fine) {
|
@media (any-pointer: fine) {
|
||||||
* {
|
* {
|
||||||
scrollbar-color: var(--fg) transparent;
|
scrollbar-color: var(--icon) transparent;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
@ -130,7 +130,7 @@ body {
|
||||||
}
|
}
|
||||||
// Body should have background to scrollbar otherwise it will use white (body color?)
|
// Body should have background to scrollbar otherwise it will use white (body color?)
|
||||||
html {
|
html {
|
||||||
scrollbar-color: var(--fg) var(--wallpaper);
|
scrollbar-color: var(--icon) var(--wallpaper);
|
||||||
background: var(--wallpaper);
|
background: var(--wallpaper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -144,6 +144,25 @@ h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: var(--background);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--roundness);
|
||||||
|
padding: 0 0.2em;
|
||||||
|
|
||||||
|
& pre,
|
||||||
|
pre & {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pre {
|
||||||
|
white-space: pre;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.iconLetter {
|
.iconLetter {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -200,6 +219,7 @@ nav {
|
||||||
background-color: var(--wallpaper);
|
background-color: var(--wallpaper);
|
||||||
background-image: var(--body-background-image);
|
background-image: var(--body-background-image);
|
||||||
background-position: 50%;
|
background-position: 50%;
|
||||||
|
transition: background-image 1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.underlay {
|
.underlay {
|
||||||
|
|
@ -410,6 +430,14 @@ nav {
|
||||||
button:not(.button-default) {
|
button:not(.button-default) {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
|
text-align: initial;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
display: inline;
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
|
|
@ -417,45 +445,6 @@ nav {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-item {
|
|
||||||
border-color: var(--border);
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 0;
|
|
||||||
border-top-width: 1px;
|
|
||||||
|
|
||||||
&.-active,
|
|
||||||
&:hover {
|
|
||||||
border-top-width: 1px;
|
|
||||||
border-bottom-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.-active + &,
|
|
||||||
&:hover + & {
|
|
||||||
border-top-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover + .menu-item-collapsible:not(.-expanded) + &,
|
|
||||||
&.-active + .menu-item-collapsible:not(.-expanded) + & {
|
|
||||||
border-top-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[aria-expanded="true"] {
|
|
||||||
border-bottom-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-top-right-radius: var(--roundness);
|
|
||||||
border-top-left-radius: var(--roundness);
|
|
||||||
border-top-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom-right-radius: var(--roundness);
|
|
||||||
border-bottom-left-radius: var(--roundness);
|
|
||||||
border-bottom-width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item,
|
.menu-item,
|
||||||
.list-item {
|
.list-item {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -474,22 +463,6 @@ nav {
|
||||||
--__line-height: 1.5em;
|
--__line-height: 1.5em;
|
||||||
--__horizontal-gap: 0.75em;
|
--__horizontal-gap: 0.75em;
|
||||||
--__vertical-gap: 0.5em;
|
--__vertical-gap: 0.5em;
|
||||||
|
|
||||||
&.-non-interactive {
|
|
||||||
cursor: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
a,
|
|
||||||
button:not(.button-default) {
|
|
||||||
text-align: initial;
|
|
||||||
padding: 0;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
display: inline;
|
|
||||||
font-family: inherit;
|
|
||||||
line-height: unset;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-unstyled {
|
.button-unstyled {
|
||||||
|
|
@ -815,9 +788,11 @@ option {
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
|
||||||
|
p,
|
||||||
span {
|
span {
|
||||||
display: block;
|
display: block;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dismiss {
|
.dismiss {
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,10 @@
|
||||||
>
|
>
|
||||||
<user-panel />
|
<user-panel />
|
||||||
<template v-if="layoutType !== 'mobile'">
|
<template v-if="layoutType !== 'mobile'">
|
||||||
<nav-panel />
|
<NavPanel />
|
||||||
<instance-specific-panel v-if="showInstanceSpecificPanel" />
|
<InstanceSpecificPanel v-if="showInstanceSpecificPanel" />
|
||||||
<features-panel v-if="!currentUser && showFeaturesPanel" />
|
<FeaturesPanel v-if="!currentUser && showFeaturesPanel" />
|
||||||
<who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
|
<WhoToFollowPanel v-if="currentUser && suggestionsEnabled" />
|
||||||
<div id="notifs-sidebar" />
|
<div id="notifs-sidebar" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -73,6 +73,7 @@
|
||||||
<StatusHistoryModal v-if="editingAvailable" />
|
<StatusHistoryModal v-if="editingAvailable" />
|
||||||
<SettingsModal :class="layoutModalClass" />
|
<SettingsModal :class="layoutModalClass" />
|
||||||
<UpdateNotification />
|
<UpdateNotification />
|
||||||
|
<GlobalError />
|
||||||
<GlobalNoticeList />
|
<GlobalNoticeList />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
491
src/api/admin.js
Normal file
491
src/api/admin.js
Normal file
|
|
@ -0,0 +1,491 @@
|
||||||
|
import { promisedRequest } from './helpers.js'
|
||||||
|
|
||||||
|
const REPORTS = '/api/v1/pleroma/admin/reports'
|
||||||
|
const CONFIG_URL = '/api/v1/pleroma/admin/config'
|
||||||
|
const DESCRIPTIONS_URL = '/api/v1/pleroma/admin/config/descriptions'
|
||||||
|
|
||||||
|
const ANNOUNCEMENTS_URL = (id = '') =>
|
||||||
|
`/api/v1/pleroma/admin/announcements/${id}`
|
||||||
|
|
||||||
|
const FRONTENDS_URL = '/api/v1/pleroma/admin/frontends'
|
||||||
|
const FRONTENDS_INSTALL_URL = '/api/v1/pleroma/admin/frontends/install'
|
||||||
|
|
||||||
|
const USERS_URL = (nickname = '') => `/api/v1/pleroma/admin/users/${nickname}`
|
||||||
|
const USERS_URL_LIST = ({
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
filters = {},
|
||||||
|
query = '',
|
||||||
|
name = '',
|
||||||
|
email = '',
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
local = false,
|
||||||
|
external = false,
|
||||||
|
active = false,
|
||||||
|
needApproval = false,
|
||||||
|
unconfirmed = false,
|
||||||
|
deactivated = false,
|
||||||
|
isAdmin = true,
|
||||||
|
isModerator = true,
|
||||||
|
} = filters
|
||||||
|
const filters_str = [
|
||||||
|
local && 'local',
|
||||||
|
external && 'external',
|
||||||
|
active && 'active',
|
||||||
|
needApproval && 'need_approval',
|
||||||
|
unconfirmed && 'unconfirmed',
|
||||||
|
deactivated && 'deactivated',
|
||||||
|
isAdmin && 'is_admin',
|
||||||
|
isModerator && 'is_moderator',
|
||||||
|
]
|
||||||
|
.filter((x) => x)
|
||||||
|
.join(',')
|
||||||
|
return `/api/v1/pleroma/admin/users?page=${page}&page_size=${pageSize}&filters=${filters_str}&query=${query}&name=${name}&email=${email}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const TAG_USER_URL = '/api/pleroma/admin/users/tag'
|
||||||
|
|
||||||
|
const PERMISSION_GROUP_URL = (right) =>
|
||||||
|
`/api/pleroma/admin/users/permission_group/${right}`
|
||||||
|
const ACTIVATE_USERS_URL = '/api/pleroma/admin/users/activate'
|
||||||
|
const DEACTIVATE_USERS_URL = '/api/pleroma/admin/users/deactivate'
|
||||||
|
const SUGGEST_USERS_URL = '/api/pleroma/admin/users/suggest'
|
||||||
|
const UNSUGGEST_USERS_URL = '/api/pleroma/admin/users/unsuggest'
|
||||||
|
const APPROVE_USERS_URL = '/api/v1/pleroma/admin/users/approve'
|
||||||
|
const CONFIRM_USERS_URL = '/api/v1/pleroma/admin/users/confirm_email'
|
||||||
|
const RESEND_CONFIRMATION_EMAIL_URL =
|
||||||
|
'/api/v1/pleroma/admin/users/resend_confirmation_email'
|
||||||
|
const LIST_STATUSES_URL = ({ id, page, pageSize, godmode, withReblogs }) =>
|
||||||
|
`/api/v1/pleroma/admin/users/${id}/statuses?page_size=${pageSize}&page=${page}&godmode=${godmode}&with_reblogs=${withReblogs}`
|
||||||
|
const CHANGE_STATUS_SCOPE_URL = (id) => `/api/v1/pleroma/admin/statuses/${id}`
|
||||||
|
const REQUIRE_PASSWORD_CHANGE_URL =
|
||||||
|
'/api/v1/pleroma/admin/users/force_password_reset'
|
||||||
|
|
||||||
|
const DISABLE_MFA_URL = '/api/v1/pleroma/admin/users/disable_mfa'
|
||||||
|
const EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji'
|
||||||
|
const EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import'
|
||||||
|
const EMOJI_PACK_URL = (name) => `/api/v1/pleroma/emoji/pack?name=${name}`
|
||||||
|
const EMOJI_PACKS_DL_REMOTE_URL = '/api/v1/pleroma/emoji/packs/download'
|
||||||
|
const EMOJI_PACKS_DL_REMOTE_ZIP_URL = '/api/v1/pleroma/emoji/packs/download_zip'
|
||||||
|
const EMOJI_PACKS_LS_REMOTE_URL = (url, page, pageSize) =>
|
||||||
|
`/api/v1/pleroma/emoji/packs/remote?url=${url}&page=${page}&page_size=${pageSize}`
|
||||||
|
const EMOJI_UPDATE_FILE_URL = (name) =>
|
||||||
|
`/api/v1/pleroma/emoji/packs/files?name=${name}`
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
export const setUsersTags = ({
|
||||||
|
tags,
|
||||||
|
credentials,
|
||||||
|
value,
|
||||||
|
screen_names: nicknames,
|
||||||
|
}) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: TAG_USER_URL,
|
||||||
|
method: value ? 'PUT' : 'DELETE',
|
||||||
|
credentials,
|
||||||
|
payload: {
|
||||||
|
nicknames,
|
||||||
|
tags,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const setUsersRight = ({
|
||||||
|
right,
|
||||||
|
credentials,
|
||||||
|
value,
|
||||||
|
screen_names: nicknames,
|
||||||
|
}) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: PERMISSION_GROUP_URL(right),
|
||||||
|
method: value ? 'POST' : 'DELETE',
|
||||||
|
credentials,
|
||||||
|
payload: {
|
||||||
|
nicknames,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const setUsersActivationStatus = ({
|
||||||
|
credentials,
|
||||||
|
screen_names: nicknames,
|
||||||
|
value,
|
||||||
|
}) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: value ? ACTIVATE_USERS_URL : DEACTIVATE_USERS_URL,
|
||||||
|
method: 'PATCH',
|
||||||
|
credentials,
|
||||||
|
payload: {
|
||||||
|
nicknames,
|
||||||
|
},
|
||||||
|
}).then((response) => response.users)
|
||||||
|
|
||||||
|
export const setUsersApprovalStatus = ({
|
||||||
|
credentials,
|
||||||
|
screen_names: nicknames,
|
||||||
|
}) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: APPROVE_USERS_URL,
|
||||||
|
method: 'PATCH',
|
||||||
|
credentials,
|
||||||
|
payload: {
|
||||||
|
nicknames,
|
||||||
|
},
|
||||||
|
}).then((response) => response.users)
|
||||||
|
|
||||||
|
export const setUsersConfirmationStatus = ({
|
||||||
|
credentials,
|
||||||
|
screen_names: nicknames,
|
||||||
|
}) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: CONFIRM_USERS_URL,
|
||||||
|
method: 'PATCH',
|
||||||
|
credentials,
|
||||||
|
payload: {
|
||||||
|
nicknames,
|
||||||
|
},
|
||||||
|
}).then((response) => response.users)
|
||||||
|
|
||||||
|
export const setUsersSuggestionStatus = ({
|
||||||
|
credentials,
|
||||||
|
screen_names: nicknames,
|
||||||
|
value,
|
||||||
|
}) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: value ? SUGGEST_USERS_URL : UNSUGGEST_USERS_URL,
|
||||||
|
method: 'PATCH',
|
||||||
|
credentials,
|
||||||
|
payload: {
|
||||||
|
nicknames,
|
||||||
|
},
|
||||||
|
}).then((response) => response.users)
|
||||||
|
|
||||||
|
export const getUserData = ({ credentials, screen_name: nickname }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: USERS_URL(nickname),
|
||||||
|
method: 'GET',
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const deleteAccounts = ({ credentials, screen_names: nicknames }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: USERS_URL(),
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials,
|
||||||
|
payload: {
|
||||||
|
nicknames,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getAnnouncements = ({ id, credentials }) =>
|
||||||
|
promisedRequest({ url: ANNOUNCEMENTS_URL(id), credentials })
|
||||||
|
|
||||||
|
// the reported list is hardly useful because standards are for dating i guess,
|
||||||
|
// so make sure to fetchIfMissing right afterward using this call
|
||||||
|
export const listUsers = ({ opts, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: USERS_URL_LIST(opts),
|
||||||
|
credentials,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const resendConfirmationEmail = ({
|
||||||
|
screen_names: nicknames,
|
||||||
|
credentials,
|
||||||
|
}) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: RESEND_CONFIRMATION_EMAIL_URL,
|
||||||
|
credentials,
|
||||||
|
method: 'PATCH',
|
||||||
|
payload: {
|
||||||
|
nicknames,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const requirePasswordChange = ({
|
||||||
|
screen_names: nicknames,
|
||||||
|
credentials,
|
||||||
|
}) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: REQUIRE_PASSWORD_CHANGE_URL,
|
||||||
|
credentials,
|
||||||
|
method: 'PATCH',
|
||||||
|
payload: {
|
||||||
|
nicknames,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const disableMFA = ({ screen_name: nickname, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: DISABLE_MFA_URL,
|
||||||
|
credentials,
|
||||||
|
method: 'PUT',
|
||||||
|
payload: {
|
||||||
|
nickname,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const listStatuses = ({ opts, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: LIST_STATUSES_URL(opts),
|
||||||
|
credentials,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const changeStatusScope = ({
|
||||||
|
opts: { id, sensitive, visibility },
|
||||||
|
credentials,
|
||||||
|
}) => {
|
||||||
|
var payload = {}
|
||||||
|
if (typeof sensitive !== 'undefined') {
|
||||||
|
payload['sensitive'] = sensitive
|
||||||
|
}
|
||||||
|
if (typeof visibility !== 'undefined') {
|
||||||
|
payload['visibility'] = visibility
|
||||||
|
}
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: CHANGE_STATUS_SCOPE_URL(id),
|
||||||
|
credentials,
|
||||||
|
method: 'PUT',
|
||||||
|
payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const announcementToPayload = ({
|
||||||
|
content,
|
||||||
|
startsAt,
|
||||||
|
endsAt,
|
||||||
|
allDay,
|
||||||
|
}) => {
|
||||||
|
const payload = { content }
|
||||||
|
|
||||||
|
if (typeof startsAt !== 'undefined') {
|
||||||
|
payload.starts_at = startsAt ? new Date(startsAt).toISOString() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof endsAt !== 'undefined') {
|
||||||
|
payload.ends_at = endsAt ? new Date(endsAt).toISOString() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof allDay !== 'undefined') {
|
||||||
|
payload.all_day = allDay
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
export const postAnnouncement = ({
|
||||||
|
credentials,
|
||||||
|
content,
|
||||||
|
startsAt,
|
||||||
|
endsAt,
|
||||||
|
allDay,
|
||||||
|
}) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: ANNOUNCEMENTS_URL(),
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
payload: announcementToPayload({ content, startsAt, endsAt, allDay }),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const editAnnouncement = ({
|
||||||
|
id,
|
||||||
|
credentials,
|
||||||
|
content,
|
||||||
|
startsAt,
|
||||||
|
endsAt,
|
||||||
|
allDay,
|
||||||
|
}) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: ANNOUNCEMENTS_URL(id),
|
||||||
|
credentials,
|
||||||
|
method: 'PATCH',
|
||||||
|
payload: announcementToPayload({ content, startsAt, endsAt, allDay }),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const deleteAnnouncement = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: ANNOUNCEMENTS_URL(id),
|
||||||
|
credentials,
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const setReportState = ({ id, state, credentials }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: REPORTS,
|
||||||
|
credentials,
|
||||||
|
method: 'PATCH',
|
||||||
|
payload: {
|
||||||
|
reports: [
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
state,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getInstanceDBConfig = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: CONFIG_URL,
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getInstanceConfigDescriptions = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: DESCRIPTIONS_URL,
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getAvailableFrontends = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: FRONTENDS_URL,
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const pushInstanceDBConfig = ({ credentials, payload }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: CONFIG_URL,
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
payload,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const installFrontend = ({ credentials, payload }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: FRONTENDS_INSTALL_URL,
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
payload,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Emoji packs
|
||||||
|
export const deleteEmojiPack = ({ name }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: EMOJI_PACK_URL(name),
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const reloadEmoji = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: EMOJI_RELOAD_URL,
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const importEmojiFromFS = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: EMOJI_IMPORT_FS_URL,
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const createEmojiPack = ({ name, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: EMOJI_PACK_URL(name),
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const listRemoteEmojiPacks = ({
|
||||||
|
instance,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
credentials,
|
||||||
|
}) => {
|
||||||
|
if (!instance.startsWith('http')) {
|
||||||
|
instance = 'https://' + instance
|
||||||
|
}
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: EMOJI_PACKS_LS_REMOTE_URL(instance, page, pageSize),
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const downloadRemoteEmojiPack = ({
|
||||||
|
instance,
|
||||||
|
packName,
|
||||||
|
as,
|
||||||
|
credentials,
|
||||||
|
}) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: EMOJI_PACKS_DL_REMOTE_URL,
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
payload: {
|
||||||
|
url: instance,
|
||||||
|
name: packName,
|
||||||
|
as,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const downloadRemoteEmojiPackZIP = ({
|
||||||
|
url,
|
||||||
|
packName,
|
||||||
|
file,
|
||||||
|
credentials,
|
||||||
|
}) => {
|
||||||
|
const data = new FormData()
|
||||||
|
if (file) data.set('file', file)
|
||||||
|
if (url) data.set('url', url)
|
||||||
|
data.set('name', packName)
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: EMOJI_PACKS_DL_REMOTE_ZIP_URL,
|
||||||
|
method: 'POST',
|
||||||
|
formData: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const saveEmojiPackMetadata = ({ name, newData, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: EMOJI_PACK_URL(name),
|
||||||
|
credentials,
|
||||||
|
method: 'PATCH',
|
||||||
|
payload: { metadata: newData },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const addNewEmojiFile = ({ packName, file, shortcode, filename }) => {
|
||||||
|
const data = new FormData()
|
||||||
|
if (filename.trim() !== '') {
|
||||||
|
data.set('filename', filename)
|
||||||
|
}
|
||||||
|
if (shortcode.trim() !== '') {
|
||||||
|
data.set('shortcode', shortcode)
|
||||||
|
}
|
||||||
|
data.set('file', file)
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: EMOJI_UPDATE_FILE_URL(packName),
|
||||||
|
method: 'POST',
|
||||||
|
formData: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateEmojiFile = ({
|
||||||
|
packName,
|
||||||
|
shortcode,
|
||||||
|
newShortcode,
|
||||||
|
newFilename,
|
||||||
|
credentials,
|
||||||
|
force,
|
||||||
|
}) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: EMOJI_UPDATE_FILE_URL(packName),
|
||||||
|
credentials,
|
||||||
|
method: 'PATCH',
|
||||||
|
payload: {
|
||||||
|
shortcode,
|
||||||
|
new_shortcode: newShortcode,
|
||||||
|
new_filename: newFilename,
|
||||||
|
force,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const deleteEmojiFile = ({ packName, shortcode }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: `${EMOJI_UPDATE_FILE_URL(packName)}&shortcode=${shortcode}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
87
src/api/chats.js
Normal file
87
src/api/chats.js
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { paramsString, promisedRequest } from './helpers.js'
|
||||||
|
|
||||||
|
import { parseChat } from 'src/services/entity_normalizer/entity_normalizer.service.js'
|
||||||
|
|
||||||
|
const PLEROMA_CHATS_URL = '/api/v1/pleroma/chats'
|
||||||
|
const PLEROMA_CHAT_URL = (id) => `/api/v1/pleroma/chats/by-account-id/${id}`
|
||||||
|
const PLEROMA_CHAT_MESSAGES_URL = (id, { maxId, sinceId, limit }) =>
|
||||||
|
`/api/v1/pleroma/chats/${id}/messages${paramsString({ maxId, sinceId, limit })}`
|
||||||
|
const PLEROMA_CHAT_READ_URL = (id) => `/api/v1/pleroma/chats/${id}/read`
|
||||||
|
const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) =>
|
||||||
|
`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
|
||||||
|
|
||||||
|
export const chats = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: PLEROMA_CHATS_URL,
|
||||||
|
credentials,
|
||||||
|
}).then(({ data }) => ({
|
||||||
|
chatList: data.map(parseChat).filter((c) => c),
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const getOrCreateChat = ({ accountId, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: PLEROMA_CHAT_URL(accountId),
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const chatMessages = ({
|
||||||
|
id,
|
||||||
|
credentials,
|
||||||
|
maxId,
|
||||||
|
sinceId,
|
||||||
|
limit = 20,
|
||||||
|
}) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: PLEROMA_CHAT_MESSAGES_URL(id, { maxId, sinceId, limit }),
|
||||||
|
method: 'GET',
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sendChatMessage = ({
|
||||||
|
id,
|
||||||
|
content,
|
||||||
|
mediaId = null,
|
||||||
|
idempotencyKey,
|
||||||
|
credentials,
|
||||||
|
}) => {
|
||||||
|
const payload = {
|
||||||
|
content,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaId) {
|
||||||
|
payload.media_id = mediaId
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {}
|
||||||
|
|
||||||
|
if (idempotencyKey) {
|
||||||
|
headers['idempotency-key'] = idempotencyKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: PLEROMA_CHAT_MESSAGES_URL(id),
|
||||||
|
method: 'POST',
|
||||||
|
payload,
|
||||||
|
credentials,
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const readChat = ({ id, lastReadId, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: PLEROMA_CHAT_READ_URL(id),
|
||||||
|
method: 'POST',
|
||||||
|
payload: {
|
||||||
|
last_read_id: lastReadId,
|
||||||
|
},
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const deleteChatMessage = ({ chatId, messageId, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: PLEROMA_DELETE_CHAT_MESSAGE_URL(chatId, messageId),
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
140
src/api/helpers.js
Normal file
140
src/api/helpers.js
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
import { snakeCase } from 'lodash'
|
||||||
|
|
||||||
|
import { StatusCodeError } from 'src/services/errors/errors'
|
||||||
|
|
||||||
|
export const paramsString = (params = {}) => {
|
||||||
|
if (params == null || params === undefined) return ''
|
||||||
|
|
||||||
|
if (typeof params !== 'object' || Array.isArray(params)) {
|
||||||
|
throw new Error('Params are not an object!')
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = (() => {
|
||||||
|
if (params instanceof Map) {
|
||||||
|
return params.entries()
|
||||||
|
} else {
|
||||||
|
return Object.entries(params)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
if (entries.length === 0) return ''
|
||||||
|
|
||||||
|
const arrays = []
|
||||||
|
const nonArrays = []
|
||||||
|
|
||||||
|
entries.forEach(([k, v]) => {
|
||||||
|
if (v == null) return // Drop nulls
|
||||||
|
if (
|
||||||
|
(typeof v === 'object' && !Array.isArray(v)) ||
|
||||||
|
typeof v === 'function'
|
||||||
|
) {
|
||||||
|
throw new Error('Param cannot be non-primitive!')
|
||||||
|
}
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
arrays.push([k, v])
|
||||||
|
} else {
|
||||||
|
nonArrays.push([k, v])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
arrays.forEach(([k, array]) => {
|
||||||
|
array.forEach((v) => {
|
||||||
|
if (
|
||||||
|
typeof v === 'object' ||
|
||||||
|
typeof v === 'function' ||
|
||||||
|
typeof v === 'undefined'
|
||||||
|
)
|
||||||
|
throw new Error('Array param cannot contain non-primitives!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
'?' +
|
||||||
|
[
|
||||||
|
...nonArrays.map(([k, v]) => [snakeCase(k), v]),
|
||||||
|
// turning [a,[1,2,3]] into [[a[],1],[a[],2],[a[],3]]
|
||||||
|
...arrays.reduce(
|
||||||
|
(acc, [k, arrayValue]) => [
|
||||||
|
...acc,
|
||||||
|
...arrayValue.map((v) => [snakeCase(k) + '[]', v]),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.map(([k, v]) => `${k}=${window.encodeURIComponent(v)}`)
|
||||||
|
.join('&')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const promisedRequest = async ({
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
payload,
|
||||||
|
formData,
|
||||||
|
cache,
|
||||||
|
credentials,
|
||||||
|
headers = {},
|
||||||
|
}) => {
|
||||||
|
const options = {
|
||||||
|
method,
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData) {
|
||||||
|
options.headers['Content-Type'] = 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cache) {
|
||||||
|
options.cache = cache
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData || payload) {
|
||||||
|
options.body = formData || JSON.stringify(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credentials) {
|
||||||
|
options.headers = {
|
||||||
|
...options.headers,
|
||||||
|
...authHeaders(credentials),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, options)
|
||||||
|
const data = await (async () => {
|
||||||
|
const [contentType] = response.headers
|
||||||
|
.get('content-type')
|
||||||
|
.split(';')
|
||||||
|
.map((x) => x.toLowerCase().trim())
|
||||||
|
const contentLength = parseInt(response.headers.get('content-length'))
|
||||||
|
if (contentLength === 0) return null
|
||||||
|
|
||||||
|
switch (contentType) {
|
||||||
|
case 'text/plain':
|
||||||
|
return await response.text()
|
||||||
|
case 'application/json':
|
||||||
|
return await response.json()
|
||||||
|
default:
|
||||||
|
return await response.bytes()
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
const { ok, status } = response
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
return { response, status, data }
|
||||||
|
} else {
|
||||||
|
throw new StatusCodeError(response.status, data, { url, options }, response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const authHeaders = (accessToken) => {
|
||||||
|
if (accessToken) {
|
||||||
|
return { Authorization: `Bearer ${accessToken}` }
|
||||||
|
} else {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/api/mfa.js
Normal file
45
src/api/mfa.js
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { promisedRequest } from './helpers.js'
|
||||||
|
|
||||||
|
export const verifyOTPCode = ({
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
instance,
|
||||||
|
mfaToken,
|
||||||
|
code,
|
||||||
|
}) => {
|
||||||
|
const formData = new window.FormData()
|
||||||
|
|
||||||
|
formData.append('client_id', clientId)
|
||||||
|
formData.append('client_secret', clientSecret)
|
||||||
|
formData.append('mfa_token', mfaToken)
|
||||||
|
formData.append('code', code)
|
||||||
|
formData.append('challenge_type', 'totp')
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: '/oauth/mfa/challenge',
|
||||||
|
method: 'POST',
|
||||||
|
formData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const verifyRecoveryCode = ({
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
instance,
|
||||||
|
mfaToken,
|
||||||
|
code,
|
||||||
|
}) => {
|
||||||
|
const formData = new window.FormData()
|
||||||
|
|
||||||
|
formData.append('client_id', clientId)
|
||||||
|
formData.append('client_secret', clientSecret)
|
||||||
|
formData.append('mfa_token', mfaToken)
|
||||||
|
formData.append('code', code)
|
||||||
|
formData.append('challenge_type', 'recovery')
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: `${instance}/oauth/mfa/challenge`,
|
||||||
|
method: 'POST',
|
||||||
|
formData,
|
||||||
|
})
|
||||||
|
}
|
||||||
146
src/api/oauth.js
Normal file
146
src/api/oauth.js
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
import { paramsString, promisedRequest } from './helpers.js'
|
||||||
|
|
||||||
|
const REDIRECT_URI = `${window.location.origin}/oauth-callback`
|
||||||
|
|
||||||
|
export const MASTODON_APP_VERIFY_URL = '/api/v1/apps/verify_credentials'
|
||||||
|
export const MASTODON_APP_URL = '/api/v1/apps'
|
||||||
|
export const OAUTH_TOKEN_URL = '/oauth/token'
|
||||||
|
export const OAUTH_MFA_CHALLENGE_URL = '/oauth/mfa/challenge'
|
||||||
|
export const OAUTH_REVOKE_URL = '/oauth/revoke'
|
||||||
|
|
||||||
|
export const createApp = () => {
|
||||||
|
const formData = new window.FormData()
|
||||||
|
|
||||||
|
formData.append('client_name', 'PleromaFE')
|
||||||
|
formData.append('website', 'https://pleroma.social')
|
||||||
|
formData.append('redirect_uris', REDIRECT_URI)
|
||||||
|
formData.append('scopes', 'read write follow push admin')
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
method: 'POST',
|
||||||
|
url: MASTODON_APP_URL,
|
||||||
|
formData,
|
||||||
|
}).then(({ data, ...rest }) => ({
|
||||||
|
...rest,
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
clientId: data.client_id,
|
||||||
|
clientSecret: data.client_secret,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const verifyAppToken = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_APP_VERIFY_URL,
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getLoginUrl = ({ instance, clientId }) => {
|
||||||
|
const data = {
|
||||||
|
responseType: 'code',
|
||||||
|
clientId,
|
||||||
|
redirectUri: REDIRECT_URI,
|
||||||
|
scope: 'read write follow push admin',
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${instance}/oauth/authorize${paramsString(data)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTokenWithCredentials = ({
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}) => {
|
||||||
|
const formData = new window.FormData()
|
||||||
|
|
||||||
|
formData.append('client_id', clientId)
|
||||||
|
formData.append('client_secret', clientSecret)
|
||||||
|
formData.append('grant_type', 'password')
|
||||||
|
formData.append('username', username)
|
||||||
|
formData.append('password', password)
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: OAUTH_TOKEN_URL,
|
||||||
|
method: 'POST',
|
||||||
|
formData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getToken = ({ clientId, clientSecret, code }) => {
|
||||||
|
const formData = new window.FormData()
|
||||||
|
|
||||||
|
formData.append('client_id', clientId)
|
||||||
|
formData.append('client_secret', clientSecret)
|
||||||
|
formData.append('grant_type', 'authorization_code')
|
||||||
|
formData.append('code', code)
|
||||||
|
formData.append('redirect_uri', `${window.location.origin}/oauth-callback`)
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: OAUTH_TOKEN_URL,
|
||||||
|
method: 'POST',
|
||||||
|
formData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getClientToken = ({ clientId, clientSecret }) => {
|
||||||
|
const formData = new window.FormData()
|
||||||
|
|
||||||
|
formData.append('client_id', clientId)
|
||||||
|
formData.append('client_secret', clientSecret)
|
||||||
|
formData.append('grant_type', 'client_credentials')
|
||||||
|
formData.append('redirect_uri', `${window.location.origin}/oauth-callback`)
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: OAUTH_TOKEN_URL,
|
||||||
|
method: 'POST',
|
||||||
|
formData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const verifyOTPCode = ({ app, mfaToken, code }) => {
|
||||||
|
const formData = new window.FormData()
|
||||||
|
|
||||||
|
formData.append('client_id', app.client_id)
|
||||||
|
formData.append('client_secret', app.client_secret)
|
||||||
|
formData.append('mfa_token', mfaToken)
|
||||||
|
formData.append('code', code)
|
||||||
|
formData.append('challenge_type', 'totp')
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: OAUTH_MFA_CHALLENGE_URL,
|
||||||
|
method: 'POST',
|
||||||
|
formData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const verifyRecoveryCode = ({ app, mfaToken, code }) => {
|
||||||
|
const formData = new window.FormData()
|
||||||
|
|
||||||
|
formData.append('client_id', app.client_id)
|
||||||
|
formData.append('client_secret', app.client_secret)
|
||||||
|
formData.append('mfa_token', mfaToken)
|
||||||
|
formData.append('code', code)
|
||||||
|
formData.append('challenge_type', 'recovery')
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: OAUTH_MFA_CHALLENGE_URL,
|
||||||
|
method: 'POST',
|
||||||
|
formData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const revokeToken = ({ app, token }) => {
|
||||||
|
const formData = new window.FormData()
|
||||||
|
|
||||||
|
formData.append('client_id', app.clientId)
|
||||||
|
formData.append('client_secret', app.clientSecret)
|
||||||
|
formData.append('token', token)
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: OAUTH_REVOKE_URL,
|
||||||
|
method: 'POST',
|
||||||
|
formData,
|
||||||
|
})
|
||||||
|
}
|
||||||
279
src/api/public.js
Normal file
279
src/api/public.js
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
import { paramsString, promisedRequest } from './helpers.js'
|
||||||
|
import { MASTODON_USER_TIMELINE_URL } from './timelines.js'
|
||||||
|
|
||||||
|
import {
|
||||||
|
parseSource,
|
||||||
|
parseStatus,
|
||||||
|
parseUser,
|
||||||
|
} from 'src/services/entity_normalizer/entity_normalizer.service.js'
|
||||||
|
|
||||||
|
const MASTODON_SUGGESTIONS_URL = '/api/v1/suggestions'
|
||||||
|
const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
|
||||||
|
const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
|
||||||
|
const MASTODON_PASSWORD_RESET_URL = ({ email }) =>
|
||||||
|
`/auth/password${paramsString({ email })}`
|
||||||
|
|
||||||
|
const MASTODON_FOLLOWING_URL = (
|
||||||
|
id,
|
||||||
|
{ minId, maxId, sinceId, limit, withRelationships },
|
||||||
|
) =>
|
||||||
|
`/api/v1/accounts/${id}/following${paramsString({ minId, maxId, sinceId, limit, withRelationships })}`
|
||||||
|
const MASTODON_FOLLOWERS_URL = (
|
||||||
|
id,
|
||||||
|
{ minId, maxId, sinceId, limit, withRelationships },
|
||||||
|
) =>
|
||||||
|
`/api/v1/accounts/${id}/followers${paramsString({ minId, maxId, sinceId, limit, withRelationships })}`
|
||||||
|
|
||||||
|
export const MASTODON_STATUS_URL = (id) => `/api/v1/statuses/${id}`
|
||||||
|
const MASTODON_STATUS_CONTEXT_URL = (id) => `/api/v1/statuses/${id}/context`
|
||||||
|
const MASTODON_STATUS_SOURCE_URL = (id) => `/api/v1/statuses/${id}/source`
|
||||||
|
const MASTODON_STATUS_HISTORY_URL = (id) => `/api/v1/statuses/${id}/history`
|
||||||
|
const MASTODON_USER_URL = '/api/v1/accounts'
|
||||||
|
const MASTODON_USER_LOOKUP_URL = ({ acct }) =>
|
||||||
|
`/api/v1/accounts/lookup${paramsString({ acct })}`
|
||||||
|
const MASTODON_POLL_URL = (id = '') => `/api/v1/polls/${id}`
|
||||||
|
const MASTODON_STATUS_FAVORITEDBY_URL = (id) =>
|
||||||
|
`/api/v1/statuses/${id}/favourited_by`
|
||||||
|
const MASTODON_STATUS_REBLOGGEDBY_URL = (id) =>
|
||||||
|
`/api/v1/statuses/${id}/reblogged_by`
|
||||||
|
const MASTODON_SEARCH_2 = ({
|
||||||
|
q,
|
||||||
|
resolve,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
following,
|
||||||
|
type,
|
||||||
|
withRelationships,
|
||||||
|
accountId,
|
||||||
|
excludeUnreviewed,
|
||||||
|
}) =>
|
||||||
|
`/api/v2/search${paramsString({ q, resolve, limit, offset, following, type, withRelationships, accountId, excludeUnreviewed })}`
|
||||||
|
const MASTODON_USER_SEARCH_URL = ({ q, resolve }) =>
|
||||||
|
`/api/v1/accounts/search${paramsString({ q, resolve })}`
|
||||||
|
const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
|
||||||
|
const PLEROMA_EMOJI_REACTIONS_URL = (id) =>
|
||||||
|
`/api/v1/pleroma/statuses/${id}/reactions`
|
||||||
|
const PLEROMA_SCROBBLES_URL = (id, { maxId, sinceId, minId, limit, offset }) =>
|
||||||
|
`/api/v1/pleroma/accounts/${id}/scrobbles${paramsString({ maxId, sinceId, minId, limit, offset })}`
|
||||||
|
|
||||||
|
const EMOJI_PACKS_URL = (page, pageSize) =>
|
||||||
|
`/api/v1/pleroma/emoji/packs${paramsString({ page, pageSize })}`
|
||||||
|
|
||||||
|
// Params needed:
|
||||||
|
// nickname
|
||||||
|
// email
|
||||||
|
// fullname
|
||||||
|
// password
|
||||||
|
// password_confirm
|
||||||
|
//
|
||||||
|
// Optional
|
||||||
|
// bio
|
||||||
|
// homepage
|
||||||
|
// location
|
||||||
|
// token
|
||||||
|
// language
|
||||||
|
export const register = ({ params, credentials }) => {
|
||||||
|
const { nickname, ...rest } = params
|
||||||
|
return promisedRequest({
|
||||||
|
url: MASTODON_REGISTRATION_URL,
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
payload: {
|
||||||
|
nickname,
|
||||||
|
locale: 'en_US',
|
||||||
|
agreement: true,
|
||||||
|
...rest,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCaptcha = () =>
|
||||||
|
promisedRequest({
|
||||||
|
url: '/api/pleroma/captcha',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const fetchUser = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: `${MASTODON_USER_URL}/${id}`,
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) }))
|
||||||
|
|
||||||
|
export const fetchUserByName = ({ name, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_USER_LOOKUP_URL({ acct: name }),
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
.then(({ data }) => data.id)
|
||||||
|
.catch((error) => {
|
||||||
|
if (error && error.statusCode === 404) {
|
||||||
|
// Either the backend does not support lookup endpoint,
|
||||||
|
// or there is no user with such name. Fallback and treat name as id.
|
||||||
|
return name
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((id) => fetchUser({ id, credentials }))
|
||||||
|
|
||||||
|
export const fetchFriends = ({ id, maxId, sinceId, limit = 20, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_FOLLOWING_URL(id, { maxId, sinceId, limit }),
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) }))
|
||||||
|
|
||||||
|
export const fetchFollowers = ({
|
||||||
|
id,
|
||||||
|
maxId,
|
||||||
|
sinceId,
|
||||||
|
limit = 20,
|
||||||
|
credentials,
|
||||||
|
}) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_FOLLOWERS_URL(id, {
|
||||||
|
maxId,
|
||||||
|
sinceId,
|
||||||
|
limit,
|
||||||
|
withRelationships: true,
|
||||||
|
}),
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) }))
|
||||||
|
|
||||||
|
export const fetchConversation = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_STATUS_CONTEXT_URL(id),
|
||||||
|
credentials,
|
||||||
|
}).then((result) => ({
|
||||||
|
...result,
|
||||||
|
data: {
|
||||||
|
...result.data,
|
||||||
|
ancestors: result.data.ancestors.map(parseStatus),
|
||||||
|
descendants: result.data.descendants.map(parseStatus),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const fetchStatus = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_STATUS_URL(id),
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||||
|
|
||||||
|
export const fetchStatusSource = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_STATUS_SOURCE_URL(id),
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseSource(data) }))
|
||||||
|
|
||||||
|
export const fetchStatusHistory = ({ status, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_STATUS_HISTORY_URL(status.id),
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => {
|
||||||
|
return [...data].reverse().map((item) => {
|
||||||
|
item.originalStatus = status
|
||||||
|
return { ...rest, data: parseStatus(item) }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export const listEmojiPacks = ({ page, pageSize, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: EMOJI_PACKS_URL(page, pageSize),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const fetchPinnedStatuses = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_USER_TIMELINE_URL(id, { pinned: true }),
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseStatus) }))
|
||||||
|
|
||||||
|
export const verifyCredentials = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_LOGIN_URL,
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) }))
|
||||||
|
|
||||||
|
export const resetPassword = ({ email }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: MASTODON_PASSWORD_RESET_URL({ email }),
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const suggestions = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_SUGGESTIONS_URL,
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const fetchPoll = ({ pollId, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_POLL_URL(encodeURIComponent(pollId)),
|
||||||
|
method: 'GET',
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const fetchFavoritedByUsers = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_STATUS_FAVORITEDBY_URL(id),
|
||||||
|
method: 'GET',
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) }))
|
||||||
|
|
||||||
|
export const fetchRebloggedByUsers = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_STATUS_REBLOGGEDBY_URL(id),
|
||||||
|
method: 'GET',
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) }))
|
||||||
|
|
||||||
|
export const fetchEmojiReactions = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: PLEROMA_EMOJI_REACTIONS_URL(id),
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({
|
||||||
|
...rest,
|
||||||
|
data: data.map((r) => {
|
||||||
|
r.accounts = r.accounts.map(parseUser)
|
||||||
|
return r
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const searchUsers = ({ credentials, query }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_USER_SEARCH_URL({ q: query, resolve: true }),
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) }))
|
||||||
|
|
||||||
|
export const search2 = ({
|
||||||
|
credentials,
|
||||||
|
q,
|
||||||
|
resolve,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
following,
|
||||||
|
type,
|
||||||
|
}) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: MASTODON_SEARCH_2({
|
||||||
|
q,
|
||||||
|
resolve,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
following,
|
||||||
|
type,
|
||||||
|
withRelationships: true,
|
||||||
|
}),
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => {
|
||||||
|
data.accounts = data.accounts.slice(0, limit).map((u) => parseUser(u))
|
||||||
|
data.statuses = data.statuses.slice(0, limit).map((s) => parseStatus(s))
|
||||||
|
return { ...rest, data }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchKnownDomains = ({ credentials }) =>
|
||||||
|
promisedRequest({ url: MASTODON_KNOWN_DOMAIN_LIST_URL, credentials })
|
||||||
|
|
||||||
|
export const fetchScrobbles = ({ accountId, limit = 1 }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: PLEROMA_SCROBBLES_URL(accountId, { limit }),
|
||||||
|
})
|
||||||
198
src/api/timelines.js
Normal file
198
src/api/timelines.js
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
import { paramsString, promisedRequest } from './helpers.js'
|
||||||
|
|
||||||
|
import {
|
||||||
|
parseLinkHeaderPagination,
|
||||||
|
parseNotification,
|
||||||
|
parseStatus,
|
||||||
|
} from 'src/services/entity_normalizer/entity_normalizer.service.js'
|
||||||
|
|
||||||
|
const MASTODON_USER_HOME_TIMELINE_URL = ({
|
||||||
|
minId,
|
||||||
|
sinceId,
|
||||||
|
maxId,
|
||||||
|
limit,
|
||||||
|
replyVisibility,
|
||||||
|
}) =>
|
||||||
|
`/api/v1/timelines/home${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
|
||||||
|
const MASTODON_LIST_TIMELINE_URL = (
|
||||||
|
id,
|
||||||
|
{ minId, sinceId, maxId, limit, replyVisibility },
|
||||||
|
) =>
|
||||||
|
`/api/v1/timelines/list/${id}${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
|
||||||
|
const MASTODON_DIRECT_MESSAGES_TIMELINE_URL = ({
|
||||||
|
minId,
|
||||||
|
sinceId,
|
||||||
|
maxId,
|
||||||
|
limit,
|
||||||
|
replyVisibility,
|
||||||
|
}) =>
|
||||||
|
`/api/v1/timelines/direct${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
|
||||||
|
const MASTODON_PUBLIC_TIMELINE = ({
|
||||||
|
minId,
|
||||||
|
sinceId,
|
||||||
|
maxId,
|
||||||
|
limit,
|
||||||
|
replyVisibility,
|
||||||
|
local,
|
||||||
|
remote,
|
||||||
|
onlyMedia,
|
||||||
|
}) =>
|
||||||
|
`/api/v1/timelines/public${paramsString({ minId, sinceId, maxId, limit, replyVisibility, local, remote, onlyMedia })}`
|
||||||
|
const MASTODON_TAG_TIMELINE_URL = (
|
||||||
|
tag,
|
||||||
|
{ minId, sinceId, maxId, limit, replyVisibility },
|
||||||
|
) =>
|
||||||
|
`/api/v1/timelines/tag/${tag}${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
|
||||||
|
export const MASTODON_USER_TIMELINE_URL = (
|
||||||
|
id,
|
||||||
|
{ minId, sinceId, maxId, limit, replyVisibility, pinned, onlyMedia },
|
||||||
|
) =>
|
||||||
|
`/api/v1/accounts/${id}/statuses${paramsString({ minId, sinceId, maxId, limit, replyVisibility, pinned, onlyMedia })}`
|
||||||
|
const MASTODON_USER_FAVORITES_TIMELINE_URL = ({
|
||||||
|
minId,
|
||||||
|
sinceId,
|
||||||
|
maxId,
|
||||||
|
limit,
|
||||||
|
replyVisibility,
|
||||||
|
}) =>
|
||||||
|
`/api/v1/favourites${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
|
||||||
|
const MASTODON_BOOKMARK_TIMELINE_URL = ({
|
||||||
|
minId,
|
||||||
|
sinceId,
|
||||||
|
maxId,
|
||||||
|
limit,
|
||||||
|
replyVisibility,
|
||||||
|
folderId,
|
||||||
|
}) =>
|
||||||
|
`/api/v1/bookmarks${paramsString({ minId, sinceId, maxId, limit, replyVisibility, folderId })}`
|
||||||
|
const PLEROMA_STATUS_QUOTES_URL = (
|
||||||
|
id,
|
||||||
|
{ minId, sinceId, maxId, limit, replyVisibility },
|
||||||
|
) =>
|
||||||
|
`/api/v1/pleroma/statuses/${id}/quotes${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
|
||||||
|
const PLEROMA_USER_FAVORITES_TIMELINE_URL = (
|
||||||
|
id,
|
||||||
|
{ minId, sinceId, maxId, limit, replyVisibility },
|
||||||
|
) =>
|
||||||
|
`/api/v1/pleroma/accounts/${id}/favourites${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
|
||||||
|
const AKKOMA_BUBBLE_TIMELINE_URL = ({
|
||||||
|
minId,
|
||||||
|
sinceId,
|
||||||
|
maxId,
|
||||||
|
limit,
|
||||||
|
replyVisibility,
|
||||||
|
}) =>
|
||||||
|
`/api/v1/timelines/bubble${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
|
||||||
|
|
||||||
|
const MASTODON_USER_NOTIFICATIONS_URL = ({
|
||||||
|
minId,
|
||||||
|
sinceId,
|
||||||
|
maxId,
|
||||||
|
limit,
|
||||||
|
includeTypes,
|
||||||
|
replyVisibility,
|
||||||
|
}) =>
|
||||||
|
`/api/v1/notifications${paramsString({ minId, sinceId, maxId, limit, includeTypes, replyVisibility })}`
|
||||||
|
|
||||||
|
export const fetchTimeline = ({
|
||||||
|
timeline,
|
||||||
|
credentials,
|
||||||
|
sinceId,
|
||||||
|
minId,
|
||||||
|
maxId,
|
||||||
|
userId,
|
||||||
|
listId,
|
||||||
|
statusId,
|
||||||
|
tag,
|
||||||
|
withMuted,
|
||||||
|
replyVisibility = 'all',
|
||||||
|
includeTypes = [],
|
||||||
|
bookmarkFolderId,
|
||||||
|
}) => {
|
||||||
|
const timelineUrls = {
|
||||||
|
friends: MASTODON_USER_HOME_TIMELINE_URL,
|
||||||
|
public: MASTODON_PUBLIC_TIMELINE,
|
||||||
|
publicAndExternal: MASTODON_PUBLIC_TIMELINE,
|
||||||
|
dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL,
|
||||||
|
user: MASTODON_USER_TIMELINE_URL,
|
||||||
|
media: MASTODON_USER_TIMELINE_URL,
|
||||||
|
list: MASTODON_LIST_TIMELINE_URL,
|
||||||
|
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
|
||||||
|
publicFavorites: PLEROMA_USER_FAVORITES_TIMELINE_URL,
|
||||||
|
bookmarks: MASTODON_BOOKMARK_TIMELINE_URL,
|
||||||
|
bubble: AKKOMA_BUBBLE_TIMELINE_URL,
|
||||||
|
tag: MASTODON_TAG_TIMELINE_URL,
|
||||||
|
quotes: PLEROMA_STATUS_QUOTES_URL,
|
||||||
|
|
||||||
|
notifications: MASTODON_USER_NOTIFICATIONS_URL,
|
||||||
|
}
|
||||||
|
const urlFunc = timelineUrls[timeline]
|
||||||
|
|
||||||
|
const twoArgs = new Set([
|
||||||
|
'user',
|
||||||
|
'media',
|
||||||
|
'list',
|
||||||
|
'publicFavorites',
|
||||||
|
'tag',
|
||||||
|
'quotes',
|
||||||
|
])
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
minId,
|
||||||
|
sinceId,
|
||||||
|
maxId,
|
||||||
|
limit: 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = (() => {
|
||||||
|
switch (timeline) {
|
||||||
|
case 'user':
|
||||||
|
case 'media':
|
||||||
|
return userId
|
||||||
|
case 'list':
|
||||||
|
return listId
|
||||||
|
case 'quotes':
|
||||||
|
return statusId
|
||||||
|
case 'tag':
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
const isNotifications = timeline === 'notifications'
|
||||||
|
|
||||||
|
if (timeline === 'media') {
|
||||||
|
params.onlyMedia = true
|
||||||
|
}
|
||||||
|
if (timeline === 'public') {
|
||||||
|
params.local = true
|
||||||
|
}
|
||||||
|
if (timeline !== 'favorites' && timeline !== 'bookmarks') {
|
||||||
|
params.withMuted = withMuted
|
||||||
|
}
|
||||||
|
if (replyVisibility !== 'all') {
|
||||||
|
params.replyVisibility = replyVisibility
|
||||||
|
}
|
||||||
|
if (timeline === 'bookmarks' && bookmarkFolderId) {
|
||||||
|
params.folderId = bookmarkFolderId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNotifications && includeTypes.length > 0) {
|
||||||
|
params.includeTypes = includeTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = twoArgs.has(timeline) ? urlFunc(id, params) : urlFunc(params)
|
||||||
|
return promisedRequest({ url, credentials }).then((result) => {
|
||||||
|
const pagination = parseLinkHeaderPagination(
|
||||||
|
result.response.headers.get('Link'),
|
||||||
|
{
|
||||||
|
flakeId: timeline !== 'bookmarks' && timeline !== 'notifications',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
data: result.data.map(isNotifications ? parseNotification : parseStatus),
|
||||||
|
pagination,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
921
src/api/user.js
Normal file
921
src/api/user.js
Normal file
|
|
@ -0,0 +1,921 @@
|
||||||
|
import { concat, last } from 'lodash'
|
||||||
|
|
||||||
|
import { paramsString, promisedRequest } from './helpers.js'
|
||||||
|
import { fetchFriends, MASTODON_STATUS_URL } from './public.js'
|
||||||
|
|
||||||
|
import {
|
||||||
|
parseAttachment,
|
||||||
|
parseStatus,
|
||||||
|
parseUser,
|
||||||
|
} from 'src/services/entity_normalizer/entity_normalizer.service.js'
|
||||||
|
|
||||||
|
const MUTES_IMPORT_URL = '/api/pleroma/mutes_import'
|
||||||
|
const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import'
|
||||||
|
const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
|
||||||
|
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
|
||||||
|
const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
|
||||||
|
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
|
||||||
|
const MOVE_ACCOUNT_URL = '/api/pleroma/move_account'
|
||||||
|
const ALIASES_URL = '/api/pleroma/aliases'
|
||||||
|
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
|
||||||
|
const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
|
||||||
|
|
||||||
|
const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'
|
||||||
|
const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
|
||||||
|
|
||||||
|
const MFA_SETUP_OTP_URL = '/api/pleroma/accounts/mfa/setup/totp'
|
||||||
|
const MFA_CONFIRM_OTP_URL = '/api/pleroma/accounts/mfa/confirm/totp'
|
||||||
|
const MFA_DISABLE_OTP_URL = '/api/pleroma/accounts/mfa/totp'
|
||||||
|
|
||||||
|
const MASTODON_DISMISS_NOTIFICATION_URL = (id) =>
|
||||||
|
`/api/v1/notifications/${id}/dismiss`
|
||||||
|
const MASTODON_FAVORITE_URL = (id) => `/api/v1/statuses/${id}/favourite`
|
||||||
|
const MASTODON_UNFAVORITE_URL = (id) => `/api/v1/statuses/${id}/unfavourite`
|
||||||
|
const MASTODON_RETWEET_URL = (id) => `/api/v1/statuses/${id}/reblog`
|
||||||
|
const MASTODON_UNRETWEET_URL = (id) => `/api/v1/statuses/${id}/unreblog`
|
||||||
|
const MASTODON_DELETE_URL = (id) => `/api/v1/statuses/${id}`
|
||||||
|
const MASTODON_FOLLOW_URL = (id) => `/api/v1/accounts/${id}/follow`
|
||||||
|
const MASTODON_UNFOLLOW_URL = (id) => `/api/v1/accounts/${id}/unfollow`
|
||||||
|
|
||||||
|
const MASTODON_FOLLOW_REQUESTS_URL = '/api/v1/follow_requests'
|
||||||
|
const MASTODON_APPROVE_USER_URL = (id) =>
|
||||||
|
`/api/v1/follow_requests/${id}/authorize`
|
||||||
|
const MASTODON_DENY_USER_URL = (id) => `/api/v1/follow_requests/${id}/reject`
|
||||||
|
const MASTODON_USER_RELATIONSHIPS_URL = ({ id, withSuspended }) =>
|
||||||
|
`/api/v1/accounts/relationships/${paramsString({ id, withSuspended })}`
|
||||||
|
const MASTODON_USER_IN_LISTS = (id) => `/api/v1/accounts/${id}/lists`
|
||||||
|
export const MASTODON_LIST_URL = (id = '') => `/api/v1/lists/${id}`
|
||||||
|
export const MASTODON_LIST_ACCOUNTS_URL = (id) => `/api/v1/lists/${id}/accounts`
|
||||||
|
const MASTODON_USER_BLOCKS_URL = ({
|
||||||
|
maxId,
|
||||||
|
sinceId,
|
||||||
|
limit,
|
||||||
|
withRelationships,
|
||||||
|
}) =>
|
||||||
|
`/api/v1/blocks/${paramsString({ maxId, sinceId, limit, withRelationships })}`
|
||||||
|
const MASTODON_USER_MUTES_URL = ({
|
||||||
|
maxId,
|
||||||
|
sinceId,
|
||||||
|
limit,
|
||||||
|
withRelationships,
|
||||||
|
}) =>
|
||||||
|
`/api/v1/mutes/${paramsString({ maxId, sinceId, limit, withRelationships })}`
|
||||||
|
const MASTODON_BLOCK_USER_URL = (id) => `/api/v1/accounts/${id}/block`
|
||||||
|
const MASTODON_UNBLOCK_USER_URL = (id) => `/api/v1/accounts/${id}/unblock`
|
||||||
|
const MASTODON_MUTE_USER_URL = (id) => `/api/v1/accounts/${id}/mute`
|
||||||
|
const MASTODON_UNMUTE_USER_URL = (id) => `/api/v1/accounts/${id}/unmute`
|
||||||
|
const MASTODON_REMOVE_USER_FROM_FOLLOWERS = (id) =>
|
||||||
|
`/api/v1/accounts/${id}/remove_from_followers`
|
||||||
|
const MASTODON_USER_NOTE_URL = (id) => `/api/v1/accounts/${id}/note`
|
||||||
|
const MASTODON_BOOKMARK_STATUS_URL = (id) => `/api/v1/statuses/${id}/bookmark`
|
||||||
|
const MASTODON_UNBOOKMARK_STATUS_URL = (id) =>
|
||||||
|
`/api/v1/statuses/${id}/unbookmark`
|
||||||
|
const MASTODON_POST_STATUS_URL = '/api/v1/statuses'
|
||||||
|
const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
|
||||||
|
const MASTODON_VOTE_URL = (id) => `/api/v1/polls/${id}/votes`
|
||||||
|
const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials'
|
||||||
|
const MASTODON_REPORT_USER_URL = '/api/v1/reports'
|
||||||
|
const MASTODON_PIN_OWN_STATUS = (id) => `/api/v1/statuses/${id}/pin`
|
||||||
|
const MASTODON_UNPIN_OWN_STATUS = (id) => `/api/v1/statuses/${id}/unpin`
|
||||||
|
const MASTODON_MUTE_CONVERSATION = (id) => `/api/v1/statuses/${id}/mute`
|
||||||
|
const MASTODON_UNMUTE_CONVERSATION = (id) => `/api/v1/statuses/${id}/unmute`
|
||||||
|
const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
|
||||||
|
const MASTODON_ANNOUNCEMENTS_URL = '/api/v1/announcements'
|
||||||
|
const MASTODON_ANNOUNCEMENTS_DISMISS_URL = (id) =>
|
||||||
|
`/api/v1/announcements/${id}/dismiss`
|
||||||
|
const PLEROMA_EMOJI_REACT_URL = (id, emoji) =>
|
||||||
|
`/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
||||||
|
const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) =>
|
||||||
|
`/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
||||||
|
const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
|
||||||
|
const PLEROMA_BOOKMARK_FOLDERS_URL = '/api/v1/pleroma/bookmark_folders'
|
||||||
|
const PLEROMA_BOOKMARK_FOLDER_URL = (id) =>
|
||||||
|
`/api/v1/pleroma/bookmark_folders/${id}`
|
||||||
|
|
||||||
|
// #Posts
|
||||||
|
export const favorite = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_FAVORITE_URL(id),
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||||
|
|
||||||
|
export const unfavorite = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_UNFAVORITE_URL(id),
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||||
|
|
||||||
|
export const retweet = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_RETWEET_URL(id),
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||||
|
|
||||||
|
export const unretweet = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_UNRETWEET_URL(id),
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||||
|
|
||||||
|
export const reactWithEmoji = ({ id, emoji, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: PLEROMA_EMOJI_REACT_URL(id, emoji),
|
||||||
|
method: 'PUT',
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||||
|
|
||||||
|
export const unreactWithEmoji = ({ id, emoji, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: PLEROMA_EMOJI_UNREACT_URL(id, emoji),
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||||
|
|
||||||
|
export const bookmarkStatus = ({ id, credentials, ...options }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_BOOKMARK_STATUS_URL(id),
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
payload: {
|
||||||
|
folder_id: options.folder_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const unbookmarkStatus = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_UNBOOKMARK_STATUS_URL(id),
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const pinOwnStatus = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_PIN_OWN_STATUS(id),
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||||
|
|
||||||
|
export const unpinOwnStatus = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_UNPIN_OWN_STATUS(id),
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||||
|
|
||||||
|
export const muteConversation = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_MUTE_CONVERSATION(id),
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||||
|
|
||||||
|
export const unmuteConversation = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_UNMUTE_CONVERSATION(id),
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||||
|
|
||||||
|
export const vote = ({ pollId, choices, credentials }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: MASTODON_VOTE_URL(encodeURIComponent(pollId)),
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
payload: {
|
||||||
|
choices,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// #Posting
|
||||||
|
export const postStatus = ({
|
||||||
|
credentials,
|
||||||
|
status,
|
||||||
|
spoilerText,
|
||||||
|
visibility,
|
||||||
|
sensitive,
|
||||||
|
poll,
|
||||||
|
mediaIds = [],
|
||||||
|
inReplyToStatusId,
|
||||||
|
quoteId,
|
||||||
|
contentType,
|
||||||
|
preview,
|
||||||
|
idempotencyKey,
|
||||||
|
}) => {
|
||||||
|
const form = new FormData()
|
||||||
|
const pollOptions = poll.options || []
|
||||||
|
|
||||||
|
form.append('status', status)
|
||||||
|
form.append('source', 'Pleroma FE')
|
||||||
|
if (spoilerText) form.append('spoiler_text', spoilerText)
|
||||||
|
if (visibility) form.append('visibility', visibility)
|
||||||
|
if (sensitive) form.append('sensitive', sensitive)
|
||||||
|
if (contentType) form.append('content_type', contentType)
|
||||||
|
mediaIds.forEach((val) => {
|
||||||
|
form.append('media_ids[]', val)
|
||||||
|
})
|
||||||
|
if (pollOptions.some((option) => option !== '')) {
|
||||||
|
const normalizedPoll = {
|
||||||
|
expires_in: parseInt(poll.expiresIn, 10),
|
||||||
|
multiple: poll.multiple,
|
||||||
|
}
|
||||||
|
Object.keys(normalizedPoll).forEach((key) => {
|
||||||
|
form.append(`poll[${key}]`, normalizedPoll[key])
|
||||||
|
})
|
||||||
|
|
||||||
|
pollOptions.forEach((option) => {
|
||||||
|
form.append('poll[options][]', option)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (inReplyToStatusId) {
|
||||||
|
form.append('in_reply_to_id', inReplyToStatusId)
|
||||||
|
}
|
||||||
|
if (quoteId) {
|
||||||
|
form.append('quote_id', quoteId)
|
||||||
|
}
|
||||||
|
if (preview) {
|
||||||
|
form.append('preview', 'true')
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {}
|
||||||
|
if (idempotencyKey) {
|
||||||
|
headers['idempotency-key'] = idempotencyKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: MASTODON_POST_STATUS_URL,
|
||||||
|
formData: form,
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
headers,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const editStatus = ({
|
||||||
|
id,
|
||||||
|
credentials,
|
||||||
|
status,
|
||||||
|
spoilerText,
|
||||||
|
sensitive,
|
||||||
|
poll,
|
||||||
|
mediaIds = [],
|
||||||
|
contentType,
|
||||||
|
}) => {
|
||||||
|
const form = new FormData()
|
||||||
|
const pollOptions = poll.options || []
|
||||||
|
|
||||||
|
form.append('status', status)
|
||||||
|
if (spoilerText) form.append('spoiler_text', spoilerText)
|
||||||
|
if (sensitive) form.append('sensitive', sensitive)
|
||||||
|
if (contentType) form.append('content_type', contentType)
|
||||||
|
mediaIds.forEach((val) => {
|
||||||
|
form.append('media_ids[]', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (pollOptions.some((option) => option !== '')) {
|
||||||
|
const normalizedPoll = {
|
||||||
|
expires_in: parseInt(poll.expiresIn, 10),
|
||||||
|
multiple: poll.multiple,
|
||||||
|
}
|
||||||
|
Object.keys(normalizedPoll).forEach((key) => {
|
||||||
|
form.append(`poll[${key}]`, normalizedPoll[key])
|
||||||
|
})
|
||||||
|
|
||||||
|
pollOptions.forEach((option) => {
|
||||||
|
form.append('poll[options][]', option)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: MASTODON_STATUS_URL(id),
|
||||||
|
formData: form,
|
||||||
|
method: 'PUT',
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteStatus = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_DELETE_URL(id),
|
||||||
|
credentials,
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const uploadMedia = ({ formData, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_MEDIA_UPLOAD_URL,
|
||||||
|
formData,
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseAttachment(data) }))
|
||||||
|
|
||||||
|
export const setMediaDescription = ({ id, description, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: `${MASTODON_MEDIA_UPLOAD_URL}/${id}`,
|
||||||
|
method: 'PUT',
|
||||||
|
credentials,
|
||||||
|
payload: {
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseAttachment(data) }))
|
||||||
|
|
||||||
|
// #Notifications
|
||||||
|
export const dismissNotification = ({ credentials, id }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_DISMISS_NOTIFICATION_URL(id),
|
||||||
|
method: 'POST',
|
||||||
|
payload: { id },
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const markNotificationsAsSeen = ({
|
||||||
|
id,
|
||||||
|
credentials,
|
||||||
|
single = false,
|
||||||
|
}) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
if (single) {
|
||||||
|
formData.append('id', id)
|
||||||
|
} else {
|
||||||
|
formData.append('max_id', id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: NOTIFICATION_READ_URL,
|
||||||
|
formData,
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// #Announcements
|
||||||
|
export const getAnnouncements = ({ credentials }) =>
|
||||||
|
promisedRequest({ url: MASTODON_ANNOUNCEMENTS_URL, credentials })
|
||||||
|
|
||||||
|
export const dismissAnnouncement = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_ANNOUNCEMENTS_DISMISS_URL(id),
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
|
||||||
|
// #Imports
|
||||||
|
export const importMutes = ({ file, credentials }) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('list', file)
|
||||||
|
return promisedRequest({
|
||||||
|
url: MUTES_IMPORT_URL,
|
||||||
|
formData,
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
}).then((response) => response.ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const importBlocks = ({ file, credentials }) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('list', file)
|
||||||
|
return promisedRequest({
|
||||||
|
url: BLOCKS_IMPORT_URL,
|
||||||
|
formData,
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
}).then((response) => response.ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const importFollows = ({ file, credentials }) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('list', file)
|
||||||
|
return promisedRequest({
|
||||||
|
url: FOLLOW_IMPORT_URL,
|
||||||
|
formData,
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
}).then((response) => response.ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const exportFriends = ({ id, credentials }) => {
|
||||||
|
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO refactor this
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
let friends = []
|
||||||
|
let more = true
|
||||||
|
while (more) {
|
||||||
|
const maxId = friends.length > 0 ? last(friends).id : undefined
|
||||||
|
const users = await fetchFriends({
|
||||||
|
id,
|
||||||
|
maxId,
|
||||||
|
credentials,
|
||||||
|
withRelationships: true,
|
||||||
|
})
|
||||||
|
friends = concat(friends, users)
|
||||||
|
if (users.length === 0) {
|
||||||
|
more = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(friends)
|
||||||
|
} catch (err) {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// #Profile settings
|
||||||
|
export const updateNotificationSettings = ({ credentials, settings }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: NOTIFICATION_SETTINGS_URL,
|
||||||
|
credentials,
|
||||||
|
method: 'PUT',
|
||||||
|
payload: settings,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateProfileImages = ({
|
||||||
|
credentials,
|
||||||
|
avatar = null,
|
||||||
|
avatarName = null,
|
||||||
|
banner = null,
|
||||||
|
background = null,
|
||||||
|
}) => {
|
||||||
|
const form = new FormData()
|
||||||
|
if (avatar !== null) {
|
||||||
|
if (avatarName !== null) {
|
||||||
|
form.append('avatar', avatar, avatarName)
|
||||||
|
} else {
|
||||||
|
form.append('avatar', avatar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (banner !== null) form.append('header', banner)
|
||||||
|
if (background !== null) form.append('pleroma_background_image', background)
|
||||||
|
return promisedRequest({
|
||||||
|
url: MASTODON_PROFILE_UPDATE_URL,
|
||||||
|
credentials,
|
||||||
|
method: 'PATCH',
|
||||||
|
formData: form,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateProfile = ({ credentials, params }) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
for (const name in params) {
|
||||||
|
if (name === 'fields_attributes') {
|
||||||
|
params[name].forEach((param, i) => {
|
||||||
|
formData.append(name + `[${i}][name]`, param.name)
|
||||||
|
formData.append(name + `[${i}][value]`, param.value)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if (typeof params[name] === 'object') {
|
||||||
|
console.warn(
|
||||||
|
'Object detected in updateProfile API call. This will not work, use updateProfileJSON instead.',
|
||||||
|
)
|
||||||
|
console.warn('Object:\n' + JSON.stringify(params[name], null, 2))
|
||||||
|
}
|
||||||
|
formData.append(name, params[name])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: MASTODON_PROFILE_UPDATE_URL,
|
||||||
|
credentials,
|
||||||
|
method: 'PATCH',
|
||||||
|
formData,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateProfileJSON = ({ credentials, params }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_PROFILE_UPDATE_URL,
|
||||||
|
credentials,
|
||||||
|
payload: params,
|
||||||
|
method: 'PATCH',
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) }))
|
||||||
|
|
||||||
|
export const changeEmail = ({ credentials, email, password }) => {
|
||||||
|
const form = new FormData()
|
||||||
|
|
||||||
|
form.append('email', email)
|
||||||
|
form.append('password', password)
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: CHANGE_EMAIL_URL,
|
||||||
|
formData: form,
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const moveAccount = ({ credentials, password, targetAccount }) => {
|
||||||
|
const form = new FormData()
|
||||||
|
|
||||||
|
form.append('password', password)
|
||||||
|
form.append('target_account', targetAccount)
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: MOVE_ACCOUNT_URL,
|
||||||
|
formData: form,
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changePassword = ({
|
||||||
|
credentials,
|
||||||
|
password,
|
||||||
|
newPassword,
|
||||||
|
newPasswordConfirmation,
|
||||||
|
}) => {
|
||||||
|
const form = new FormData()
|
||||||
|
|
||||||
|
form.append('password', password)
|
||||||
|
form.append('new_password', newPassword)
|
||||||
|
form.append('new_password_confirmation', newPasswordConfirmation)
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: CHANGE_PASSWORD_URL,
|
||||||
|
formData: form,
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// #MFA
|
||||||
|
export const settingsMFA = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MFA_SETTINGS_URL,
|
||||||
|
credentials,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const mfaDisableOTP = ({ credentials, password }) => {
|
||||||
|
const form = new FormData()
|
||||||
|
|
||||||
|
form.append('password', password)
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: MFA_DISABLE_OTP_URL,
|
||||||
|
formData: form,
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mfaConfirmOTP = ({ credentials, password, token }) => {
|
||||||
|
const form = new FormData()
|
||||||
|
|
||||||
|
form.append('password', password)
|
||||||
|
form.append('code', token)
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: MFA_CONFIRM_OTP_URL,
|
||||||
|
formData: form,
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
export const mfaSetupOTP = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MFA_SETUP_OTP_URL,
|
||||||
|
credentials,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
export const generateMfaBackupCodes = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MFA_BACKUP_CODES_URL,
|
||||||
|
credentials,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
|
||||||
|
// #Aliases
|
||||||
|
export const addAlias = ({ credentials, alias }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: ALIASES_URL,
|
||||||
|
method: 'PUT',
|
||||||
|
credentials,
|
||||||
|
payload: { alias },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const deleteAlias = ({ credentials, alias }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: ALIASES_URL,
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials,
|
||||||
|
payload: { alias },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const listAliases = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: ALIASES_URL,
|
||||||
|
method: 'GET',
|
||||||
|
credentials,
|
||||||
|
params: {
|
||||||
|
_cacheBooster: new Date().getTime(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// User manipulation
|
||||||
|
export const fetchUserRelationship = ({ id, withSuspended, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_USER_RELATIONSHIPS_URL({ id, withSuspended }),
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const followUser = ({ id, credentials, ...options }) => {
|
||||||
|
const payload = {}
|
||||||
|
|
||||||
|
if (options.reblogs !== undefined) {
|
||||||
|
payload.reblogs = options.reblogs
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.notify !== undefined) {
|
||||||
|
payload.notify = options.notify
|
||||||
|
}
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: MASTODON_FOLLOW_URL(id),
|
||||||
|
payload,
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const unfollowUser = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_UNFOLLOW_URL(id),
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
export const fetchUserInLists = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_USER_IN_LISTS(id),
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const removeUserFromFollowers = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_REMOVE_USER_FROM_FOLLOWERS(id),
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const fetchFollowRequests = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_FOLLOW_REQUESTS_URL,
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) }))
|
||||||
|
|
||||||
|
export const approveUser = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_APPROVE_USER_URL(id),
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const denyUser = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_DENY_USER_URL(id),
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const editUserNote = ({ id, credentials, comment }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_USER_NOTE_URL(id),
|
||||||
|
credentials,
|
||||||
|
payload: {
|
||||||
|
comment,
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const fetchMutes = ({ maxId, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_USER_MUTES_URL({ maxId, withRelationships: true }),
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) }))
|
||||||
|
|
||||||
|
export const muteUser = ({ id, expiresIn, credentials }) => {
|
||||||
|
const payload = {}
|
||||||
|
if (expiresIn) {
|
||||||
|
payload.expires_in = expiresIn
|
||||||
|
}
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: MASTODON_MUTE_USER_URL(id),
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const unmuteUser = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_UNMUTE_USER_URL(id),
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const fetchBlocks = ({ maxId, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_USER_BLOCKS_URL({ maxId, withRelationships: true }),
|
||||||
|
credentials,
|
||||||
|
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) }))
|
||||||
|
|
||||||
|
export const blockUser = ({ id, expiresIn, credentials }) => {
|
||||||
|
const payload = {}
|
||||||
|
if (expiresIn) {
|
||||||
|
payload.duration = expiresIn
|
||||||
|
}
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: MASTODON_BLOCK_USER_URL(id),
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const unblockUser = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_UNBLOCK_USER_URL(id),
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const reportUser = ({
|
||||||
|
credentials,
|
||||||
|
userId,
|
||||||
|
statusIds,
|
||||||
|
comment,
|
||||||
|
forward,
|
||||||
|
}) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_REPORT_USER_URL,
|
||||||
|
method: 'POST',
|
||||||
|
payload: {
|
||||||
|
account_id: userId,
|
||||||
|
status_ids: statusIds,
|
||||||
|
comment,
|
||||||
|
forward,
|
||||||
|
},
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
// #Domain mutes
|
||||||
|
export const fetchDomainMutes = ({ credentials }) =>
|
||||||
|
promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials })
|
||||||
|
|
||||||
|
export const muteDomain = ({ domain, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_DOMAIN_BLOCKS_URL,
|
||||||
|
method: 'POST',
|
||||||
|
payload: { domain },
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const unmuteDomain = ({ domain, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_DOMAIN_BLOCKS_URL,
|
||||||
|
method: 'DELETE',
|
||||||
|
payload: { domain },
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
// #Backups
|
||||||
|
export const addBackup = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: PLEROMA_BACKUP_URL,
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const listBackups = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: PLEROMA_BACKUP_URL,
|
||||||
|
method: 'GET',
|
||||||
|
credentials,
|
||||||
|
params: {
|
||||||
|
_cacheBooster: new Date().getTime(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// #OAuth
|
||||||
|
export const fetchOAuthTokens = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: '/api/oauth_tokens.json',
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const revokeOAuthToken = ({ id, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: `/api/oauth_tokens/${id}`,
|
||||||
|
credentials,
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
|
||||||
|
// #Lists
|
||||||
|
export const fetchLists = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_LIST_URL(),
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const createList = ({ title, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_LIST_URL(),
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
payload: { title },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getList = ({ listId, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_LIST_URL(listId),
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const updateList = ({ listId, title, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_LIST_URL(listId),
|
||||||
|
|
||||||
|
credentials,
|
||||||
|
method: 'PUT',
|
||||||
|
payload: { title },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getListAccounts = ({ listId, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_LIST_ACCOUNTS_URL(listId),
|
||||||
|
credentials,
|
||||||
|
}).then((data) => data.map(({ id }) => id))
|
||||||
|
|
||||||
|
export const addAccountsToList = ({ listId, accountIds, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_LIST_ACCOUNTS_URL(listId),
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
payload: { account_ids: accountIds },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const removeAccountsFromList = ({ listId, accountIds, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_LIST_ACCOUNTS_URL(listId),
|
||||||
|
credentials,
|
||||||
|
method: 'DELETE',
|
||||||
|
payload: { account_ids: accountIds },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const deleteList = ({ listId, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: MASTODON_LIST_URL(listId),
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
// #Bookmarks
|
||||||
|
export const fetchBookmarkFolders = ({ credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: PLEROMA_BOOKMARK_FOLDERS_URL,
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const createBookmarkFolder = ({ name, emoji, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: PLEROMA_BOOKMARK_FOLDERS_URL,
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
payload: { name, emoji },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const updateBookmarkFolder = ({ folderId, name, emoji, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: PLEROMA_BOOKMARK_FOLDER_URL(folderId),
|
||||||
|
credentials,
|
||||||
|
method: 'PATCH',
|
||||||
|
payload: { name, emoji },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const deleteBookmarkFolder = ({ folderId, credentials }) =>
|
||||||
|
promisedRequest({
|
||||||
|
url: PLEROMA_BOOKMARK_FOLDER_URL(folderId),
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
|
||||||
|
// #So long and thanks for all the fish
|
||||||
|
export const deleteAccount = ({ credentials, password }) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
formData.append('password', password)
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: DELETE_ACCOUNT_URL,
|
||||||
|
formData,
|
||||||
|
method: 'POST',
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
}
|
||||||
176
src/api/websocket.js
Normal file
176
src/api/websocket.js
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
import { paramsString } from './helpers.js'
|
||||||
|
|
||||||
|
import {
|
||||||
|
parseChat,
|
||||||
|
parseNotification,
|
||||||
|
parseStatus,
|
||||||
|
} from 'src/services/entity_normalizer/entity_normalizer.service.js'
|
||||||
|
|
||||||
|
const MASTODON_STREAMING = ({ accessToken, stream }) =>
|
||||||
|
`/api/v1/streaming${paramsString({ accessToken, stream })}`
|
||||||
|
|
||||||
|
export const getMastodonSocketURI = ({ credentials, stream }) => {
|
||||||
|
return MASTODON_STREAMING({ accessToken: credentials, stream })
|
||||||
|
}
|
||||||
|
|
||||||
|
const MASTODON_STREAMING_EVENTS = new Set([
|
||||||
|
'update',
|
||||||
|
'notification',
|
||||||
|
'delete',
|
||||||
|
'filters_changed',
|
||||||
|
'status.update',
|
||||||
|
])
|
||||||
|
|
||||||
|
const PLEROMA_STREAMING_EVENTS = new Set([
|
||||||
|
'pleroma:chat_update',
|
||||||
|
'pleroma:respond',
|
||||||
|
])
|
||||||
|
|
||||||
|
// A thin wrapper around WebSocket API that allows adding a pre-processor to it
|
||||||
|
// Uses EventTarget and a CustomEvent to proxy events
|
||||||
|
export const ProcessedWS = ({
|
||||||
|
url,
|
||||||
|
preprocessor = handleMastoWS,
|
||||||
|
id = 'Unknown',
|
||||||
|
credentials,
|
||||||
|
}) => {
|
||||||
|
const eventTarget = new EventTarget()
|
||||||
|
const socket = new WebSocket(url)
|
||||||
|
if (!socket) throw new Error(`Failed to create socket ${id}`)
|
||||||
|
const proxy = (original, eventName, processor = (a) => a) => {
|
||||||
|
original.addEventListener(eventName, (eventData) => {
|
||||||
|
eventTarget.dispatchEvent(
|
||||||
|
new CustomEvent(eventName, { detail: processor(eventData) }),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
socket.addEventListener('open', (wsEvent) => {
|
||||||
|
console.debug(`[WS][${id}] Socket connected`, wsEvent)
|
||||||
|
if (credentials) {
|
||||||
|
socket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'pleroma:authenticate',
|
||||||
|
token: credentials,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
socket.addEventListener('error', (wsEvent) => {
|
||||||
|
console.debug(`[WS][${id}] Socket errored`, wsEvent)
|
||||||
|
})
|
||||||
|
socket.addEventListener('close', (wsEvent) => {
|
||||||
|
console.debug(
|
||||||
|
`[WS][${id}] Socket disconnected with code ${wsEvent.code}`,
|
||||||
|
wsEvent,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
// Commented code reason: very spammy, uncomment to enable message debug logging
|
||||||
|
/*
|
||||||
|
socket.addEventListener('message', (wsEvent) => {
|
||||||
|
console.debug(
|
||||||
|
`[WS][${id}] Message received`,
|
||||||
|
wsEvent
|
||||||
|
)
|
||||||
|
})
|
||||||
|
/**/
|
||||||
|
|
||||||
|
const onAuthenticated = () => {
|
||||||
|
eventTarget.dispatchEvent(new CustomEvent('pleroma:authenticated'))
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy(socket, 'open')
|
||||||
|
proxy(socket, 'close')
|
||||||
|
proxy(socket, 'message', (event) => preprocessor(event, { onAuthenticated }))
|
||||||
|
proxy(socket, 'error')
|
||||||
|
|
||||||
|
// 1000 = Normal Closure
|
||||||
|
eventTarget.close = () => {
|
||||||
|
socket.close(1000, 'Shutting down socket')
|
||||||
|
}
|
||||||
|
eventTarget.getState = () => socket.readyState
|
||||||
|
eventTarget.subscribe = (stream, args = {}) => {
|
||||||
|
console.debug(`[WS][${id}] Subscribing to stream ${stream} with args`, args)
|
||||||
|
socket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'subscribe',
|
||||||
|
stream,
|
||||||
|
...args,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
eventTarget.unsubscribe = (stream, args = {}) => {
|
||||||
|
console.debug(
|
||||||
|
`[WS][${id}] Unsubscribing from stream ${stream} with args`,
|
||||||
|
args,
|
||||||
|
)
|
||||||
|
socket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'unsubscribe',
|
||||||
|
stream,
|
||||||
|
...args,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleMastoWS = (
|
||||||
|
wsEvent,
|
||||||
|
{
|
||||||
|
onAuthenticated = () => {
|
||||||
|
/* no-op */
|
||||||
|
},
|
||||||
|
} = {},
|
||||||
|
) => {
|
||||||
|
const { data } = wsEvent
|
||||||
|
if (!data) return
|
||||||
|
const parsedEvent = JSON.parse(data)
|
||||||
|
const { event, payload } = parsedEvent
|
||||||
|
if (
|
||||||
|
MASTODON_STREAMING_EVENTS.has(event) ||
|
||||||
|
PLEROMA_STREAMING_EVENTS.has(event)
|
||||||
|
) {
|
||||||
|
// MastoBE and PleromaBE both send payload for delete as a PLAIN string
|
||||||
|
if (event === 'delete') {
|
||||||
|
return { event, id: payload }
|
||||||
|
}
|
||||||
|
const data = payload ? JSON.parse(payload) : null
|
||||||
|
if (event === 'pleroma:respond') {
|
||||||
|
if (data.type === 'pleroma:authenticate') {
|
||||||
|
if (data.result === 'success') {
|
||||||
|
console.debug('[WS] Successfully authenticated')
|
||||||
|
onAuthenticated()
|
||||||
|
} else {
|
||||||
|
if (data.error === 'already_authenticated') {
|
||||||
|
onAuthenticated()
|
||||||
|
} else {
|
||||||
|
console.error('[WS] Unable to authenticate:', data.error)
|
||||||
|
wsEvent.target.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} else if (event === 'update') {
|
||||||
|
return { event, status: parseStatus(data) }
|
||||||
|
} else if (event === 'status.update') {
|
||||||
|
return { event, status: parseStatus(data) }
|
||||||
|
} else if (event === 'notification') {
|
||||||
|
return { event, notification: parseNotification(data) }
|
||||||
|
} else if (event === 'pleroma:chat_update') {
|
||||||
|
return { event, chatUpdate: parseChat(data) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('Unknown event', wsEvent)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WSConnectionStatus = Object.freeze({
|
||||||
|
JOINED: 1,
|
||||||
|
CLOSED: 2,
|
||||||
|
ERROR: 3,
|
||||||
|
DISABLED: 4,
|
||||||
|
STARTING: 5,
|
||||||
|
STARTING_INITIAL: 6,
|
||||||
|
})
|
||||||
|
|
@ -6,6 +6,10 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||||
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 RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||||
|
import Status from 'src/components/status/status.vue'
|
||||||
|
import StillImage from 'src/components/still-image/still-image.vue'
|
||||||
|
|
||||||
import { config } from '@fortawesome/fontawesome-svg-core'
|
import { config } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
|
|
@ -15,7 +19,6 @@ import {
|
||||||
config.autoAddCss = false
|
config.autoAddCss = false
|
||||||
|
|
||||||
import App from '../App.vue'
|
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 FaviconService from '../services/favicon_service/favicon_service.js'
|
||||||
import { applyStyleConfig } from '../services/style_setter/style_setter.js'
|
import { applyStyleConfig } from '../services/style_setter/style_setter.js'
|
||||||
import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
|
import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
|
||||||
|
|
@ -25,7 +28,6 @@ import {
|
||||||
} from '../services/window_utils/window_utils'
|
} from '../services/window_utils/window_utils'
|
||||||
import routes from './routes'
|
import routes from './routes'
|
||||||
|
|
||||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
|
||||||
import { useAuthFlowStore } from 'src/stores/auth_flow'
|
import { useAuthFlowStore } from 'src/stores/auth_flow'
|
||||||
import { useEmojiStore } from 'src/stores/emoji.js'
|
import { useEmojiStore } from 'src/stores/emoji.js'
|
||||||
import { useI18nStore } from 'src/stores/i18n'
|
import { useI18nStore } from 'src/stores/i18n'
|
||||||
|
|
@ -34,7 +36,7 @@ import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.j
|
||||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||||
import { useLocalConfigStore } from 'src/stores/local_config.js'
|
import { useLocalConfigStore } from 'src/stores/local_config.js'
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
import { useOAuthStore } from 'src/stores/oauth'
|
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||||
import { useSyncConfigStore } from 'src/stores/sync_config.js'
|
import { useSyncConfigStore } from 'src/stores/sync_config.js'
|
||||||
import { useUserHighlightStore } from 'src/stores/user_highlight.js'
|
import { useUserHighlightStore } from 'src/stores/user_highlight.js'
|
||||||
|
|
||||||
|
|
@ -152,8 +154,10 @@ const getStaticConfig = async () => {
|
||||||
throw res
|
throw res
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to load static/config.json, continuing without it.')
|
console.warn(
|
||||||
console.warn(error)
|
'Failed to load static/config.json, continuing without it.',
|
||||||
|
error,
|
||||||
|
)
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -175,14 +179,16 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
||||||
if (source === 'name') return
|
if (source === 'name') return
|
||||||
if (INSTANCE_IDENTIY_EXTERNAL.has(source)) return
|
if (INSTANCE_IDENTIY_EXTERNAL.has(source)) return
|
||||||
useInstanceStore().set({
|
useInstanceStore().set({
|
||||||
value: config[source] ?? INSTANCE_IDENTITY_DEFAULT_DEFINITIONS[source].default,
|
value:
|
||||||
|
config[source] ?? INSTANCE_IDENTITY_DEFAULT_DEFINITIONS[source].default,
|
||||||
path: `instanceIdentity.${source}`,
|
path: `instanceIdentity.${source}`,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Object.keys(INSTANCE_DEFAULT_CONFIG_DEFINITIONS).forEach((source) =>
|
Object.keys(INSTANCE_DEFAULT_CONFIG_DEFINITIONS).forEach((source) =>
|
||||||
useInstanceStore().set({
|
useInstanceStore().set({
|
||||||
value: config[source] ?? INSTANCE_DEFAULT_CONFIG_DEFINITIONS[source].default,
|
value:
|
||||||
|
config[source] ?? INSTANCE_DEFAULT_CONFIG_DEFINITIONS[source].default,
|
||||||
path: `prefsStorage.${source}`,
|
path: `prefsStorage.${source}`,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -253,16 +259,6 @@ const getStickers = async ({ store }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAppSecret = async ({ store }) => {
|
|
||||||
const oauth = useOAuthStore()
|
|
||||||
if (oauth.userToken) {
|
|
||||||
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())
|
||||||
useInstanceStore().set({
|
useInstanceStore().set({
|
||||||
|
|
@ -440,8 +436,7 @@ const getNodeInfo = async ({ store }) => {
|
||||||
throw res
|
throw res
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Could not load nodeinfo')
|
console.warn('Could not load nodeinfo', e)
|
||||||
console.warn(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -454,14 +449,13 @@ const setConfig = async ({ store }) => {
|
||||||
const apiConfig = configInfos[0]
|
const apiConfig = configInfos[0]
|
||||||
const staticConfig = configInfos[1]
|
const staticConfig = configInfos[1]
|
||||||
|
|
||||||
getAppSecret({ store })
|
|
||||||
await setSettings({ store, apiConfig, staticConfig })
|
await setSettings({ store, apiConfig, staticConfig })
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkOAuthToken = async ({ store }) => {
|
const checkOAuthToken = async ({ store }) => {
|
||||||
const oauth = useOAuthStore()
|
const oauth = useOAuthStore()
|
||||||
if (oauth.getUserToken) {
|
if (oauth.userToken) {
|
||||||
return store.dispatch('loginUser', oauth.getUserToken)
|
return store.dispatch('loginUser', oauth.userToken)
|
||||||
}
|
}
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
@ -473,6 +467,16 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
||||||
// "Plugins are only applied to stores created after the plugins themselves, and after pinia is passed to the app, otherwise they won't be applied."
|
// "Plugins are only applied to stores created after the plugins themselves, and after pinia is passed to the app, otherwise they won't be applied."
|
||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
|
|
||||||
|
app.config.errorHandler = (error, instance, info) => {
|
||||||
|
console.error(
|
||||||
|
'Global Vue Error Handler caught an error:',
|
||||||
|
error,
|
||||||
|
instance,
|
||||||
|
info,
|
||||||
|
)
|
||||||
|
useInterfaceStore().setGlobalError({ error, instance, info })
|
||||||
|
}
|
||||||
|
|
||||||
const waitForAllStoresToLoad = async () => {
|
const waitForAllStoresToLoad = async () => {
|
||||||
// the stores that do not persist technically do not need to be awaited here,
|
// the stores that do not persist technically do not need to be awaited here,
|
||||||
// but that involves either hard-coding the stores in some place (prone to errors)
|
// but that involves either hard-coding the stores in some place (prone to errors)
|
||||||
|
|
@ -571,10 +575,6 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
||||||
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
|
|
||||||
store.dispatch('fetchMutes')
|
|
||||||
store.dispatch('loadDrafts')
|
|
||||||
useAnnouncementsStore().startFetchingAnnouncements()
|
|
||||||
getTOS({ store })
|
getTOS({ store })
|
||||||
getStickers({ store })
|
getStickers({ store })
|
||||||
|
|
||||||
|
|
@ -608,6 +608,9 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
||||||
|
|
||||||
app.component('FAIcon', FontAwesomeIcon)
|
app.component('FAIcon', FontAwesomeIcon)
|
||||||
app.component('FALayers', FontAwesomeLayers)
|
app.component('FALayers', FontAwesomeLayers)
|
||||||
|
app.component('Status', Status)
|
||||||
|
app.component('RichContent', RichContent)
|
||||||
|
app.component('StillImage', StillImage)
|
||||||
|
|
||||||
// remove after vue 3.3
|
// remove after vue 3.3
|
||||||
app.config.unwrapInjectedRef = true
|
app.config.unwrapInjectedRef = true
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,16 @@
|
||||||
import About from 'components/about/about.vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
import AnnouncementsPage from 'components/announcements_page/announcements_page.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 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 BookmarkTimeline from 'src/components/bookmark_timeline/bookmark_timeline.vue'
|
||||||
|
import BubbleTimeline from 'src/components/bubble_timeline/bubble_timeline.vue'
|
||||||
|
import ConversationPage from 'src/components/conversation-page/conversation-page.vue'
|
||||||
|
import DMs from 'src/components/dm_timeline/dm_timeline.vue'
|
||||||
|
import FriendsTimeline from 'src/components/friends_timeline/friends_timeline.vue'
|
||||||
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
|
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
|
||||||
import BookmarkFolderEdit from '../components/bookmark_folder_edit/bookmark_folder_edit.vue'
|
import PublicAndExternalTimeline from 'src/components/public_and_external_timeline/public_and_external_timeline.vue'
|
||||||
import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue'
|
import PublicTimeline from 'src/components/public_timeline/public_timeline.vue'
|
||||||
import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue'
|
import QuotesTimeline from 'src/components/quotes_timeline/quotes_timeline.vue'
|
||||||
|
import RemoteUserResolver from 'src/components/remote_user_resolver/remote_user_resolver.vue'
|
||||||
|
import TagTimeline from 'src/components/tag_timeline/tag_timeline.vue'
|
||||||
|
|
||||||
import { useInstanceStore } from 'src/stores/instance.js'
|
import { useInstanceStore } from 'src/stores/instance.js'
|
||||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||||
|
|
@ -100,12 +80,23 @@ export default (store) => {
|
||||||
{
|
{
|
||||||
name: 'external-user-profile',
|
name: 'external-user-profile',
|
||||||
path: '/users/$:id',
|
path: '/users/$:id',
|
||||||
component: UserProfile,
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/user_profile/user_profile.vue'),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'user-profile-admin-view',
|
||||||
|
path: '/users/$:id/admin_view',
|
||||||
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/user_profile/user_profile_admin_view.vue'),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'interactions',
|
name: 'interactions',
|
||||||
path: '/users/:username/interactions',
|
path: '/users/:username/interactions',
|
||||||
component: Interactions,
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/interactions/interactions.vue'),
|
||||||
|
),
|
||||||
beforeEnter: validateAuthenticatedRoute,
|
beforeEnter: validateAuthenticatedRoute,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -114,69 +105,148 @@ export default (store) => {
|
||||||
component: DMs,
|
component: DMs,
|
||||||
beforeEnter: validateAuthenticatedRoute,
|
beforeEnter: validateAuthenticatedRoute,
|
||||||
},
|
},
|
||||||
{ name: 'registration', path: '/registration', component: Registration },
|
{
|
||||||
|
name: 'registration',
|
||||||
|
path: '/registration',
|
||||||
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/registration/registration.vue'),
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'password-reset',
|
name: 'password-reset',
|
||||||
path: '/password-reset',
|
path: '/password-reset',
|
||||||
component: PasswordReset,
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/password_reset/password_reset.vue'),
|
||||||
|
),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'registration-token',
|
name: 'registration-token',
|
||||||
path: '/registration/:token',
|
path: '/registration/:token',
|
||||||
component: Registration,
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/registration/registration.vue'),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'friend-requests',
|
name: 'friend-requests',
|
||||||
path: '/friend-requests',
|
path: '/friend-requests',
|
||||||
component: FollowRequests,
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/follow_requests/follow_requests.vue'),
|
||||||
|
),
|
||||||
beforeEnter: validateAuthenticatedRoute,
|
beforeEnter: validateAuthenticatedRoute,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'notifications',
|
name: 'notifications',
|
||||||
path: '/:username/notifications',
|
path: '/:username/notifications',
|
||||||
component: Notifications,
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/notifications/notifications.vue'),
|
||||||
|
),
|
||||||
props: () => ({ disableTeleport: true }),
|
props: () => ({ disableTeleport: true }),
|
||||||
beforeEnter: validateAuthenticatedRoute,
|
beforeEnter: validateAuthenticatedRoute,
|
||||||
},
|
},
|
||||||
{ name: 'login', path: '/login', component: AuthForm },
|
{
|
||||||
|
name: 'login',
|
||||||
|
path: '/login',
|
||||||
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/auth_form/auth_form.js'),
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'shout-panel',
|
name: 'shout-panel',
|
||||||
path: '/shout-panel',
|
path: '/shout-panel',
|
||||||
component: ShoutPanel,
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/shout_panel/shout_panel.vue'),
|
||||||
|
),
|
||||||
props: () => ({ floating: false }),
|
props: () => ({ floating: false }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'oauth-callback',
|
name: 'oauth-callback',
|
||||||
path: '/oauth-callback',
|
path: '/oauth-callback',
|
||||||
component: OAuthCallback,
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/oauth_callback/oauth_callback.vue'),
|
||||||
|
),
|
||||||
props: (route) => ({ code: route.query.code }),
|
props: (route) => ({ code: route.query.code }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'search',
|
name: 'search',
|
||||||
path: '/search',
|
path: '/search',
|
||||||
component: Search,
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/search/search.vue'),
|
||||||
|
),
|
||||||
props: (route) => ({ query: route.query.query }),
|
props: (route) => ({ query: route.query.query }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'who-to-follow',
|
name: 'who-to-follow',
|
||||||
path: '/who-to-follow',
|
path: '/who-to-follow',
|
||||||
component: WhoToFollow,
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/who_to_follow/who_to_follow.vue'),
|
||||||
|
),
|
||||||
beforeEnter: validateAuthenticatedRoute,
|
beforeEnter: validateAuthenticatedRoute,
|
||||||
},
|
},
|
||||||
{ name: 'about', path: '/about', component: About },
|
{
|
||||||
|
name: 'about',
|
||||||
|
path: '/about',
|
||||||
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/about/about.vue'),
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'announcements',
|
name: 'announcements',
|
||||||
path: '/announcements',
|
path: '/announcements',
|
||||||
component: AnnouncementsPage,
|
component: defineAsyncComponent(
|
||||||
|
() =>
|
||||||
|
import('src/components/announcements_page/announcements_page.vue'),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'drafts',
|
||||||
|
path: '/drafts',
|
||||||
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/drafts/drafts.vue'),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'user-profile',
|
||||||
|
path: '/users/:name',
|
||||||
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/user_profile/user_profile.vue'),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'legacy-user-profile',
|
||||||
|
path: '/:name',
|
||||||
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/user_profile/user_profile.vue'),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lists',
|
||||||
|
path: '/lists',
|
||||||
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/lists/lists.vue'),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lists-timeline',
|
||||||
|
path: '/lists/:id',
|
||||||
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/lists_timeline/lists_timeline.vue'),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lists-edit',
|
||||||
|
path: '/lists/:id/edit',
|
||||||
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/lists_edit/lists_edit.vue'),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lists-new',
|
||||||
|
path: '/lists/new',
|
||||||
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/lists_edit/lists_edit.vue'),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{ name: 'drafts', path: '/drafts', component: Drafts },
|
|
||||||
{ name: 'user-profile', path: '/users/:name', component: UserProfile },
|
|
||||||
{ name: 'legacy-user-profile', path: '/:name', component: UserProfile },
|
|
||||||
{ name: 'lists', path: '/lists', component: Lists },
|
|
||||||
{ name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline },
|
|
||||||
{ name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit },
|
|
||||||
{ name: 'lists-new', path: '/lists/new', component: ListsEdit },
|
|
||||||
{
|
{
|
||||||
name: 'edit-navigation',
|
name: 'edit-navigation',
|
||||||
path: '/nav-edit',
|
path: '/nav-edit',
|
||||||
|
|
@ -187,12 +257,19 @@ export default (store) => {
|
||||||
{
|
{
|
||||||
name: 'bookmark-folders',
|
name: 'bookmark-folders',
|
||||||
path: '/bookmark_folders',
|
path: '/bookmark_folders',
|
||||||
component: BookmarkFolders,
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/bookmark_folders/bookmark_folders.vue'),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'bookmark-folder-new',
|
name: 'bookmark-folder-new',
|
||||||
path: '/bookmarks/new-folder',
|
path: '/bookmarks/new-folder',
|
||||||
component: BookmarkFolderEdit,
|
component: defineAsyncComponent(
|
||||||
|
() =>
|
||||||
|
import(
|
||||||
|
'src/components/bookmark_folder_edit/bookmark_folder_edit.vue'
|
||||||
|
),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'bookmark-folder',
|
name: 'bookmark-folder',
|
||||||
|
|
@ -202,7 +279,12 @@ export default (store) => {
|
||||||
{
|
{
|
||||||
name: 'bookmark-folder-edit',
|
name: 'bookmark-folder-edit',
|
||||||
path: '/bookmarks/:id/edit',
|
path: '/bookmarks/:id/edit',
|
||||||
component: BookmarkFolderEdit,
|
component: defineAsyncComponent(
|
||||||
|
() =>
|
||||||
|
import(
|
||||||
|
'src/components/bookmark_folder_edit/bookmark_folder_edit.vue'
|
||||||
|
),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -211,14 +293,18 @@ export default (store) => {
|
||||||
{
|
{
|
||||||
name: 'chat',
|
name: 'chat',
|
||||||
path: '/users/:username/chats/:recipient_id',
|
path: '/users/:username/chats/:recipient_id',
|
||||||
component: Chat,
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/chat/chat.vue'),
|
||||||
|
),
|
||||||
meta: { dontScroll: false },
|
meta: { dontScroll: false },
|
||||||
beforeEnter: validateAuthenticatedRoute,
|
beforeEnter: validateAuthenticatedRoute,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'chats',
|
name: 'chats',
|
||||||
path: '/users/:username/chats',
|
path: '/users/:username/chats',
|
||||||
component: ChatList,
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/chat_list/chat_list.vue'),
|
||||||
|
),
|
||||||
meta: { dontScroll: false },
|
meta: { dontScroll: false },
|
||||||
beforeEnter: validateAuthenticatedRoute,
|
beforeEnter: validateAuthenticatedRoute,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { mapState } from 'pinia'
|
import { mapState } from 'pinia'
|
||||||
|
|
||||||
import FeaturesPanel from '../features_panel/features_panel.vue'
|
import FeaturesPanel from 'src/components/features_panel/features_panel.vue'
|
||||||
import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue'
|
import InstanceSpecificPanel from 'src/components/instance_specific_panel/instance_specific_panel.vue'
|
||||||
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue'
|
import MRFTransparencyPanel from 'src/components/mrf_transparency_panel/mrf_transparency_panel.vue'
|
||||||
import StaffPanel from '../staff_panel/staff_panel.vue'
|
import StaffPanel from 'src/components/staff_panel/staff_panel.vue'
|
||||||
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
|
import TermsOfServicePanel from 'src/components/terms_of_service_panel/terms_of_service_panel.vue'
|
||||||
|
|
||||||
import { useInstanceStore } from 'src/stores/instance.js'
|
import { useInstanceStore } from 'src/stores/instance.js'
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
|
|
@ -27,7 +27,11 @@ const About = {
|
||||||
frontendVersionLink() {
|
frontendVersionLink() {
|
||||||
return pleromaFeCommitUrl + this.frontendVersion
|
return pleromaFeCommitUrl + this.frontendVersion
|
||||||
},
|
},
|
||||||
...mapState(useInstanceStore, ['backendVersion', 'backendRepository', 'frontendVersion']),
|
...mapState(useInstanceStore, [
|
||||||
|
'backendVersion',
|
||||||
|
'backendRepository',
|
||||||
|
'frontendVersion',
|
||||||
|
]),
|
||||||
showInstanceSpecificPanel() {
|
showInstanceSpecificPanel() {
|
||||||
return (
|
return (
|
||||||
useInstanceStore().instanceIdentity.showInstanceSpecificPanel &&
|
useInstanceStore().instanceIdentity.showInstanceSpecificPanel &&
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@
|
||||||
<features-panel v-if="showFeaturesPanel" />
|
<features-panel v-if="showFeaturesPanel" />
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<div class="title">{{ $t('settings.version.title') }}</div>
|
<div class="title">
|
||||||
|
{{ $t('settings.version.title') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<dl>
|
<dl>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { mapState } from 'pinia'
|
import { mapState } from 'pinia'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
import Popover from 'src/components/popover/popover.vue'
|
||||||
|
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||||
import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
|
import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
|
||||||
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
|
||||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
|
||||||
import Popover from '../popover/popover.vue'
|
|
||||||
import ProgressButton from '../progress_button/progress_button.vue'
|
|
||||||
|
|
||||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
|
|
@ -27,8 +26,15 @@ const AccountActions = {
|
||||||
ProgressButton,
|
ProgressButton,
|
||||||
Popover,
|
Popover,
|
||||||
UserListMenu,
|
UserListMenu,
|
||||||
ConfirmModal,
|
ConfirmModal: defineAsyncComponent(
|
||||||
UserTimedFilterModal,
|
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||||
|
),
|
||||||
|
UserTimedFilterModal: defineAsyncComponent(
|
||||||
|
() =>
|
||||||
|
import(
|
||||||
|
'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
||||||
|
),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showConfirmRemoveUserFromFollowers() {
|
showConfirmRemoveUserFromFollowers() {
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@
|
||||||
</template>
|
</template>
|
||||||
</Popover>
|
</Popover>
|
||||||
<teleport to="#modal">
|
<teleport to="#modal">
|
||||||
<confirm-modal
|
<ConfirmModal
|
||||||
v-if="showingConfirmBlock && !blockExpiration"
|
v-if="showingConfirmBlock && !blockExpiration"
|
||||||
ref="blockDialog"
|
ref="blockDialog"
|
||||||
:title="$t('user_card.block_confirm_title')"
|
:title="$t('user_card.block_confirm_title')"
|
||||||
|
|
@ -114,10 +114,10 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</confirm-modal>
|
</ConfirmModal>
|
||||||
</teleport>
|
</teleport>
|
||||||
<teleport to="#modal">
|
<teleport to="#modal">
|
||||||
<confirm-modal
|
<ConfirmModal
|
||||||
v-if="showingConfirmRemoveFollower"
|
v-if="showingConfirmRemoveFollower"
|
||||||
:title="$t('user_card.remove_follower_confirm_title')"
|
:title="$t('user_card.remove_follower_confirm_title')"
|
||||||
:confirm-text="$t('user_card.remove_follower_confirm_accept_button')"
|
:confirm-text="$t('user_card.remove_follower_confirm_accept_button')"
|
||||||
|
|
@ -136,7 +136,7 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</confirm-modal>
|
</ConfirmModal>
|
||||||
<UserTimedFilterModal
|
<UserTimedFilterModal
|
||||||
v-if="blockExpiration"
|
v-if="blockExpiration"
|
||||||
ref="timedBlockDialog"
|
ref="timedBlockDialog"
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export default {
|
||||||
validInnerComponents: ['Text', 'Icon', 'Link', 'Border', 'ButtonUnstyled'],
|
validInnerComponents: ['Text', 'Icon', 'Link', 'Border', 'ButtonUnstyled'],
|
||||||
variants: {
|
variants: {
|
||||||
normal: '.neutral',
|
normal: '.neutral',
|
||||||
|
info: '.info',
|
||||||
error: '.error',
|
error: '.error',
|
||||||
warning: '.warning',
|
warning: '.warning',
|
||||||
success: '.success',
|
success: '.success',
|
||||||
|
|
@ -47,5 +48,11 @@ export default {
|
||||||
background: '--cGreen',
|
background: '--cGreen',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
variant: 'info',
|
||||||
|
directives: {
|
||||||
|
background: '--cBlue',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
import AnnouncementEditor from 'src/components/announcement_editor/announcement_editor.vue'
|
||||||
import localeService from '../../services/locale/locale.service.js'
|
import localeService from '../../services/locale/locale.service.js'
|
||||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
|
||||||
import RichContent from '../rich_content/rich_content.jsx'
|
|
||||||
|
|
||||||
import { useAnnouncementsStore } from 'src/stores/announcements.js'
|
import { useAnnouncementsStore } from 'src/stores/announcements.js'
|
||||||
|
|
||||||
const Announcement = {
|
const Announcement = {
|
||||||
components: {
|
components: {
|
||||||
AnnouncementEditor,
|
AnnouncementEditor,
|
||||||
RichContent,
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -33,9 +31,7 @@ const Announcement = {
|
||||||
canEditAnnouncement() {
|
canEditAnnouncement() {
|
||||||
return (
|
return (
|
||||||
this.currentUser &&
|
this.currentUser &&
|
||||||
this.currentUser.privileges.includes(
|
this.currentUser.privileges.has('announcements_manage_announcements')
|
||||||
'announcements_manage_announcements',
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
content() {
|
content() {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
|
|
||||||
const AnnouncementEditor = {
|
const AnnouncementEditor = {
|
||||||
components: {
|
components: {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
import Announcement from '../announcement/announcement.vue'
|
import Announcement from 'src/components/announcement/announcement.vue'
|
||||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
import AnnouncementEditor from 'src/components/announcement_editor/announcement_editor.vue'
|
||||||
|
|
||||||
import { useAnnouncementsStore } from 'src/stores/announcements.js'
|
import { useAnnouncementsStore } from 'src/stores/announcements.js'
|
||||||
|
|
||||||
|
|
@ -35,9 +35,7 @@ const AnnouncementsPage = {
|
||||||
canPostAnnouncement() {
|
canPostAnnouncement() {
|
||||||
return (
|
return (
|
||||||
this.currentUser &&
|
this.currentUser &&
|
||||||
this.currentUser.privileges.includes(
|
this.currentUser.privileges.has('announcements_manage_announcements')
|
||||||
'announcements_manage_announcements',
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { mapState } from 'pinia'
|
import { mapState } from 'pinia'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
import Popover from 'src/components/popover/popover.vue'
|
||||||
import nsfwImage from '../../assets/nsfw.png'
|
import nsfwImage from '../../assets/nsfw.png'
|
||||||
import Flash from '../flash/flash.vue'
|
|
||||||
import StillImage from '../still-image/still-image.vue'
|
|
||||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
|
||||||
|
|
||||||
import { useInstanceStore } from 'src/stores/instance.js'
|
import { useInstanceStore } from 'src/stores/instance.js'
|
||||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||||
|
|
@ -53,6 +52,7 @@ const Attachment = {
|
||||||
'shiftDn',
|
'shiftDn',
|
||||||
'edit',
|
'edit',
|
||||||
],
|
],
|
||||||
|
emits: ['play', 'pause', 'naturalSizeLoad'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
localDescription: this.description || this.attachment.description,
|
localDescription: this.description || this.attachment.description,
|
||||||
|
|
@ -65,13 +65,15 @@ const Attachment = {
|
||||||
modalOpen: false,
|
modalOpen: false,
|
||||||
showHidden: false,
|
showHidden: false,
|
||||||
flashLoaded: false,
|
flashLoaded: false,
|
||||||
showDescription: false,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Flash,
|
Flash: defineAsyncComponent(() => import('src/components/flash/flash.vue')),
|
||||||
StillImage,
|
|
||||||
VideoAttachment,
|
VideoAttachment: defineAsyncComponent(
|
||||||
|
() => import('src/components/video_attachment/video_attachment.vue'),
|
||||||
|
),
|
||||||
|
Popover,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
classNames() {
|
classNames() {
|
||||||
|
|
@ -180,9 +182,6 @@ const Attachment = {
|
||||||
setFlashLoaded(event) {
|
setFlashLoaded(event) {
|
||||||
this.flashLoaded = event
|
this.flashLoaded = event
|
||||||
},
|
},
|
||||||
toggleDescription() {
|
|
||||||
this.showDescription = !this.showDescription
|
|
||||||
},
|
|
||||||
toggleHidden(event) {
|
toggleHidden(event) {
|
||||||
if (
|
if (
|
||||||
this.mergedConfig.useOneClickNsfw &&
|
this.mergedConfig.useOneClickNsfw &&
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@
|
||||||
width: 2em;
|
width: 2em;
|
||||||
height: 2em;
|
height: 2em;
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
font-size: 1.25em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,3 +265,27 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.description-popover {
|
||||||
|
padding: 1em;
|
||||||
|
width: 50ch;
|
||||||
|
max-width: 90vw;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: block;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 10.5em;
|
||||||
|
text-wrap: pretty;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,21 +30,16 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)"
|
v-if="size !== 'hide' && !hideDescription && edit"
|
||||||
class="description-container"
|
class="description-container"
|
||||||
:class="{ '-static': !edit }"
|
:class="{ '-static': !edit }"
|
||||||
>
|
>
|
||||||
<input
|
<textarea
|
||||||
v-if="edit"
|
|
||||||
v-model="localDescription"
|
v-model="localDescription"
|
||||||
type="text"
|
type="text"
|
||||||
class="input description-field"
|
class="input description-field"
|
||||||
:placeholder="$t('post_status.media_description')"
|
:placeholder="$t('post_status.media_description')"
|
||||||
@keydown.enter.prevent=""
|
/>
|
||||||
>
|
|
||||||
<p v-else>
|
|
||||||
{{ localDescription }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
|
|
@ -87,14 +82,22 @@
|
||||||
>
|
>
|
||||||
<FAIcon icon="stop" />
|
<FAIcon icon="stop" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<Popover
|
||||||
v-if="attachment.description && size !== 'small' && !edit && attachment.type !== 'unknown'"
|
v-if="attachment.description && size !== 'small' && !edit && attachment.type !== 'unknown'"
|
||||||
class="button-default attachment-button -transparent"
|
trigger="click"
|
||||||
:title="$t('status.show_attachment_description')"
|
popover-class="popover popover-default description-popover"
|
||||||
@click.prevent="toggleDescription"
|
:trigger-attrs="{ 'class': 'button-default attachment-button -transparent', 'title': $t('status.attachment_description') }"
|
||||||
>
|
>
|
||||||
<FAIcon icon="align-right" />
|
<template #trigger>
|
||||||
</button>
|
<FAIcon icon="align-right" />
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<details open>
|
||||||
|
<summary>{{ $t('status.attachment_description') }}</summary>
|
||||||
|
<span>{{ localDescription }}</span>
|
||||||
|
</details>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
<button
|
<button
|
||||||
v-if="!useModal && attachment.type !== 'unknown'"
|
v-if="!useModal && attachment.type !== 'unknown'"
|
||||||
class="button-default attachment-button -transparent"
|
class="button-default attachment-button -transparent"
|
||||||
|
|
@ -244,21 +247,16 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))"
|
v-if="size !== 'hide' && !hideDescription && edit"
|
||||||
class="description-container"
|
class="description-container"
|
||||||
:class="{ '-static': !edit }"
|
:class="{ '-static': !edit }"
|
||||||
>
|
>
|
||||||
<input
|
<textarea
|
||||||
v-if="edit"
|
|
||||||
v-model="localDescription"
|
v-model="localDescription"
|
||||||
type="text"
|
type="text"
|
||||||
class="input description-field"
|
class="input description-field"
|
||||||
:placeholder="$t('post_status.media_description')"
|
:placeholder="$t('post_status.media_description')"
|
||||||
@keydown.enter.prevent=""
|
/>
|
||||||
>
|
|
||||||
<p v-else>
|
|
||||||
{{ localDescription }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { mapState } from 'pinia'
|
import { mapState } from 'pinia'
|
||||||
import { h, resolveComponent } from 'vue'
|
import { h, resolveComponent } from 'vue'
|
||||||
|
|
||||||
import LoginForm from '../login_form/login_form.vue'
|
import LoginForm from 'src/components/login_form/login_form.vue'
|
||||||
import MFARecoveryForm from '../mfa_form/recovery_form.vue'
|
import MFARecoveryForm from 'src/components/mfa_form/recovery_form.vue'
|
||||||
import MFATOTPForm from '../mfa_form/totp_form.vue'
|
import MFATOTPForm from 'src/components/mfa_form/totp_form.vue'
|
||||||
|
|
||||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||||
|
|
||||||
import { useInstanceStore } from 'src/stores/instance.js'
|
import { useInstanceStore } from 'src/stores/instance.js'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserLink from 'src/components/user_link/user_link.vue'
|
||||||
import UserLink from '../user_link/user_link.vue'
|
import UserPopover from 'src/components/user_popover/user_popover.vue'
|
||||||
import UserPopover from '../user_popover/user_popover.vue'
|
|
||||||
|
|
||||||
import { useInstanceStore } from 'src/stores/instance.js'
|
import { useInstanceStore } from 'src/stores/instance.js'
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
|
|
@ -9,11 +8,19 @@ import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
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'
|
||||||
|
|
||||||
const BasicUserCard = {
|
const BasicUserCard = {
|
||||||
props: ['user'],
|
props: {
|
||||||
|
user: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
showLineLabels: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
UserPopover,
|
UserPopover,
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
RichContent,
|
|
||||||
UserLink,
|
UserLink,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -29,7 +36,7 @@ const BasicUserCard = {
|
||||||
allowNonSquareEmoji() {
|
allowNonSquareEmoji() {
|
||||||
return useMergedConfigStore().mergedConfig.nonSquareEmoji
|
return useMergedConfigStore().mergedConfig.nonSquareEmoji
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BasicUserCard
|
export default BasicUserCard
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,10 @@
|
||||||
:title="user.name"
|
:title="user.name"
|
||||||
class="basic-user-card-user-name"
|
class="basic-user-card-user-name"
|
||||||
>
|
>
|
||||||
|
<strong v-if="showLineLabels">
|
||||||
|
{{ $t('admin_dash.users.labels.name_colon') }}
|
||||||
|
{{ ' ' }}
|
||||||
|
</strong>
|
||||||
<RichContent
|
<RichContent
|
||||||
class="basic-user-card-user-name-value"
|
class="basic-user-card-user-name-value"
|
||||||
:html="user.name"
|
:html="user.name"
|
||||||
|
|
@ -31,6 +35,10 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
<strong v-if="showLineLabels">
|
||||||
|
{{ $t('admin_dash.users.labels.handle_colon') }}
|
||||||
|
{{ ' ' }}
|
||||||
|
</strong>
|
||||||
<user-link
|
<user-link
|
||||||
class="basic-user-card-screen-name"
|
class="basic-user-card-screen-name"
|
||||||
:user="user"
|
:user="user"
|
||||||
|
|
@ -46,8 +54,10 @@
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.basic-user-card {
|
.basic-user-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 0;
|
flex: 1 1 10em;
|
||||||
|
min-width: 1em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
line-height: 1.25;
|
||||||
|
|
||||||
--emoji-size: 1em;
|
--emoji-size: 1em;
|
||||||
|
|
||||||
|
|
@ -69,7 +79,7 @@
|
||||||
|
|
||||||
&-user-name-value,
|
&-user-name-value,
|
||||||
&-screen-name {
|
&-screen-name {
|
||||||
display: inline-block;
|
display: inline;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { mapState } from 'pinia'
|
import { mapState } from 'pinia'
|
||||||
|
|
||||||
|
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
|
||||||
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
||||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
|
||||||
|
|
||||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<basic-user-card :user="user">
|
<BasicUserCard :user="user">
|
||||||
<div class="block-card-content-container">
|
<div class="block-card-content-container">
|
||||||
<span
|
<span
|
||||||
v-if="blocked && blockExpiryAvailable"
|
v-if="blocked && blockExpiryAvailable"
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
:is-mute="false"
|
:is-mute="false"
|
||||||
/>
|
/>
|
||||||
</teleport>
|
</teleport>
|
||||||
</basic-user-card>
|
</BasicUserCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./block_card.js"></script>
|
<script src="./block_card.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
|
||||||
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
|
|
||||||
library.add(faEllipsisH)
|
|
||||||
|
|
||||||
const BookmarkFolderCard = {
|
|
||||||
props: ['folder', 'allBookmarks'],
|
|
||||||
computed: {
|
|
||||||
firstLetter() {
|
|
||||||
return this.folder ? this.folder.name[0] : null
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BookmarkFolderCard
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
v-if="allBookmarks"
|
|
||||||
class="bookmark-folder-card"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'bookmarks' }"
|
|
||||||
class="bookmark-folder-name"
|
|
||||||
>
|
|
||||||
<span class="icon">
|
|
||||||
<FAIcon
|
|
||||||
fixed-width
|
|
||||||
class="fa-scale-110 menu-icon"
|
|
||||||
icon="bookmark"
|
|
||||||
/>
|
|
||||||
</span>{{ $t('nav.all_bookmarks') }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="bookmark-folder-card"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'bookmark-folder', params: { id: folder.id } }"
|
|
||||||
class="bookmark-folder-name"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-if="folder.emoji_url"
|
|
||||||
class="iconEmoji iconEmoji-image"
|
|
||||||
:src="folder.emoji_url"
|
|
||||||
:alt="folder.emoji"
|
|
||||||
:title="folder.emoji"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-else-if="folder.emoji"
|
|
||||||
class="iconEmoji"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{{ folder.emoji }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-else-if="firstLetter"
|
|
||||||
class="icon iconLetter fa-scale-110"
|
|
||||||
>{{ firstLetter }}</span>{{ folder.name }}
|
|
||||||
</router-link>
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'bookmark-folder-edit', params: { id: folder.id } }"
|
|
||||||
class="button-folder-edit"
|
|
||||||
>
|
|
||||||
<FAIcon
|
|
||||||
class="fa-scale-110 fa-old-padding"
|
|
||||||
icon="ellipsis-h"
|
|
||||||
/>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./bookmark_folder_card.js"></script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.bookmark-folder-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.bookmark-folder-name {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
.icon,
|
|
||||||
.iconLetter,
|
|
||||||
.iconEmoji {
|
|
||||||
display: inline-block;
|
|
||||||
height: 2.5rem;
|
|
||||||
width: 2.5rem;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon,
|
|
||||||
.iconLetter {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
line-height: 2.5rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconEmoji {
|
|
||||||
text-align: center;
|
|
||||||
object-fit: contain;
|
|
||||||
vertical-align: middle;
|
|
||||||
|
|
||||||
> span {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
line-height: 2.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
img.iconEmoji {
|
|
||||||
padding: 0.25em;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bookmark-folder-name,
|
|
||||||
.button-folder-edit {
|
|
||||||
margin: 0;
|
|
||||||
padding: 1em;
|
|
||||||
color: var(--link);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import apiService from '../../services/api/api.service'
|
import EmojiPicker from 'src/components/emoji_picker/emoji_picker.vue'
|
||||||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
|
||||||
|
|
||||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
|
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
|
||||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||||
|
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||||
|
|
||||||
|
import { fetchBookmarkFolders } from 'src/api/user.js'
|
||||||
|
|
||||||
const BookmarkFolderEdit = {
|
const BookmarkFolderEdit = {
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -22,8 +24,10 @@ const BookmarkFolderEdit = {
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (!this.id) return
|
if (!this.id) return
|
||||||
const credentials = this.$store.state.users.currentUser.credentials
|
|
||||||
apiService.fetchBookmarkFolders({ credentials }).then((folders) => {
|
fetchBookmarkFolders({
|
||||||
|
credentials: useOAuthStore().token,
|
||||||
|
}).then(({ data: 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue'
|
import FolderCard from 'src/components/folder_card/folder_card.vue'
|
||||||
|
|
||||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
|
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@ const BookmarkFolders = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
BookmarkFolderCard,
|
FolderCard,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookmarkFolders() {
|
bookmarkFolders() {
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,28 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<BookmarkFolderCard
|
<div class="list-item FolderCard">
|
||||||
:all-bookmarks="true"
|
<router-link
|
||||||
class="list-item"
|
:to="{ name: 'bookmarks' }"
|
||||||
/>
|
class="folder-name"
|
||||||
<BookmarkFolderCard
|
>
|
||||||
|
<span class="icon">
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110 menu-icon"
|
||||||
|
icon="bookmark"
|
||||||
|
/>
|
||||||
|
</span>{{ $t('nav.all_bookmarks') }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<FolderCard
|
||||||
v-for="folder in bookmarkFolders.slice().reverse()"
|
v-for="folder in bookmarkFolders.slice().reverse()"
|
||||||
:key="folder"
|
:key="folder"
|
||||||
:folder="folder"
|
:name="folder.name"
|
||||||
|
:emoji="folder.emoji"
|
||||||
|
:emoji-url="folder.emoji_url"
|
||||||
|
:link="{ name: 'bookmark-folder', params: { id: folder.id } }"
|
||||||
|
:link-edit="{ name: 'bookmark-folder-edit', params: { id: folder.id } }"
|
||||||
class="list-item"
|
class="list-item"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import Timeline from '../timeline/timeline.vue'
|
import Timeline from 'src/components/timeline/timeline.vue'
|
||||||
|
|
||||||
const Bookmarks = {
|
const Bookmarks = {
|
||||||
created() {
|
created() {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import Timeline from '../timeline/timeline.vue'
|
import Timeline from 'src/components/timeline/timeline.vue'
|
||||||
|
|
||||||
const BubbleTimeline = {
|
const BubbleTimeline = {
|
||||||
components: {
|
components: {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import _ from 'lodash'
|
import { throttle } from 'lodash'
|
||||||
import { mapState as mapPiniaState } from 'pinia'
|
import { mapState as mapPiniaState } from 'pinia'
|
||||||
import { mapGetters, mapState } from 'vuex'
|
import { mapGetters, mapState } from 'vuex'
|
||||||
|
|
||||||
import { WSConnectionStatus } from '../../services/api/api.service.js'
|
import ChatMessage from 'src/components/chat_message/chat_message.vue'
|
||||||
|
import ChatTitle from 'src/components/chat_title/chat_title.vue'
|
||||||
|
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
|
||||||
import chatService from '../../services/chat_service/chat_service.js'
|
import chatService from '../../services/chat_service/chat_service.js'
|
||||||
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
|
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
|
||||||
import { promiseInterval } from '../../services/promise_interval/promise_interval.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 {
|
import {
|
||||||
getNewTopPosition,
|
getNewTopPosition,
|
||||||
getScrollPosition,
|
getScrollPosition,
|
||||||
|
|
@ -17,6 +16,14 @@ import {
|
||||||
} from './chat_layout_utils.js'
|
} from './chat_layout_utils.js'
|
||||||
|
|
||||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||||
|
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||||
|
|
||||||
|
import {
|
||||||
|
chatMessages,
|
||||||
|
getOrCreateChat,
|
||||||
|
sendChatMessage,
|
||||||
|
} from 'src/api/chats.js'
|
||||||
|
import { WSConnectionStatus } from 'src/api/websocket.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
|
import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
@ -115,7 +122,6 @@ const Chat = {
|
||||||
mobileLayout: (store) => store.layoutType === 'mobile',
|
mobileLayout: (store) => store.layoutType === 'mobile',
|
||||||
}),
|
}),
|
||||||
...mapState({
|
...mapState({
|
||||||
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,
|
||||||
}),
|
}),
|
||||||
|
|
@ -224,7 +230,7 @@ const Chat = {
|
||||||
}
|
}
|
||||||
}, 5000)
|
}, 5000)
|
||||||
},
|
},
|
||||||
handleScroll: _.throttle(function () {
|
handleScroll: throttle(function () {
|
||||||
this.lastScrollPosition = getScrollPosition()
|
this.lastScrollPosition = getScrollPosition()
|
||||||
if (!this.currentChat) {
|
if (!this.currentChat) {
|
||||||
return
|
return
|
||||||
|
|
@ -267,43 +273,48 @@ const Chat = {
|
||||||
const fetchOlderMessages = !!maxId
|
const fetchOlderMessages = !!maxId
|
||||||
const sinceId = fetchLatest && chatMessageService.maxId
|
const sinceId = fetchLatest && chatMessageService.maxId
|
||||||
|
|
||||||
return this.backendInteractor
|
return chatMessages({
|
||||||
.chatMessages({ id: chatId, maxId, sinceId })
|
id: chatId,
|
||||||
.then((messages) => {
|
maxId,
|
||||||
// Clear the current chat in case we're recovering from a ws connection loss.
|
sinceId,
|
||||||
if (isFirstFetch) {
|
credentials: useOAuthStore().token,
|
||||||
chatService.clear(chatMessageService)
|
}).then(({ data: messages }) => {
|
||||||
}
|
// Clear the current chat in case we're recovering from a ws connection loss.
|
||||||
|
if (isFirstFetch) {
|
||||||
|
chatService.clear(chatMessageService)
|
||||||
|
}
|
||||||
|
|
||||||
const positionBeforeUpdate = getScrollPosition()
|
const positionBeforeUpdate = getScrollPosition()
|
||||||
this.$store
|
this.$store
|
||||||
.dispatch('addChatMessages', { chatId, messages })
|
.dispatch('addChatMessages', { chatId, messages })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (fetchOlderMessages) {
|
if (fetchOlderMessages) {
|
||||||
this.handleScrollUp(positionBeforeUpdate)
|
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({
|
this.fetchChat({
|
||||||
maxId: this.currentChatMessageService.minId,
|
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({
|
const { data } = await getOrCreateChat({
|
||||||
accountId: this.recipientId,
|
accountId: this.recipientId,
|
||||||
|
credentials: useOAuthStore().token,
|
||||||
})
|
})
|
||||||
|
chat = data
|
||||||
} 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
|
||||||
|
|
@ -369,9 +380,11 @@ const Chat = {
|
||||||
doSendMessage({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
|
doSendMessage({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
|
||||||
if (retriesLeft <= 0) return
|
if (retriesLeft <= 0) return
|
||||||
|
|
||||||
this.backendInteractor
|
sendChatMessage({
|
||||||
.sendChatMessage(params)
|
params,
|
||||||
.then((data) => {
|
credentials: useOAuthStore().token,
|
||||||
|
})
|
||||||
|
.then(({ data }) => {
|
||||||
this.$store.dispatch('addChatMessages', {
|
this.$store.dispatch('addChatMessages', {
|
||||||
chatId: this.currentChat.id,
|
chatId: this.currentChat.id,
|
||||||
updateMaxId: false,
|
updateMaxId: false,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { mapGetters, mapState } from 'vuex'
|
import { mapGetters, mapState } from 'vuex'
|
||||||
|
|
||||||
import ChatListItem from '../chat_list_item/chat_list_item.vue'
|
import ChatListItem from 'src/components/chat_list_item/chat_list_item.vue'
|
||||||
import ChatNew from '../chat_new/chat_new.vue'
|
import ChatNew from 'src/components/chat_new/chat_new.vue'
|
||||||
import List from '../list/list.vue'
|
import List from 'src/components/list/list.vue'
|
||||||
|
|
||||||
const ChatList = {
|
const ChatList = {
|
||||||
components: {
|
components: {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
v-if="sortedChatList.length > 0"
|
v-if="sortedChatList.length > 0"
|
||||||
class="timeline"
|
class="timeline"
|
||||||
>
|
>
|
||||||
<List :items="sortedChatList">
|
<List :external-items="sortedChatList">
|
||||||
<template #item="{item}">
|
<template #item="{item}">
|
||||||
<ChatListItem
|
<ChatListItem
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
import AvatarList from '../avatar_list/avatar_list.vue'
|
import AvatarList from 'src/components/avatar_list/avatar_list.vue'
|
||||||
import ChatTitle from '../chat_title/chat_title.vue'
|
import ChatTitle from 'src/components/chat_title/chat_title.vue'
|
||||||
import StatusBody from '../status_content/status_content.vue'
|
import StatusBody from 'src/components/status_content/status_content.vue'
|
||||||
import Timeago from '../timeago/timeago.vue'
|
import Timeago from 'src/components/timeago/timeago.vue'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||||
|
|
||||||
const ChatListItem = {
|
const ChatListItem = {
|
||||||
name: 'ChatListItem',
|
name: 'ChatListItem',
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
:focus {
|
:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { mapState as mapPiniaState } from 'pinia'
|
import { mapState as mapPiniaState } from 'pinia'
|
||||||
import { defineAsyncComponent } from 'vue'
|
import { mapState } from 'vuex'
|
||||||
import { mapGetters, mapState } from 'vuex'
|
|
||||||
|
|
||||||
import Attachment from '../attachment/attachment.vue'
|
import Attachment from 'src/components/attachment/attachment.vue'
|
||||||
import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
|
import ChatMessageDate from 'src/components/chat_message_date/chat_message_date.vue'
|
||||||
import Gallery from '../gallery/gallery.vue'
|
import Gallery from 'src/components/gallery/gallery.vue'
|
||||||
import LinkPreview from '../link-preview/link-preview.vue'
|
import LinkPreview from 'src/components/link-preview/link-preview.vue'
|
||||||
import Popover from '../popover/popover.vue'
|
import Popover from 'src/components/popover/popover.vue'
|
||||||
import StatusContent from '../status_content/status_content.vue'
|
import StatusContent from 'src/components/status_content/status_content.vue'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||||
|
import UserPopover from 'src/components/user_popover/user_popover.vue'
|
||||||
|
|
||||||
import { useInstanceStore } from 'src/stores/instance.js'
|
import { useInstanceStore } from 'src/stores/instance.js'
|
||||||
import { useInterfaceStore } from 'src/stores/interface'
|
import { useInterfaceStore } from 'src/stores/interface'
|
||||||
|
|
@ -37,9 +37,7 @@ const ChatMessage = {
|
||||||
Gallery,
|
Gallery,
|
||||||
LinkPreview,
|
LinkPreview,
|
||||||
ChatMessageDate,
|
ChatMessageDate,
|
||||||
UserPopover: defineAsyncComponent(
|
UserPopover,
|
||||||
() => 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.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { mapGetters, mapState } from 'vuex'
|
import { mapGetters, mapState } from 'vuex'
|
||||||
|
|
||||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||||
|
|
||||||
|
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
|
import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
@ -22,7 +24,9 @@ const chatNew = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
const { chats } = await this.backendInteractor.chats()
|
const { chats } = await chats({
|
||||||
|
credentials: useOAuthStore().token,
|
||||||
|
})
|
||||||
chats.forEach((chat) => this.suggestions.push(chat.account))
|
chats.forEach((chat) => this.suggestions.push(chat.account))
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -38,7 +42,6 @@ const chatNew = {
|
||||||
},
|
},
|
||||||
...mapState({
|
...mapState({
|
||||||
currentUser: (state) => state.users.currentUser,
|
currentUser: (state) => state.users.currentUser,
|
||||||
backendInteractor: (state) => state.api.backendInteractor,
|
|
||||||
}),
|
}),
|
||||||
...mapGetters(['findUser']),
|
...mapGetters(['findUser']),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
import { defineAsyncComponent } from 'vue'
|
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||||
|
import UserPopover from 'src/components/user_popover/user_popover.vue'
|
||||||
|
|
||||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ChatTitle',
|
name: 'ChatTitle',
|
||||||
components: {
|
components: {
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
RichContent,
|
|
||||||
UserPopover: defineAsyncComponent(
|
UserPopover,
|
||||||
() => import('../user_popover/user_popover.vue'),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
props: ['user', 'withAvatar'],
|
props: ['user', 'withAvatar'],
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { throttle } from 'lodash'
|
import { throttle } from 'lodash'
|
||||||
|
|
||||||
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faEyeDropper } from '@fortawesome/free-solid-svg-icons'
|
import { faEyeDropper } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
@ -88,6 +88,7 @@ export default {
|
||||||
label: {
|
label: {
|
||||||
required: false,
|
required: false,
|
||||||
type: String,
|
type: String,
|
||||||
|
default: '',
|
||||||
},
|
},
|
||||||
// use unstyled, uh, style
|
// use unstyled, uh, style
|
||||||
unstyled: {
|
unstyled: {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
import DialogModal from '../dialog_modal/dialog_modal.vue'
|
import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { faCircleQuestion } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(faCircleQuestion)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component emits the following events:
|
* This component emits the following events:
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,20 @@
|
||||||
<span v-text="title" />
|
<span v-text="title" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<slot />
|
<div class="content">
|
||||||
|
<FAIcon
|
||||||
|
class="confirm-icon"
|
||||||
|
icon="circle-question"
|
||||||
|
size="3x"
|
||||||
|
fixed-width
|
||||||
|
/>
|
||||||
|
<div class="text">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="below">
|
||||||
|
<slot name="below" />
|
||||||
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<slot name="footerLeft" />
|
<slot name="footerLeft" />
|
||||||
<button
|
<button
|
||||||
|
|
@ -29,3 +41,43 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./confirm_modal.js"></script>
|
<script src="./confirm_modal.js"></script>
|
||||||
|
<style lang="scss">
|
||||||
|
.confirm-modal {
|
||||||
|
.confirm-icon {
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
margin-bottom: 0.75em;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.below:not(:empty) {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
max-width: 50ch;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
margin-right: 3.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { mapState } from 'pinia'
|
import { mapState } from 'pinia'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
import Select from 'src/components/select/select.vue'
|
import Select from 'src/components/select/select.vue'
|
||||||
import ConfirmModal from './confirm_modal.vue'
|
|
||||||
|
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
|
|
||||||
|
|
@ -12,7 +12,10 @@ export default {
|
||||||
showing: false,
|
showing: false,
|
||||||
}),
|
}),
|
||||||
components: {
|
components: {
|
||||||
ConfirmModal,
|
ConfirmModal: defineAsyncComponent(
|
||||||
|
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||||
|
),
|
||||||
|
|
||||||
Select,
|
Select,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<confirm-modal
|
<ConfirmModal
|
||||||
v-if="showing"
|
v-if="showing"
|
||||||
:title="$t('user_card.mute_confirm_title')"
|
:title="$t('user_card.mute_confirm_title')"
|
||||||
:confirm-text="$t('user_card.mute_confirm_accept_button')"
|
:confirm-text="$t('user_card.mute_confirm_accept_button')"
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
<span v-text="user.screen_name_ui" />
|
<span v-text="user.screen_name_ui" />
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</confirm-modal>
|
</ConfirmModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./mute_confirm.js" />
|
<script src="./mute_confirm.js" />
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import Conversation from '../conversation/conversation.vue'
|
import Conversation from 'src/components/conversation/conversation.vue'
|
||||||
|
|
||||||
const conversationPage = {
|
const conversationPage = {
|
||||||
components: {
|
components: {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<conversation
|
<Conversation
|
||||||
:collapsable="false"
|
|
||||||
is-page="true"
|
|
||||||
:status-id="statusId"
|
:status-id="statusId"
|
||||||
|
is-page
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,16 @@ import { clone, filter, findIndex, get, reduce } from 'lodash'
|
||||||
import { mapState as mapPiniaState } from 'pinia'
|
import { mapState as mapPiniaState } from 'pinia'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
import { WSConnectionStatus } from '../../services/api/api.service.js'
|
import QuickFilterSettings from 'src/components/quick_filter_settings/quick_filter_settings.vue'
|
||||||
import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.vue'
|
import QuickViewSettings from 'src/components/quick_view_settings/quick_view_settings.vue'
|
||||||
import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue'
|
import ThreadTree from 'src/components/thread_tree/thread_tree.vue'
|
||||||
import Status from '../status/status.vue'
|
|
||||||
import ThreadTree from '../thread_tree/thread_tree.vue'
|
|
||||||
|
|
||||||
import { useInterfaceStore } from 'src/stores/interface'
|
import { useInterfaceStore } from 'src/stores/interface'
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
|
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||||
|
|
||||||
|
import { fetchConversation, fetchStatus } from 'src/api/public.js'
|
||||||
|
import { WSConnectionStatus } from 'src/api/websocket.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
|
|
@ -53,25 +55,56 @@ const sortAndFilterConversation = (conversation, statusoid) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversation = {
|
const conversation = {
|
||||||
|
props: {
|
||||||
|
statusId: {
|
||||||
|
// Main thing
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
collapsable: {
|
||||||
|
// Whether conversation can be collapsed
|
||||||
|
// i.e. when it's not a page
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isPage: {
|
||||||
|
// Whether conversation is rendered as a standalone page
|
||||||
|
// as opposed to embedded into a timeline
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
pinnedStatusIdsObject: {
|
||||||
|
// Used for user profile, map of pinned statuses
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
inProfile: {
|
||||||
|
// Whether conversation is rendered in a user profile
|
||||||
|
// used for overriding muted status
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
profileUserId: {
|
||||||
|
// used with inProfile, user id of the profile
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
virtualHidden: {
|
||||||
|
// Whether conversation is suspended. Controls rendering of statuses
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
highlight: null,
|
focused: null,
|
||||||
expanded: false,
|
expanded: false,
|
||||||
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
|
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
|
||||||
statusContentPropertiesObject: {},
|
|
||||||
inlineDivePosition: null,
|
inlineDivePosition: null,
|
||||||
loadStatusError: null,
|
loadStatusError: null,
|
||||||
|
unsuspendibleIds: new Set(),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: [
|
|
||||||
'statusId',
|
|
||||||
'collapsable',
|
|
||||||
'isPage',
|
|
||||||
'pinnedStatusIdsObject',
|
|
||||||
'inProfile',
|
|
||||||
'profileUserId',
|
|
||||||
'virtualHidden',
|
|
||||||
],
|
|
||||||
created() {
|
created() {
|
||||||
if (this.isPage) {
|
if (this.isPage) {
|
||||||
this.fetchConversation()
|
this.fetchConversation()
|
||||||
|
|
@ -116,16 +149,7 @@ const conversation = {
|
||||||
return this.otherRepliesButtonPosition === 'inside'
|
return this.otherRepliesButtonPosition === 'inside'
|
||||||
},
|
},
|
||||||
suspendable() {
|
suspendable() {
|
||||||
if (this.isTreeView) {
|
return this.unsuspendibleIds.size > 0
|
||||||
return Object.entries(this.statusContentProperties).every(
|
|
||||||
([, prop]) => !prop.replying && prop.mediaPlaying.length === 0,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
|
|
||||||
return this.$refs.statusComponent.every((s) => s.suspendable)
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
hideStatus() {
|
hideStatus() {
|
||||||
return this.virtualHidden && this.suspendable
|
return this.virtualHidden && this.suspendable
|
||||||
|
|
@ -362,36 +386,11 @@ const conversation = {
|
||||||
return a
|
return a
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
statusContentProperties() {
|
|
||||||
return this.conversation.reduce((a, k) => {
|
|
||||||
const id = k.id
|
|
||||||
const props = (() => {
|
|
||||||
const def = {
|
|
||||||
showingTall: false,
|
|
||||||
expandingSubject: false,
|
|
||||||
showingLongSubject: false,
|
|
||||||
isReplying: false,
|
|
||||||
mediaPlaying: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.statusContentPropertiesObject[id]) {
|
|
||||||
return {
|
|
||||||
...def,
|
|
||||||
...this.statusContentPropertiesObject[id],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return def
|
|
||||||
})()
|
|
||||||
|
|
||||||
a[id] = props
|
|
||||||
return a
|
|
||||||
}, {})
|
|
||||||
},
|
|
||||||
canDive() {
|
canDive() {
|
||||||
return this.isTreeView && this.isExpanded
|
return this.isTreeView && this.isExpanded
|
||||||
},
|
},
|
||||||
maybeHighlight() {
|
maybeFocused() {
|
||||||
return this.isExpanded ? this.highlight : null
|
return this.isExpanded ? this.focused : null
|
||||||
},
|
},
|
||||||
...mapPiniaState(useMergedConfigStore, ['mergedConfig']),
|
...mapPiniaState(useMergedConfigStore, ['mergedConfig']),
|
||||||
...mapState({
|
...mapState({
|
||||||
|
|
@ -402,7 +401,6 @@ const conversation = {
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Status,
|
|
||||||
ThreadTree,
|
ThreadTree,
|
||||||
QuickFilterSettings,
|
QuickFilterSettings,
|
||||||
QuickViewSettings,
|
QuickViewSettings,
|
||||||
|
|
@ -416,7 +414,7 @@ const conversation = {
|
||||||
oldConversationId &&
|
oldConversationId &&
|
||||||
newConversationId === oldConversationId
|
newConversationId === oldConversationId
|
||||||
) {
|
) {
|
||||||
this.setHighlight(this.originalStatusId)
|
this.setFocused(this.originalStatusId)
|
||||||
} else {
|
} else {
|
||||||
this.fetchConversation()
|
this.fetchConversation()
|
||||||
}
|
}
|
||||||
|
|
@ -438,38 +436,36 @@ const conversation = {
|
||||||
methods: {
|
methods: {
|
||||||
fetchConversation() {
|
fetchConversation() {
|
||||||
if (this.status) {
|
if (this.status) {
|
||||||
this.$store.state.api.backendInteractor
|
fetchConversation({
|
||||||
.fetchConversation({ id: this.statusId })
|
id: this.statusId,
|
||||||
.then(({ ancestors, descendants }) => {
|
credentials: useOAuthStore().token,
|
||||||
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
|
}).then(({ data: { ancestors, descendants } }) => {
|
||||||
this.$store.dispatch('addNewStatuses', { statuses: descendants })
|
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
|
||||||
this.setHighlight(this.originalStatusId)
|
this.$store.dispatch('addNewStatuses', { statuses: descendants })
|
||||||
})
|
this.setFocused(this.originalStatusId)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
this.loadStatusError = null
|
this.loadStatusError = null
|
||||||
this.$store.state.api.backendInteractor
|
fetchStatus({
|
||||||
.fetchStatus({ id: this.statusId })
|
id: this.statusId,
|
||||||
.then((status) => {
|
credentials: useOAuthStore().token,
|
||||||
|
})
|
||||||
|
.then(({ data: status }) => {
|
||||||
this.$store.dispatch('addNewStatuses', { statuses: [status] })
|
this.$store.dispatch('addNewStatuses', { statuses: [status] })
|
||||||
this.fetchConversation()
|
this.fetchConversation()
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
console.error(error)
|
||||||
this.loadStatusError = error
|
this.loadStatusError = error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isFocused(id) {
|
|
||||||
return this.isExpanded && id === this.highlight
|
|
||||||
},
|
|
||||||
getReplies(id) {
|
getReplies(id) {
|
||||||
return this.replies[id] || []
|
return this.replies[id] || []
|
||||||
},
|
},
|
||||||
getHighlight() {
|
setFocused(id) {
|
||||||
return this.isExpanded ? this.highlight : null
|
|
||||||
},
|
|
||||||
setHighlight(id) {
|
|
||||||
if (!id) return
|
if (!id) return
|
||||||
this.highlight = id
|
this.focused = id
|
||||||
|
|
||||||
if (!this.streamingEnabled) {
|
if (!this.streamingEnabled) {
|
||||||
this.$store.dispatch('fetchStatus', id)
|
this.$store.dispatch('fetchStatus', id)
|
||||||
|
|
@ -509,22 +505,6 @@ const conversation = {
|
||||||
showThreadRecursively(id) {
|
showThreadRecursively(id) {
|
||||||
this.setThreadDisplayRecursively(id, 'showing')
|
this.setThreadDisplayRecursively(id, 'showing')
|
||||||
},
|
},
|
||||||
setStatusContentProperty(id, name, value) {
|
|
||||||
this.statusContentPropertiesObject = {
|
|
||||||
...this.statusContentPropertiesObject,
|
|
||||||
[id]: {
|
|
||||||
...this.statusContentPropertiesObject[id],
|
|
||||||
[name]: value,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toggleStatusContentProperty(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)
|
||||||
|
|
@ -550,7 +530,7 @@ const conversation = {
|
||||||
// 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.setFocused(this.statusId)
|
||||||
},
|
},
|
||||||
tryScrollTo(id) {
|
tryScrollTo(id) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
|
|
@ -568,7 +548,7 @@ const conversation = {
|
||||||
// contain scrolling calls, as we do not want the page to jump
|
// contain scrolling calls, as we do not want the page to jump
|
||||||
// when we scroll with an expanded conversation.
|
// when we scroll with an expanded conversation.
|
||||||
//
|
//
|
||||||
// Now the method is to rely solely on the `highlight` watcher
|
// Now the method is to rely solely on the `focused` watcher
|
||||||
// in `status` components.
|
// in `status` components.
|
||||||
// In linear views, all statuses are rendered at all times, but
|
// In linear views, all statuses are rendered at all times, but
|
||||||
// in tree views, it is possible that a change in active status
|
// in tree views, it is possible that a change in active status
|
||||||
|
|
@ -576,9 +556,9 @@ const conversation = {
|
||||||
// status becomes an ancestor status, and thus they will be
|
// status becomes an ancestor status, and thus they will be
|
||||||
// different).
|
// different).
|
||||||
// Here, let the components be rendered first, in order to trigger
|
// Here, let the components be rendered first, in order to trigger
|
||||||
// the `highlight` watcher.
|
// the `focused` watcher.
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.setHighlight(id)
|
this.setFocused(id)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
goToCurrent() {
|
goToCurrent() {
|
||||||
|
|
@ -624,6 +604,13 @@ const conversation = {
|
||||||
this.undive()
|
this.undive()
|
||||||
this.threadDisplayStatusObject = {}
|
this.threadDisplayStatusObject = {}
|
||||||
},
|
},
|
||||||
|
onStatusSuspendStateChange({ id, suspend }) {
|
||||||
|
if (!suspend) {
|
||||||
|
this.unsuspendibleIds.add(id)
|
||||||
|
} else {
|
||||||
|
this.unsuspendibleIds.delete(id)
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,8 +96,7 @@
|
||||||
:replies="getReplies(status.id)"
|
:replies="getReplies(status.id)"
|
||||||
|
|
||||||
:expandable="!isExpanded"
|
:expandable="!isExpanded"
|
||||||
:focused="isFocused(status.id)"
|
:focused="maybeFocused === status.id"
|
||||||
:highlight="getHighlight()"
|
|
||||||
:inline-expanded="collapsable && isExpanded"
|
:inline-expanded="collapsable && isExpanded"
|
||||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||||
:in-profile="inProfile"
|
:in-profile="inProfile"
|
||||||
|
|
@ -105,21 +104,11 @@
|
||||||
:profile-user-id="profileUserId"
|
:profile-user-id="profileUserId"
|
||||||
:simple-tree="treeViewIsSimple"
|
:simple-tree="treeViewIsSimple"
|
||||||
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
||||||
:dive="() => diveIntoStatus(status.id)"
|
can-dive
|
||||||
|
|
||||||
:controlled-showing-tall="statusContentProperties[status.id].showingTall"
|
@goto="setFocused"
|
||||||
:controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')"
|
@dive="() => diveIntoStatus(status.id)"
|
||||||
:controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
|
@suspendable-state-change="onStatusSuspendStateChange"
|
||||||
:controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')"
|
|
||||||
:controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject"
|
|
||||||
:controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')"
|
|
||||||
:controlled-replying="statusContentProperties[status.id].replying"
|
|
||||||
:controlled-toggle-replying="() => toggleStatusContentProperty(status.id, 'replying')"
|
|
||||||
:controlled-media-playing="statusContentProperties[status.id].mediaPlaying"
|
|
||||||
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
|
|
||||||
|
|
||||||
@goto="setHighlight"
|
|
||||||
@toggle-expanded="toggleExpanded"
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
||||||
|
|
@ -150,7 +139,7 @@
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<thread-tree
|
<ThreadTree
|
||||||
v-for="status in showingTopLevel"
|
v-for="status in showingTopLevel"
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
ref="statusComponent"
|
ref="statusComponent"
|
||||||
|
|
@ -164,22 +153,20 @@
|
||||||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||||
:profile-user-id="profileUserId"
|
:profile-user-id="profileUserId"
|
||||||
|
|
||||||
:is-focused-function="isFocused"
|
|
||||||
:get-replies="getReplies"
|
:get-replies="getReplies"
|
||||||
:highlight="maybeHighlight"
|
:focused="maybeFocused"
|
||||||
:set-highlight="setHighlight"
|
|
||||||
:toggle-expanded="toggleExpanded"
|
:toggle-expanded="toggleExpanded"
|
||||||
|
|
||||||
:simple="treeViewIsSimple"
|
:simple="treeViewIsSimple"
|
||||||
:toggle-thread-display="toggleThreadDisplay"
|
|
||||||
:thread-display-status="threadDisplayStatus"
|
:thread-display-status="threadDisplayStatus"
|
||||||
:show-thread-recursively="showThreadRecursively"
|
:show-thread-recursively="showThreadRecursively"
|
||||||
:total-reply-count="totalReplyCount"
|
:total-reply-count="totalReplyCount"
|
||||||
:total-reply-depth="totalReplyDepth"
|
:total-reply-depth="totalReplyDepth"
|
||||||
:status-content-properties="statusContentProperties"
|
:can-dive="canDive"
|
||||||
:set-status-content-property="setStatusContentProperty"
|
|
||||||
:toggle-status-content-property="toggleStatusContentProperty"
|
@goto="setFocused"
|
||||||
:dive="canDive ? diveIntoStatus : undefined"
|
@dive="diveIntoStatus"
|
||||||
|
@suspendable-state-change="onStatusSuspendStateChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
@ -187,7 +174,7 @@
|
||||||
class="thread-body"
|
class="thread-body"
|
||||||
>
|
>
|
||||||
<article>
|
<article>
|
||||||
<status
|
<Status
|
||||||
v-for="status in conversation"
|
v-for="status in conversation"
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
ref="statusComponent"
|
ref="statusComponent"
|
||||||
|
|
@ -196,16 +183,16 @@
|
||||||
:replies="getReplies(status.id)"
|
:replies="getReplies(status.id)"
|
||||||
|
|
||||||
:expandable="!isExpanded"
|
:expandable="!isExpanded"
|
||||||
:focused="isFocused(status.id)"
|
:focused="maybeFocused === status.id || maybeFocused === status.retweeted_status?.id"
|
||||||
:highlight="getHighlight()"
|
|
||||||
:inline-expanded="collapsable && isExpanded"
|
:inline-expanded="collapsable && isExpanded"
|
||||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||||
:in-profile="inProfile"
|
:in-profile="inProfile"
|
||||||
:in-conversation="isExpanded"
|
:in-conversation="isExpanded"
|
||||||
:profile-user-id="profileUserId"
|
:profile-user-id="profileUserId"
|
||||||
|
|
||||||
@goto="setHighlight"
|
@goto="setFocused"
|
||||||
@toggle-expanded="toggleExpanded"
|
@toggle-expanded="toggleExpanded"
|
||||||
|
@suspendable-state-change="onStatusSuspendStateChange"
|
||||||
/>
|
/>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import SearchBar from 'components/search_bar/search_bar.vue'
|
import SearchBar from 'components/search_bar/search_bar.vue'
|
||||||
import { mapActions, mapState } from 'pinia'
|
import { mapActions, mapState } from 'pinia'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
|
||||||
|
|
||||||
import { useInstanceStore } from 'src/stores/instance.js'
|
import { useInstanceStore } from 'src/stores/instance.js'
|
||||||
import { useInterfaceStore } from 'src/stores/interface'
|
import { useInterfaceStore } from 'src/stores/interface'
|
||||||
|
|
@ -39,7 +38,9 @@ library.add(
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
SearchBar,
|
SearchBar,
|
||||||
ConfirmModal,
|
ConfirmModal: defineAsyncComponent(
|
||||||
|
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
searchBarHidden: true,
|
searchBarHidden: true,
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<teleport to="#modal">
|
<teleport to="#modal">
|
||||||
<confirm-modal
|
<ConfirmModal
|
||||||
v-if="showingConfirmLogout"
|
v-if="showingConfirmLogout"
|
||||||
:title="$t('login.logout_confirm_title')"
|
:title="$t('login.logout_confirm_title')"
|
||||||
:confirm-danger="true"
|
:confirm-danger="true"
|
||||||
|
|
@ -89,7 +89,7 @@
|
||||||
@cancelled="hideConfirmLogout"
|
@cancelled="hideConfirmLogout"
|
||||||
>
|
>
|
||||||
{{ $t('login.logout_confirm') }}
|
{{ $t('login.logout_confirm') }}
|
||||||
</confirm-modal>
|
</ConfirmModal>
|
||||||
</teleport>
|
</teleport>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import Timeline from '../timeline/timeline.vue'
|
import Timeline from 'src/components/timeline/timeline.vue'
|
||||||
|
|
||||||
const DMs = {
|
const DMs = {
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import ProgressButton from '../progress_button/progress_button.vue'
|
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||||
|
|
||||||
const DomainMuteCard = {
|
const DomainMuteCard = {
|
||||||
props: ['domain'],
|
props: ['domain'],
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
|
|
||||||
import EditStatusForm from 'src/components/edit_status_form/edit_status_form.vue'
|
|
||||||
import Gallery from 'src/components/gallery/gallery.vue'
|
import Gallery from 'src/components/gallery/gallery.vue'
|
||||||
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
|
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
|
||||||
import StatusContent from 'src/components/status_content/status_content.vue'
|
import StatusContent from 'src/components/status_content/status_content.vue'
|
||||||
|
|
@ -16,8 +15,12 @@ library.add(faPollH)
|
||||||
const Draft = {
|
const Draft = {
|
||||||
components: {
|
components: {
|
||||||
PostStatusForm,
|
PostStatusForm,
|
||||||
EditStatusForm,
|
EditStatusForm: defineAsyncComponent(
|
||||||
ConfirmModal,
|
() => import('src/components/edit_status_form/edit_status_form.vue'),
|
||||||
|
),
|
||||||
|
ConfirmModal: defineAsyncComponent(
|
||||||
|
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||||
|
),
|
||||||
StatusContent,
|
StatusContent,
|
||||||
Gallery,
|
Gallery,
|
||||||
},
|
},
|
||||||
|
|
@ -66,15 +69,6 @@ const Draft = {
|
||||||
localCollapseSubjectDefault() {
|
localCollapseSubjectDefault() {
|
||||||
return useMergedConfigStore().mergedConfig.collapseMessageWithSubject
|
return useMergedConfigStore().mergedConfig.collapseMessageWithSubject
|
||||||
},
|
},
|
||||||
nsfwClickthrough() {
|
|
||||||
if (!this.draft.nsfw) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (this.draft.summary && this.localCollapseSubjectDefault) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
editing(newVal) {
|
editing(newVal) {
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
class="faint"
|
class="faint"
|
||||||
>{{ $t('drafts.empty') }}</p>
|
>{{ $t('drafts.empty') }}</p>
|
||||||
</span>
|
</span>
|
||||||
<gallery
|
<Gallery
|
||||||
v-if="draft.files?.length !== 0"
|
v-if="draft.files?.length !== 0"
|
||||||
class="attachments media-body"
|
class="attachments media-body"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
|
|
@ -77,7 +77,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<teleport to="#modal">
|
<teleport to="#modal">
|
||||||
<confirm-modal
|
<ConfirmModal
|
||||||
v-if="showingConfirmDialog"
|
v-if="showingConfirmDialog"
|
||||||
:title="$t('drafts.abandon_confirm_title')"
|
:title="$t('drafts.abandon_confirm_title')"
|
||||||
:confirm-text="$t('drafts.abandon_confirm_accept_button')"
|
:confirm-text="$t('drafts.abandon_confirm_accept_button')"
|
||||||
|
|
@ -86,7 +86,7 @@
|
||||||
@cancelled="hideConfirmDialog"
|
@cancelled="hideConfirmDialog"
|
||||||
>
|
>
|
||||||
{{ $t('drafts.abandon_confirm') }}
|
{{ $t('drafts.abandon_confirm') }}
|
||||||
</confirm-modal>
|
</ConfirmModal>
|
||||||
</teleport>
|
</teleport>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
|
|
||||||
|
|
@ -9,7 +9,9 @@ const DraftCloser = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
DialogModal,
|
DialogModal: defineAsyncComponent(
|
||||||
|
() => import('src/components/dialog_modal/dialog_modal.vue'),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
emits: ['save', 'discard'],
|
emits: ['save', 'discard'],
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<teleport to="#modal">
|
<teleport to="#modal">
|
||||||
<dialog-modal
|
<DialogModal
|
||||||
v-if="showing"
|
v-if="showing"
|
||||||
v-body-scroll-lock="true"
|
v-body-scroll-lock="true"
|
||||||
class="confirm-modal"
|
class="confirm-modal"
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
{{ $t('post_status.close_confirm_continue_composing_button') }}
|
{{ $t('post_status.close_confirm_continue_composing_button') }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</dialog-modal>
|
</DialogModal>
|
||||||
</teleport>
|
</teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
import Draft from 'src/components/draft/draft.vue'
|
import Draft from 'src/components/draft/draft.vue'
|
||||||
import List from 'src/components/list/list.vue'
|
import List from 'src/components/list/list.vue'
|
||||||
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
|
|
||||||
|
|
||||||
const Drafts = {
|
const Drafts = {
|
||||||
components: {
|
components: {
|
||||||
Draft,
|
Draft,
|
||||||
List,
|
List,
|
||||||
ConfirmModal,
|
ConfirmModal: defineAsyncComponent(
|
||||||
|
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showingConfirmDialog: false
|
showingConfirmDialog: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -23,12 +26,14 @@ const Drafts = {
|
||||||
this.showingConfirmDialog = true
|
this.showingConfirmDialog = true
|
||||||
},
|
},
|
||||||
doAbandonAll() {
|
doAbandonAll() {
|
||||||
this.$store.dispatch('abandonAllDrafts').then(() => this.hideConfirmDialog())
|
this.$store
|
||||||
|
.dispatch('abandonAllDrafts')
|
||||||
|
.then(() => this.hideConfirmDialog())
|
||||||
},
|
},
|
||||||
hideConfirmDialog() {
|
hideConfirmDialog() {
|
||||||
this.showingConfirmDialog = false
|
this.showingConfirmDialog = false
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Drafts
|
export default Drafts
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<List
|
<List
|
||||||
:items="drafts"
|
:external-items="drafts"
|
||||||
:non-interactive="true"
|
:non-interactive="true"
|
||||||
>
|
>
|
||||||
<template #item="{ item: draft }">
|
<template #item="{ item: draft }">
|
||||||
|
|
@ -58,6 +58,7 @@
|
||||||
.Drafts {
|
.Drafts {
|
||||||
.draft {
|
.draft {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remove-all {
|
.remove-all {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
import PostStatusForm from 'src/components/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: {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import get from 'lodash/get'
|
import { get } from 'lodash'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
import EditStatusForm from '../edit_status_form/edit_status_form.vue'
|
import Modal from 'src/components/modal/modal.vue'
|
||||||
import Modal from '../modal/modal.vue'
|
|
||||||
|
|
||||||
import { useEditStatusStore } from 'src/stores/editStatus.js'
|
import { useEditStatusStore } from 'src/stores/editStatus.js'
|
||||||
|
|
||||||
const EditStatusModal = {
|
const EditStatusModal = {
|
||||||
components: {
|
components: {
|
||||||
EditStatusForm,
|
EditStatusForm: defineAsyncComponent(
|
||||||
|
() => import('src/components/edit_status_form/edit_status_form.vue'),
|
||||||
|
),
|
||||||
Modal,
|
Modal,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
:params="params"
|
:params="params"
|
||||||
@posted="doCloseModal"
|
@posted="doCloseModal"
|
||||||
@draft-done="doCloseModal"
|
@draft-done="doCloseModal"
|
||||||
@can-close="doCloseModal"
|
@close-accepted="doCloseModal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -333,7 +333,6 @@ const EmojiInput = {
|
||||||
if (!this.pickerShown) {
|
if (!this.pickerShown) {
|
||||||
this.scrollIntoView()
|
this.scrollIntoView()
|
||||||
this.$refs.picker.showPicker()
|
this.$refs.picker.showPicker()
|
||||||
this.$refs.picker.startEmojiLoad()
|
|
||||||
} else {
|
} else {
|
||||||
this.$refs.picker.hidePicker()
|
this.$refs.picker.hidePicker()
|
||||||
}
|
}
|
||||||
|
|
@ -590,7 +589,7 @@ const EmojiInput = {
|
||||||
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) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<div
|
<div
|
||||||
ref="root"
|
ref="root"
|
||||||
class="input emoji-input"
|
class="input emoji-input"
|
||||||
:class="{ '-with-picker': !hideEmojiButton, '-textarea': this.input?.tagName === 'TEXTAREA' }"
|
:class="{ '-with-picker': !hideEmojiButton, '-textarea': input?.tagName === 'TEXTAREA' }"
|
||||||
>
|
>
|
||||||
<slot
|
<slot
|
||||||
:id="'textbox-' + randomSeed"
|
:id="'textbox-' + randomSeed"
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
:title="$t('emoji.add_emoji')"
|
:title="$t('emoji.add_emoji')"
|
||||||
@click.prevent="togglePicker"
|
@click.prevent="togglePicker"
|
||||||
>
|
>
|
||||||
<FAIcon :icon="['far', 'smile-beam']" />
|
<FAIcon :icon="['far', 'face-smile-beam']" />
|
||||||
</button>
|
</button>
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
v-if="enableEmojiPicker"
|
v-if="enableEmojiPicker"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { useEmojiStore } from 'src/stores/emoji.js'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* suggest - generates a suggestor function to be used by emoji-input
|
* suggest - generates a suggestor function to be used by emoji-input
|
||||||
* data: object providing source information for specific types of suggestions:
|
* data: object providing source information for specific types of suggestions:
|
||||||
|
|
@ -28,14 +26,14 @@ 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().substring(1)
|
||||||
return emojis
|
return emojis
|
||||||
.map((emoji) => ({ ...emoji, ...nameKeywordLocalizer(emoji) }))
|
.map((emoji) => ({ ...emoji, ...nameKeywordLocalizer(emoji) }))
|
||||||
.filter(
|
.filter(
|
||||||
(emoji) =>
|
(emoji) =>
|
||||||
emoji.names
|
emoji.names
|
||||||
.concat(emoji.keywords)
|
.concat(emoji.keywords)
|
||||||
.filter((kw) => kw.toLowerCase().match(noPrefix)).length,
|
.filter((kw) => kw.toLowerCase().includes(noPrefix)).length,
|
||||||
)
|
)
|
||||||
.map((k) => {
|
.map((k) => {
|
||||||
let score = 0
|
let score = 0
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import { chunk, debounce, trim } from 'lodash'
|
import { chunk, debounce, trim } from 'lodash'
|
||||||
import { defineAsyncComponent } from 'vue'
|
import { defineAsyncComponent } from '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 { 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 { useEmojiStore } from 'src/stores/emoji.js'
|
import { useEmojiStore } from 'src/stores/emoji.js'
|
||||||
import { useInstanceStore } from 'src/stores/instance.js'
|
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
|
@ -129,6 +127,7 @@ const EmojiPicker = {
|
||||||
hideCustomEmojiInPicker: false,
|
hideCustomEmojiInPicker: false,
|
||||||
// Lazy-load only after the first time `showing` becomes true.
|
// Lazy-load only after the first time `showing` becomes true.
|
||||||
contentLoaded: false,
|
contentLoaded: false,
|
||||||
|
popoverShown: false,
|
||||||
groupRefs: {},
|
groupRefs: {},
|
||||||
emojiRefs: {},
|
emojiRefs: {},
|
||||||
filteredEmojiGroups: [],
|
filteredEmojiGroups: [],
|
||||||
|
|
@ -138,10 +137,10 @@ const EmojiPicker = {
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
StickerPicker: defineAsyncComponent(
|
StickerPicker: defineAsyncComponent(
|
||||||
() => import('../sticker_picker/sticker_picker.vue'),
|
() => import('src/components/sticker_picker/sticker_picker.vue'),
|
||||||
),
|
),
|
||||||
Checkbox,
|
Checkbox,
|
||||||
StillImage,
|
|
||||||
Popover,
|
Popover,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -176,6 +175,13 @@ const EmojiPicker = {
|
||||||
const fullEmojiSize = emojiSizeReal + 2 * 0.2 * fontSizeMultiplier * 14
|
const fullEmojiSize = emojiSizeReal + 2 * 0.2 * fontSizeMultiplier * 14
|
||||||
this.emojiSize = fullEmojiSize
|
this.emojiSize = fullEmojiSize
|
||||||
},
|
},
|
||||||
|
togglePicker() {
|
||||||
|
if (this.popoverShown) {
|
||||||
|
this.hidePicker()
|
||||||
|
} else {
|
||||||
|
this.showPicker()
|
||||||
|
}
|
||||||
|
},
|
||||||
showPicker() {
|
showPicker() {
|
||||||
this.$refs.popover.showPopover()
|
this.$refs.popover.showPopover()
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
|
@ -194,10 +200,10 @@ const EmojiPicker = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPopoverShown() {
|
onPopoverShown() {
|
||||||
this.$emit('show')
|
this.popoverShown = true
|
||||||
},
|
},
|
||||||
onPopoverClosed() {
|
onPopoverClosed() {
|
||||||
this.$emit('close')
|
this.popoverShown = false
|
||||||
},
|
},
|
||||||
onStickerUploaded(e) {
|
onStickerUploaded(e) {
|
||||||
this.$emit('sticker-uploaded', e)
|
this.$emit('sticker-uploaded', e)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
trigger="click"
|
trigger="click"
|
||||||
popover-class="emoji-picker popover-default"
|
popover-class="emoji-picker popover-default"
|
||||||
:hide-trigger="true"
|
:hide-trigger="true"
|
||||||
|
placement="bottom"
|
||||||
@show="onPopoverShown"
|
@show="onPopoverShown"
|
||||||
@close="onPopoverClosed"
|
@close="onPopoverClosed"
|
||||||
>
|
>
|
||||||
|
|
@ -48,7 +49,7 @@
|
||||||
v-if="group.image"
|
v-if="group.image"
|
||||||
class="emoji-picker-header-image"
|
class="emoji-picker-header-image"
|
||||||
>
|
>
|
||||||
<still-image
|
<StillImage
|
||||||
:alt="group.text"
|
:alt="group.text"
|
||||||
:src="group.image"
|
:src="group.image"
|
||||||
/>
|
/>
|
||||||
|
|
@ -130,7 +131,7 @@
|
||||||
v-if="!emoji.imageUrl"
|
v-if="!emoji.imageUrl"
|
||||||
class="emoji-picker-emoji -unicode"
|
class="emoji-picker-emoji -unicode"
|
||||||
>{{ emoji.replacement }}</span>
|
>{{ emoji.replacement }}</span>
|
||||||
<still-image
|
<StillImage
|
||||||
v-else
|
v-else
|
||||||
class="emoji-picker-emoji -custom"
|
class="emoji-picker-emoji -custom"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import StillImage from 'src/components/still-image/still-image.vue'
|
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserListPopover from 'src/components/user_list_popover/user_list_popover.vue'
|
||||||
import UserListPopover from '../user_list_popover/user_list_popover.vue'
|
|
||||||
|
|
||||||
import { useInstanceStore } from 'src/stores/instance.js'
|
import { useInstanceStore } from 'src/stores/instance.js'
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
|
|
@ -17,7 +16,6 @@ const EmojiReactions = {
|
||||||
components: {
|
components: {
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
UserListPopover,
|
UserListPopover,
|
||||||
StillImage,
|
|
||||||
},
|
},
|
||||||
props: ['status'],
|
props: ['status'],
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
|
|
||||||
36
src/components/error_modal/error_modal.js
Normal file
36
src/components/error_modal/error_modal.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { faCircleXmark } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(faCircleXmark)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component emits the following events:
|
||||||
|
* cancelled, emitted when the action should not be performed;
|
||||||
|
* accepted, emitted when the action should be performed;
|
||||||
|
*
|
||||||
|
* The caller should close this dialog after receiving any of the two events.
|
||||||
|
*/
|
||||||
|
const ErrorModal = {
|
||||||
|
components: {
|
||||||
|
DialogModal,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
clearText: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
recoverText: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: Error,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['clear', 'recover'],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorModal
|
||||||
103
src/components/error_modal/error_modal.vue
Normal file
103
src/components/error_modal/error_modal.vue
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
<template>
|
||||||
|
<DialogModal
|
||||||
|
v-body-scroll-lock="true"
|
||||||
|
class="error-modal"
|
||||||
|
@cancel="onCancel"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span v-text="title ?? $t('general.generic_error')" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<FAIcon
|
||||||
|
class="error-icon"
|
||||||
|
icon="circle-xmark"
|
||||||
|
size="3x"
|
||||||
|
fixed-width
|
||||||
|
/>
|
||||||
|
<div class="text">
|
||||||
|
<slot>
|
||||||
|
<p>
|
||||||
|
<strong><code v-text="error.name" /></strong>
|
||||||
|
{{ ' - ' }}
|
||||||
|
<span v-text="error.message" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary>{{ $t('general.generic_error_details') }}</summary>
|
||||||
|
<code
|
||||||
|
class="stack pre"
|
||||||
|
v-text="error.stack"
|
||||||
|
/>
|
||||||
|
</details>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="below">
|
||||||
|
<slot name="below" />
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<slot name="footerLeft" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="recoverText"
|
||||||
|
class="btn button-default"
|
||||||
|
@click.prevent="$emit('recover')"
|
||||||
|
v-text="recoverText"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
@click.prevent="$emit('clear')"
|
||||||
|
v-text="clearText ?? $t('general.close')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./error_modal.js"></script>
|
||||||
|
<style lang="scss">
|
||||||
|
.error-modal {
|
||||||
|
.error-icon {
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
margin-bottom: 0.75em;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack {
|
||||||
|
margin: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.below:not(:empty) {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
max-width: 50ch;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
margin-right: 3.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
38
src/components/folder_card/folder_card.js
Normal file
38
src/components/folder_card/folder_card.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(faEllipsisH)
|
||||||
|
|
||||||
|
const FolderCard = {
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
emoji: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
emojiUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
linkEdit: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
firstLetter() {
|
||||||
|
return this.name[0]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FolderCard
|
||||||
84
src/components/folder_card/folder_card.vue
Normal file
84
src/components/folder_card/folder_card.vue
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
<template>
|
||||||
|
<div class="FolderCard">
|
||||||
|
<router-link
|
||||||
|
:to="link"
|
||||||
|
class="folder-name"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="emojiUrl"
|
||||||
|
class="iconEmoji iconEmoji-image"
|
||||||
|
:src="emojiUrl"
|
||||||
|
:alt="emoji"
|
||||||
|
:title="emoji"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-else-if="emoji"
|
||||||
|
class="iconEmoji"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ emoji }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="firstLetter"
|
||||||
|
class="icon iconLetter fa-scale-110"
|
||||||
|
>{{ firstLetter }}</span>{{ name }}
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
:to="linkEdit"
|
||||||
|
class="button-folder-edit"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="ellipsis-h"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./folder_card.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.FolderCard {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.folder-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.icon,
|
||||||
|
.iconLetter,
|
||||||
|
.iconEmoji {
|
||||||
|
display: inline-block;
|
||||||
|
height: 2.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon,
|
||||||
|
.iconLetter {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 2.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconEmoji {
|
||||||
|
text-align: center;
|
||||||
|
object-fit: contain;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img.iconEmoji {
|
||||||
|
padding: 0.25em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
requestFollow,
|
requestFollow,
|
||||||
requestUnfollow,
|
requestUnfollow,
|
||||||
} from '../../services/follow_manipulate/follow_manipulate'
|
} from '../../services/follow_manipulate/follow_manipulate'
|
||||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
|
||||||
|
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
export default {
|
export default {
|
||||||
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
|
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
|
||||||
components: {
|
components: {
|
||||||
ConfirmModal,
|
ConfirmModal: defineAsyncComponent(
|
||||||
|
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
<teleport to="#modal">
|
<teleport to="#modal">
|
||||||
<confirm-modal
|
<ConfirmModal
|
||||||
v-if="showingConfirmUnfollow"
|
v-if="showingConfirmUnfollow"
|
||||||
:title="$t('user_card.unfollow_confirm_title')"
|
:title="$t('user_card.unfollow_confirm_title')"
|
||||||
:confirm-text="$t('user_card.unfollow_confirm_accept_button')"
|
:confirm-text="$t('user_card.unfollow_confirm_accept_button')"
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</confirm-modal>
|
</ConfirmModal>
|
||||||
</teleport>
|
</teleport>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
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