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.
|
||||
# Official framework image. Look for the different tagged releases at:
|
||||
# https://hub.docker.com/r/library/node/tags/
|
||||
image: node:18
|
||||
image: node:20
|
||||
|
||||
stages:
|
||||
- check-changelog
|
||||
|
|
@ -73,7 +73,7 @@ test:
|
|||
|
||||
e2e-pleroma:
|
||||
stage: test
|
||||
image: mcr.microsoft.com/playwright:v1.57.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.61.0-jammy
|
||||
services:
|
||||
- name: postgres:15-alpine
|
||||
alias: db
|
||||
|
|
@ -157,7 +157,7 @@ e2e-pleroma:
|
|||
variables:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
||||
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_PASSWORD: pleroma
|
||||
POSTGRES_DB: pleroma
|
||||
|
|
@ -175,7 +175,7 @@ e2e-pleroma:
|
|||
NOTIFY_EMAIL: $E2E_ADMIN_EMAIL
|
||||
VITE_PROXY_TARGET: http://pleroma:4000
|
||||
VITE_PROXY_ORIGIN: http://localhost:4000
|
||||
E2E_BASE_URL: http://localhost:8080
|
||||
E2E_BASE_URL: http://localhost:8099
|
||||
script:
|
||||
- npm install -g yarn@1.22.22
|
||||
- yarn --frozen-lockfile
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
18.20.8
|
||||
20.19.0
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ labels:
|
|||
|
||||
steps:
|
||||
build:
|
||||
image: docker.io/node:18-alpine
|
||||
image: docker.io/node:20-alpine
|
||||
commands:
|
||||
- apk add --no-cache zip git
|
||||
- yarn --frozen-lockfile
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ when:
|
|||
steps:
|
||||
install-depends:
|
||||
image: &node-image
|
||||
docker.io/node:18-alpine
|
||||
docker.io/node:20-alpine
|
||||
commands:
|
||||
- yarn --frozen-lockfile
|
||||
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ variables:
|
|||
|
||||
steps:
|
||||
test:
|
||||
image: mcr.microsoft.com/playwright:v1.57.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.61.0-jammy
|
||||
entrypoint: *script_file_entrypoint
|
||||
environment:
|
||||
APT_CACHE_DIR: apt-cache
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
E2E_BASE_URL: http://localhost:8080
|
||||
E2E_BASE_URL: http://localhost:8099
|
||||
FF_NETWORK_PER_BUILD: "true"
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
||||
VITE_PROXY_ORIGIN: "http://pleroma:4000"
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ variables:
|
|||
|
||||
steps:
|
||||
test:
|
||||
image: mcr.microsoft.com/playwright:v1.57.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.61.0-jammy
|
||||
environment:
|
||||
APT_CACHE_DIR: apt-cache
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
"noUnusedLabels": "error",
|
||||
"noUnusedPrivateClassMembers": "error",
|
||||
"noUnusedVariables": "error",
|
||||
"noUnusedImports": "error",
|
||||
"useIsNan": "error",
|
||||
"useValidForDirection": "error",
|
||||
"useValidTypeof": "error",
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ export default function () {
|
|||
const warning = warnings[i]
|
||||
console.warn(' ' + warning)
|
||||
}
|
||||
console.warn()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { readFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import * as esbuild from 'esbuild'
|
||||
import { exactRegex } from '@rolldown/pluginutils'
|
||||
import { build } from 'vite'
|
||||
|
||||
import {
|
||||
|
|
@ -15,106 +14,6 @@ const getSWMessagesAsText = async () => {
|
|||
}
|
||||
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
|
||||
// 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;
|
||||
|
|
@ -122,12 +21,17 @@ export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => {
|
|||
// Run another vite build just for the service worker targeting iife at
|
||||
// the end of the build.
|
||||
export const buildSwPlugin = ({ swSrc, swDest }) => {
|
||||
const swFullSrc = resolve(projectRoot, swSrc)
|
||||
const swEnvName = 'virtual:pleroma-fe/service_worker_env'
|
||||
const swEnvNameResolved = '\0' + swEnvName
|
||||
|
||||
let config
|
||||
|
||||
return {
|
||||
name: 'build-sw-plugin',
|
||||
enforce: 'post',
|
||||
apply: 'build',
|
||||
configResolved(resolvedConfig) {
|
||||
resolvedConfig
|
||||
config = {
|
||||
define: resolvedConfig.define,
|
||||
resolve: resolvedConfig.resolve,
|
||||
|
|
@ -135,15 +39,16 @@ export const buildSwPlugin = ({ swSrc, swDest }) => {
|
|||
publicDir: false,
|
||||
build: {
|
||||
...resolvedConfig.build,
|
||||
lib: {
|
||||
entry: swSrc,
|
||||
formats: ['iife'],
|
||||
name: 'sw_pleroma',
|
||||
},
|
||||
emptyOutDir: false,
|
||||
rollupOptions: {
|
||||
rolldownOptions: {
|
||||
input: {
|
||||
main: swSrc,
|
||||
},
|
||||
context: 'self',
|
||||
output: {
|
||||
entryFileNames: swDest,
|
||||
codeSplitting: false,
|
||||
format: 'iife',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -157,29 +62,64 @@ export const buildSwPlugin = ({ swSrc, swDest }) => {
|
|||
const assets = Object.keys(bundle)
|
||||
.filter((name) => !/\.map$/.test(name))
|
||||
.map((name) => '/' + name)
|
||||
|
||||
config.plugins.push({
|
||||
name: 'build-sw-env-plugin',
|
||||
resolveId(id) {
|
||||
if (id === swEnvName) {
|
||||
return swEnvNameResolved
|
||||
}
|
||||
return null
|
||||
mode: 'production',
|
||||
resolveId: {
|
||||
filter: { id: exactRegex(swEnvName) },
|
||||
handler: () => swEnvNameResolved,
|
||||
},
|
||||
load(id) {
|
||||
if (id === swEnvNameResolved) {
|
||||
return getProdSwEnv({ assets })
|
||||
}
|
||||
return null
|
||||
load: {
|
||||
filter: { id: exactRegex(swEnvNameResolved) },
|
||||
handler() {
|
||||
return `self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };`
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
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: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
async handler() {
|
||||
if (process.env.VITEST) return
|
||||
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
|
||||
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
|
||||
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
|
||||
some font overrides not working
|
||||
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
|
||||
|
||||
pleroma:
|
||||
image: ${PLEROMA_IMAGE:-git.pleroma.social:5050/pleroma/pleroma:stable}
|
||||
image: ${PLEROMA_IMAGE:-git.pleroma.social/pleroma/pleroma:stable}
|
||||
environment:
|
||||
DB_USER: pleroma
|
||||
DB_PASS: pleroma
|
||||
|
|
@ -39,6 +39,8 @@ services:
|
|||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 60
|
||||
ports:
|
||||
- 4000:4000
|
||||
|
||||
e2e:
|
||||
build:
|
||||
|
|
@ -51,7 +53,7 @@ services:
|
|||
CI: "1"
|
||||
VITE_PROXY_TARGET: http://pleroma: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_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin}
|
||||
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
|
||||
|
||||
|
|
|
|||
27
package.json
27
package.json
|
|
@ -43,7 +43,7 @@
|
|||
"localforage": "1.10.0",
|
||||
"parse-link-header": "2.0.0",
|
||||
"phoenix": "1.8.1",
|
||||
"pinia": "^3.0.0",
|
||||
"pinia": "^3.0.4",
|
||||
"punycode.js": "2.3.1",
|
||||
"qrcode": "1.5.4",
|
||||
"querystring-es3": "0.2.1",
|
||||
|
|
@ -65,10 +65,12 @@
|
|||
"@biomejs/biome": "2.3.11",
|
||||
"@pinia/testing": "1.0.3",
|
||||
"@ungap/event-target": "0.2.4",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||
"@vitest/browser": "^3.0.7",
|
||||
"@vitest/ui": "^3.0.7",
|
||||
"@vitejs/devtools": "^0.3.1",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.5",
|
||||
"@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-plugin-jsx": "1.5.0",
|
||||
"@vue/compiler-sfc": "3.5.22",
|
||||
|
|
@ -94,13 +96,14 @@
|
|||
"http-proxy-middleware": "3.0.5",
|
||||
"iso-639-1": "3.1.5",
|
||||
"lodash": "4.17.21",
|
||||
"msw": "2.10.5",
|
||||
"msw": "2.14.6",
|
||||
"nightwatch": "3.12.2",
|
||||
"playwright": "1.57.0",
|
||||
"oxc": "^1.0.1",
|
||||
"playwright": "1.61.0",
|
||||
"postcss": "8.5.6",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-scss": "^4.0.6",
|
||||
"sass": "1.93.2",
|
||||
"sass-embedded": "^1.100.0",
|
||||
"selenium-server": "3.141.59",
|
||||
"semver": "7.7.3",
|
||||
"serve-static": "2.2.0",
|
||||
|
|
@ -113,10 +116,10 @@
|
|||
"stylelint-config-recommended-scss": "^14.0.0",
|
||||
"stylelint-config-recommended-vue": "^1.6.0",
|
||||
"stylelint-config-standard": "38.0.0",
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-eslint2": "^5.0.3",
|
||||
"vite-plugin-stylelint": "^6.0.0",
|
||||
"vitest": "^3.0.7",
|
||||
"vite": "^8.0.0",
|
||||
"vite-plugin-eslint2": "^5.1.0",
|
||||
"vite-plugin-stylelint": "^6.1.0",
|
||||
"vitest": "^4.1.7",
|
||||
"vue-eslint-parser": "10.2.0"
|
||||
},
|
||||
"type": "module",
|
||||
|
|
|
|||
80
src/App.js
80
src/App.js
|
|
@ -2,22 +2,15 @@ import { throttle } from 'lodash'
|
|||
import { mapState } from 'pinia'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import DesktopNav from './components/desktop_nav/desktop_nav.vue'
|
||||
import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
|
||||
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
||||
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
|
||||
import MediaModal from './components/media_modal/media_modal.vue'
|
||||
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||
import NavPanel from './components/nav_panel/nav_panel.vue'
|
||||
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
||||
import ShoutPanel from './components/shout_panel/shout_panel.vue'
|
||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||
import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
|
||||
import UserPanel from './components/user_panel/user_panel.vue'
|
||||
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
||||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||
import DesktopNav from 'src/components/desktop_nav/desktop_nav.vue'
|
||||
import FeaturesPanel from 'src/components/features_panel/features_panel.vue'
|
||||
import GlobalError from 'src/components/global_error/global_error.vue'
|
||||
import GlobalNoticeList from 'src/components/global_notice_list/global_notice_list.vue'
|
||||
import InstanceSpecificPanel from 'src/components/instance_specific_panel/instance_specific_panel.vue'
|
||||
import MobileNav from 'src/components/mobile_nav/mobile_nav.vue'
|
||||
import MobilePostStatusButton from 'src/components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
|
||||
import UserPanel from 'src/components/user_panel/user_panel.vue'
|
||||
import { getOrCreateServiceWorker } from './services/sw/sw'
|
||||
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 { 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
|
||||
window.toValue = (x) => JSON.parse(JSON.stringify(x))
|
||||
|
||||
|
|
@ -41,27 +31,45 @@ export default {
|
|||
UserPanel,
|
||||
NavPanel,
|
||||
Notifications: defineAsyncComponent(
|
||||
() => import('./components/notifications/notifications.vue'),
|
||||
() => import('src/components/notifications/notifications.vue'),
|
||||
),
|
||||
InstanceSpecificPanel,
|
||||
FeaturesPanel,
|
||||
WhoToFollowPanel,
|
||||
ShoutPanel,
|
||||
MediaModal,
|
||||
SideDrawer,
|
||||
WhoToFollowPanel: defineAsyncComponent(
|
||||
() =>
|
||||
import('src/components/who_to_follow_panel/who_to_follow_panel.vue'),
|
||||
),
|
||||
ShoutPanel: defineAsyncComponent(
|
||||
() => import('src/components/shout_panel/shout_panel.vue'),
|
||||
),
|
||||
MediaModal: defineAsyncComponent(
|
||||
() => import('src/components/media_modal/media_modal.vue'),
|
||||
),
|
||||
MobilePostStatusButton,
|
||||
MobileNav,
|
||||
DesktopNav,
|
||||
SettingsModal: defineAsyncComponent(
|
||||
() => import('./components/settings_modal/settings_modal.vue'),
|
||||
() => import('src/components/settings_modal/settings_modal.vue'),
|
||||
),
|
||||
UpdateNotification: defineAsyncComponent(
|
||||
() => import('./components/update_notification/update_notification.vue'),
|
||||
() =>
|
||||
import('src/components/update_notification/update_notification.vue'),
|
||||
),
|
||||
UserReportingModal,
|
||||
PostStatusModal,
|
||||
EditStatusModal,
|
||||
StatusHistoryModal,
|
||||
PostStatusModal: defineAsyncComponent(
|
||||
() => import('src/components/post_status_modal/post_status_modal.vue'),
|
||||
),
|
||||
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,
|
||||
},
|
||||
data: () => ({
|
||||
|
|
@ -149,13 +157,23 @@ export default {
|
|||
userBackground() {
|
||||
return this.currentUser.background_image
|
||||
},
|
||||
foreignProfileBackground() {
|
||||
return (
|
||||
useMergedConfigStore().mergedConfig.allowForeignUserBackground &&
|
||||
useInterfaceStore().foreignProfileBackground
|
||||
)
|
||||
},
|
||||
instanceBackground() {
|
||||
return useMergedConfigStore().mergedConfig.hideInstanceWallpaper
|
||||
? null
|
||||
: this.instanceBackgroundUrl
|
||||
},
|
||||
background() {
|
||||
return this.userBackground || this.instanceBackground
|
||||
return (
|
||||
this.foreignProfileBackground ||
|
||||
this.userBackground ||
|
||||
this.instanceBackground
|
||||
)
|
||||
},
|
||||
bgStyle() {
|
||||
if (this.background) {
|
||||
|
|
|
|||
89
src/App.scss
89
src/App.scss
|
|
@ -50,7 +50,7 @@ body {
|
|||
// have a cursor/pointer to operate them
|
||||
@media (any-pointer: fine) {
|
||||
* {
|
||||
scrollbar-color: var(--fg) transparent;
|
||||
scrollbar-color: var(--icon) transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background: transparent;
|
||||
|
|
@ -130,7 +130,7 @@ body {
|
|||
}
|
||||
// Body should have background to scrollbar otherwise it will use white (body color?)
|
||||
html {
|
||||
scrollbar-color: var(--fg) var(--wallpaper);
|
||||
scrollbar-color: var(--icon) var(--wallpaper);
|
||||
background: var(--wallpaper);
|
||||
}
|
||||
}
|
||||
|
|
@ -144,6 +144,25 @@ h4 {
|
|||
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 {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
|
|
@ -200,6 +219,7 @@ nav {
|
|||
background-color: var(--wallpaper);
|
||||
background-image: var(--body-background-image);
|
||||
background-position: 50%;
|
||||
transition: background-image 1s;
|
||||
}
|
||||
|
||||
.underlay {
|
||||
|
|
@ -410,6 +430,14 @@ nav {
|
|||
button:not(.button-default) {
|
||||
color: var(--text);
|
||||
font-size: 100%;
|
||||
text-align: initial;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
display: inline;
|
||||
font-family: inherit;
|
||||
line-height: unset;
|
||||
}
|
||||
|
||||
&.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,
|
||||
.list-item {
|
||||
display: block;
|
||||
|
|
@ -474,22 +463,6 @@ nav {
|
|||
--__line-height: 1.5em;
|
||||
--__horizontal-gap: 0.75em;
|
||||
--__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 {
|
||||
|
|
@ -815,9 +788,11 @@ option {
|
|||
align-items: baseline;
|
||||
line-height: 1.5;
|
||||
|
||||
p,
|
||||
span {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dismiss {
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@
|
|||
>
|
||||
<user-panel />
|
||||
<template v-if="layoutType !== 'mobile'">
|
||||
<nav-panel />
|
||||
<instance-specific-panel v-if="showInstanceSpecificPanel" />
|
||||
<features-panel v-if="!currentUser && showFeaturesPanel" />
|
||||
<who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
|
||||
<NavPanel />
|
||||
<InstanceSpecificPanel v-if="showInstanceSpecificPanel" />
|
||||
<FeaturesPanel v-if="!currentUser && showFeaturesPanel" />
|
||||
<WhoToFollowPanel v-if="currentUser && suggestionsEnabled" />
|
||||
<div id="notifs-sidebar" />
|
||||
</template>
|
||||
</div>
|
||||
|
|
@ -73,6 +73,7 @@
|
|||
<StatusHistoryModal v-if="editingAvailable" />
|
||||
<SettingsModal :class="layoutModalClass" />
|
||||
<UpdateNotification />
|
||||
<GlobalError />
|
||||
<GlobalNoticeList />
|
||||
</div>
|
||||
</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 '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 {
|
||||
FontAwesomeIcon,
|
||||
|
|
@ -15,7 +19,6 @@ import {
|
|||
config.autoAddCss = false
|
||||
|
||||
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 { applyStyleConfig } from '../services/style_setter/style_setter.js'
|
||||
import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
|
||||
|
|
@ -25,7 +28,6 @@ import {
|
|||
} from '../services/window_utils/window_utils'
|
||||
import routes from './routes'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow'
|
||||
import { useEmojiStore } from 'src/stores/emoji.js'
|
||||
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 { useLocalConfigStore } from 'src/stores/local_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 { useUserHighlightStore } from 'src/stores/user_highlight.js'
|
||||
|
||||
|
|
@ -152,8 +154,10 @@ const getStaticConfig = async () => {
|
|||
throw res
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load static/config.json, continuing without it.')
|
||||
console.warn(error)
|
||||
console.warn(
|
||||
'Failed to load static/config.json, continuing without it.',
|
||||
error,
|
||||
)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
|
@ -175,14 +179,16 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
if (source === 'name') return
|
||||
if (INSTANCE_IDENTIY_EXTERNAL.has(source)) return
|
||||
useInstanceStore().set({
|
||||
value: config[source] ?? INSTANCE_IDENTITY_DEFAULT_DEFINITIONS[source].default,
|
||||
value:
|
||||
config[source] ?? INSTANCE_IDENTITY_DEFAULT_DEFINITIONS[source].default,
|
||||
path: `instanceIdentity.${source}`,
|
||||
})
|
||||
})
|
||||
|
||||
Object.keys(INSTANCE_DEFAULT_CONFIG_DEFINITIONS).forEach((source) =>
|
||||
useInstanceStore().set({
|
||||
value: config[source] ?? INSTANCE_DEFAULT_CONFIG_DEFINITIONS[source].default,
|
||||
value:
|
||||
config[source] ?? INSTANCE_DEFAULT_CONFIG_DEFINITIONS[source].default,
|
||||
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 nicknames = accounts.map((uri) => uri.split('/').pop())
|
||||
useInstanceStore().set({
|
||||
|
|
@ -440,8 +436,7 @@ const getNodeInfo = async ({ store }) => {
|
|||
throw res
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not load nodeinfo')
|
||||
console.warn(e)
|
||||
console.warn('Could not load nodeinfo', e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -454,14 +449,13 @@ const setConfig = async ({ store }) => {
|
|||
const apiConfig = configInfos[0]
|
||||
const staticConfig = configInfos[1]
|
||||
|
||||
getAppSecret({ store })
|
||||
await setSettings({ store, apiConfig, staticConfig })
|
||||
}
|
||||
|
||||
const checkOAuthToken = async ({ store }) => {
|
||||
const oauth = useOAuthStore()
|
||||
if (oauth.getUserToken) {
|
||||
return store.dispatch('loginUser', oauth.getUserToken)
|
||||
if (oauth.userToken) {
|
||||
return store.dispatch('loginUser', oauth.userToken)
|
||||
}
|
||||
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."
|
||||
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 () => {
|
||||
// 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)
|
||||
|
|
@ -571,10 +575,6 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
|||
getInstanceConfig({ store }),
|
||||
]).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 })
|
||||
getStickers({ store })
|
||||
|
||||
|
|
@ -608,6 +608,9 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
|||
|
||||
app.component('FAIcon', FontAwesomeIcon)
|
||||
app.component('FALayers', FontAwesomeLayers)
|
||||
app.component('Status', Status)
|
||||
app.component('RichContent', RichContent)
|
||||
app.component('StillImage', StillImage)
|
||||
|
||||
// remove after vue 3.3
|
||||
app.config.unwrapInjectedRef = true
|
||||
|
|
|
|||
|
|
@ -1,36 +1,16 @@
|
|||
import About from 'components/about/about.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 { defineAsyncComponent } from '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 BookmarkFolderEdit from '../components/bookmark_folder_edit/bookmark_folder_edit.vue'
|
||||
import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue'
|
||||
import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue'
|
||||
import PublicAndExternalTimeline from 'src/components/public_and_external_timeline/public_and_external_timeline.vue'
|
||||
import PublicTimeline from 'src/components/public_timeline/public_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 { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
|
|
@ -100,12 +80,23 @@ export default (store) => {
|
|||
{
|
||||
name: 'external-user-profile',
|
||||
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',
|
||||
path: '/users/:username/interactions',
|
||||
component: Interactions,
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/interactions/interactions.vue'),
|
||||
),
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
|
|
@ -114,69 +105,148 @@ export default (store) => {
|
|||
component: DMs,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{ name: 'registration', path: '/registration', component: Registration },
|
||||
{
|
||||
name: 'registration',
|
||||
path: '/registration',
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/registration/registration.vue'),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'password-reset',
|
||||
path: '/password-reset',
|
||||
component: PasswordReset,
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/password_reset/password_reset.vue'),
|
||||
),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
name: 'registration-token',
|
||||
path: '/registration/:token',
|
||||
component: Registration,
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/registration/registration.vue'),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'friend-requests',
|
||||
path: '/friend-requests',
|
||||
component: FollowRequests,
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/follow_requests/follow_requests.vue'),
|
||||
),
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'notifications',
|
||||
path: '/:username/notifications',
|
||||
component: Notifications,
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/notifications/notifications.vue'),
|
||||
),
|
||||
props: () => ({ disableTeleport: true }),
|
||||
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',
|
||||
path: '/shout-panel',
|
||||
component: ShoutPanel,
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/shout_panel/shout_panel.vue'),
|
||||
),
|
||||
props: () => ({ floating: false }),
|
||||
},
|
||||
{
|
||||
name: 'oauth-callback',
|
||||
path: '/oauth-callback',
|
||||
component: OAuthCallback,
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/oauth_callback/oauth_callback.vue'),
|
||||
),
|
||||
props: (route) => ({ code: route.query.code }),
|
||||
},
|
||||
{
|
||||
name: 'search',
|
||||
path: '/search',
|
||||
component: Search,
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/search/search.vue'),
|
||||
),
|
||||
props: (route) => ({ query: route.query.query }),
|
||||
},
|
||||
{
|
||||
name: 'who-to-follow',
|
||||
path: '/who-to-follow',
|
||||
component: WhoToFollow,
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/who_to_follow/who_to_follow.vue'),
|
||||
),
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{ name: 'about', path: '/about', component: About },
|
||||
{
|
||||
name: 'about',
|
||||
path: '/about',
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/about/about.vue'),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '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',
|
||||
path: '/nav-edit',
|
||||
|
|
@ -187,12 +257,19 @@ export default (store) => {
|
|||
{
|
||||
name: 'bookmark-folders',
|
||||
path: '/bookmark_folders',
|
||||
component: BookmarkFolders,
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/bookmark_folders/bookmark_folders.vue'),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'bookmark-folder-new',
|
||||
path: '/bookmarks/new-folder',
|
||||
component: BookmarkFolderEdit,
|
||||
component: defineAsyncComponent(
|
||||
() =>
|
||||
import(
|
||||
'src/components/bookmark_folder_edit/bookmark_folder_edit.vue'
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'bookmark-folder',
|
||||
|
|
@ -202,7 +279,12 @@ export default (store) => {
|
|||
{
|
||||
name: 'bookmark-folder-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',
|
||||
path: '/users/:username/chats/:recipient_id',
|
||||
component: Chat,
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/chat/chat.vue'),
|
||||
),
|
||||
meta: { dontScroll: false },
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'chats',
|
||||
path: '/users/:username/chats',
|
||||
component: ChatList,
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/chat_list/chat_list.vue'),
|
||||
),
|
||||
meta: { dontScroll: false },
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { mapState } from 'pinia'
|
||||
|
||||
import FeaturesPanel from '../features_panel/features_panel.vue'
|
||||
import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue'
|
||||
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue'
|
||||
import StaffPanel from '../staff_panel/staff_panel.vue'
|
||||
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
|
||||
import FeaturesPanel from 'src/components/features_panel/features_panel.vue'
|
||||
import InstanceSpecificPanel from 'src/components/instance_specific_panel/instance_specific_panel.vue'
|
||||
import MRFTransparencyPanel from 'src/components/mrf_transparency_panel/mrf_transparency_panel.vue'
|
||||
import StaffPanel from 'src/components/staff_panel/staff_panel.vue'
|
||||
import TermsOfServicePanel from 'src/components/terms_of_service_panel/terms_of_service_panel.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
|
@ -27,7 +27,11 @@ const About = {
|
|||
frontendVersionLink() {
|
||||
return pleromaFeCommitUrl + this.frontendVersion
|
||||
},
|
||||
...mapState(useInstanceStore, ['backendVersion', 'backendRepository', 'frontendVersion']),
|
||||
...mapState(useInstanceStore, [
|
||||
'backendVersion',
|
||||
'backendRepository',
|
||||
'frontendVersion',
|
||||
]),
|
||||
showInstanceSpecificPanel() {
|
||||
return (
|
||||
useInstanceStore().instanceIdentity.showInstanceSpecificPanel &&
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@
|
|||
<features-panel v-if="showFeaturesPanel" />
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="title">{{ $t('settings.version.title') }}</div>
|
||||
<div class="title">
|
||||
{{ $t('settings.version.title') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<dl>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
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 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 { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
|
@ -27,8 +26,15 @@ const AccountActions = {
|
|||
ProgressButton,
|
||||
Popover,
|
||||
UserListMenu,
|
||||
ConfirmModal,
|
||||
UserTimedFilterModal,
|
||||
ConfirmModal: defineAsyncComponent(
|
||||
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||
),
|
||||
UserTimedFilterModal: defineAsyncComponent(
|
||||
() =>
|
||||
import(
|
||||
'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
||||
),
|
||||
),
|
||||
},
|
||||
methods: {
|
||||
showConfirmRemoveUserFromFollowers() {
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@
|
|||
</template>
|
||||
</Popover>
|
||||
<teleport to="#modal">
|
||||
<confirm-modal
|
||||
<ConfirmModal
|
||||
v-if="showingConfirmBlock && !blockExpiration"
|
||||
ref="blockDialog"
|
||||
:title="$t('user_card.block_confirm_title')"
|
||||
|
|
@ -114,10 +114,10 @@
|
|||
/>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
</ConfirmModal>
|
||||
</teleport>
|
||||
<teleport to="#modal">
|
||||
<confirm-modal
|
||||
<ConfirmModal
|
||||
v-if="showingConfirmRemoveFollower"
|
||||
:title="$t('user_card.remove_follower_confirm_title')"
|
||||
:confirm-text="$t('user_card.remove_follower_confirm_accept_button')"
|
||||
|
|
@ -136,7 +136,7 @@
|
|||
/>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
</ConfirmModal>
|
||||
<UserTimedFilterModal
|
||||
v-if="blockExpiration"
|
||||
ref="timedBlockDialog"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export default {
|
|||
validInnerComponents: ['Text', 'Icon', 'Link', 'Border', 'ButtonUnstyled'],
|
||||
variants: {
|
||||
normal: '.neutral',
|
||||
info: '.info',
|
||||
error: '.error',
|
||||
warning: '.warning',
|
||||
success: '.success',
|
||||
|
|
@ -47,5 +48,11 @@ export default {
|
|||
background: '--cGreen',
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'info',
|
||||
directives: {
|
||||
background: '--cBlue',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
import AnnouncementEditor from 'src/components/announcement_editor/announcement_editor.vue'
|
||||
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'
|
||||
|
||||
const Announcement = {
|
||||
components: {
|
||||
AnnouncementEditor,
|
||||
RichContent,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -33,9 +31,7 @@ const Announcement = {
|
|||
canEditAnnouncement() {
|
||||
return (
|
||||
this.currentUser &&
|
||||
this.currentUser.privileges.includes(
|
||||
'announcements_manage_announcements',
|
||||
)
|
||||
this.currentUser.privileges.has('announcements_manage_announcements')
|
||||
)
|
||||
},
|
||||
content() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const AnnouncementEditor = {
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
import Announcement from '../announcement/announcement.vue'
|
||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||
import Announcement from 'src/components/announcement/announcement.vue'
|
||||
import AnnouncementEditor from 'src/components/announcement_editor/announcement_editor.vue'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements.js'
|
||||
|
||||
|
|
@ -35,9 +35,7 @@ const AnnouncementsPage = {
|
|||
canPostAnnouncement() {
|
||||
return (
|
||||
this.currentUser &&
|
||||
this.currentUser.privileges.includes(
|
||||
'announcements_manage_announcements',
|
||||
)
|
||||
this.currentUser.privileges.has('announcements_manage_announcements')
|
||||
)
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { mapState } from 'pinia'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
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 { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
|
|
@ -53,6 +52,7 @@ const Attachment = {
|
|||
'shiftDn',
|
||||
'edit',
|
||||
],
|
||||
emits: ['play', 'pause', 'naturalSizeLoad'],
|
||||
data() {
|
||||
return {
|
||||
localDescription: this.description || this.attachment.description,
|
||||
|
|
@ -65,13 +65,15 @@ const Attachment = {
|
|||
modalOpen: false,
|
||||
showHidden: false,
|
||||
flashLoaded: false,
|
||||
showDescription: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Flash,
|
||||
StillImage,
|
||||
VideoAttachment,
|
||||
Flash: defineAsyncComponent(() => import('src/components/flash/flash.vue')),
|
||||
|
||||
VideoAttachment: defineAsyncComponent(
|
||||
() => import('src/components/video_attachment/video_attachment.vue'),
|
||||
),
|
||||
Popover,
|
||||
},
|
||||
computed: {
|
||||
classNames() {
|
||||
|
|
@ -180,9 +182,6 @@ const Attachment = {
|
|||
setFlashLoaded(event) {
|
||||
this.flashLoaded = event
|
||||
},
|
||||
toggleDescription() {
|
||||
this.showDescription = !this.showDescription
|
||||
},
|
||||
toggleHidden(event) {
|
||||
if (
|
||||
this.mergedConfig.useOneClickNsfw &&
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@
|
|||
width: 2em;
|
||||
height: 2em;
|
||||
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>
|
||||
</div>
|
||||
<div
|
||||
v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)"
|
||||
v-if="size !== 'hide' && !hideDescription && edit"
|
||||
class="description-container"
|
||||
:class="{ '-static': !edit }"
|
||||
>
|
||||
<input
|
||||
v-if="edit"
|
||||
<textarea
|
||||
v-model="localDescription"
|
||||
type="text"
|
||||
class="input description-field"
|
||||
:placeholder="$t('post_status.media_description')"
|
||||
@keydown.enter.prevent=""
|
||||
>
|
||||
<p v-else>
|
||||
{{ localDescription }}
|
||||
</p>
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
|
|
@ -87,14 +82,22 @@
|
|||
>
|
||||
<FAIcon icon="stop" />
|
||||
</button>
|
||||
<button
|
||||
<Popover
|
||||
v-if="attachment.description && size !== 'small' && !edit && attachment.type !== 'unknown'"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.show_attachment_description')"
|
||||
@click.prevent="toggleDescription"
|
||||
trigger="click"
|
||||
popover-class="popover popover-default description-popover"
|
||||
:trigger-attrs="{ 'class': 'button-default attachment-button -transparent', 'title': $t('status.attachment_description') }"
|
||||
>
|
||||
<FAIcon icon="align-right" />
|
||||
</button>
|
||||
<template #trigger>
|
||||
<FAIcon icon="align-right" />
|
||||
</template>
|
||||
<template #content>
|
||||
<details open>
|
||||
<summary>{{ $t('status.attachment_description') }}</summary>
|
||||
<span>{{ localDescription }}</span>
|
||||
</details>
|
||||
</template>
|
||||
</Popover>
|
||||
<button
|
||||
v-if="!useModal && attachment.type !== 'unknown'"
|
||||
class="button-default attachment-button -transparent"
|
||||
|
|
@ -244,21 +247,16 @@
|
|||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))"
|
||||
v-if="size !== 'hide' && !hideDescription && edit"
|
||||
class="description-container"
|
||||
:class="{ '-static': !edit }"
|
||||
>
|
||||
<input
|
||||
v-if="edit"
|
||||
<textarea
|
||||
v-model="localDescription"
|
||||
type="text"
|
||||
class="input description-field"
|
||||
:placeholder="$t('post_status.media_description')"
|
||||
@keydown.enter.prevent=""
|
||||
>
|
||||
<p v-else>
|
||||
{{ localDescription }}
|
||||
</p>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { mapState } from 'pinia'
|
||||
import { h, resolveComponent } from 'vue'
|
||||
|
||||
import LoginForm from '../login_form/login_form.vue'
|
||||
import MFARecoveryForm from '../mfa_form/recovery_form.vue'
|
||||
import MFATOTPForm from '../mfa_form/totp_form.vue'
|
||||
import LoginForm from 'src/components/login_form/login_form.vue'
|
||||
import MFARecoveryForm from 'src/components/mfa_form/recovery_form.vue'
|
||||
import MFATOTPForm from 'src/components/mfa_form/totp_form.vue'
|
||||
|
||||
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'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserLink from '../user_link/user_link.vue'
|
||||
import UserPopover from '../user_popover/user_popover.vue'
|
||||
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||
import UserLink from 'src/components/user_link/user_link.vue'
|
||||
import UserPopover from 'src/components/user_popover/user_popover.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.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'
|
||||
|
||||
const BasicUserCard = {
|
||||
props: ['user'],
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
},
|
||||
showLineLabels: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
UserPopover,
|
||||
UserAvatar,
|
||||
RichContent,
|
||||
|
||||
UserLink,
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -29,7 +36,7 @@ const BasicUserCard = {
|
|||
allowNonSquareEmoji() {
|
||||
return useMergedConfigStore().mergedConfig.nonSquareEmoji
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default BasicUserCard
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@
|
|||
:title="user.name"
|
||||
class="basic-user-card-user-name"
|
||||
>
|
||||
<strong v-if="showLineLabels">
|
||||
{{ $t('admin_dash.users.labels.name_colon') }}
|
||||
{{ ' ' }}
|
||||
</strong>
|
||||
<RichContent
|
||||
class="basic-user-card-user-name-value"
|
||||
:html="user.name"
|
||||
|
|
@ -31,6 +35,10 @@
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<strong v-if="showLineLabels">
|
||||
{{ $t('admin_dash.users.labels.handle_colon') }}
|
||||
{{ ' ' }}
|
||||
</strong>
|
||||
<user-link
|
||||
class="basic-user-card-screen-name"
|
||||
:user="user"
|
||||
|
|
@ -46,8 +54,10 @@
|
|||
<style lang="scss">
|
||||
.basic-user-card {
|
||||
display: flex;
|
||||
flex: 1 0;
|
||||
flex: 1 1 10em;
|
||||
min-width: 1em;
|
||||
margin: 0;
|
||||
line-height: 1.25;
|
||||
|
||||
--emoji-size: 1em;
|
||||
|
||||
|
|
@ -69,7 +79,7 @@
|
|||
|
||||
&-user-name-value,
|
||||
&-screen-name {
|
||||
display: inline-block;
|
||||
display: inline;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
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 BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<basic-user-card :user="user">
|
||||
<BasicUserCard :user="user">
|
||||
<div class="block-card-content-container">
|
||||
<span
|
||||
v-if="blocked && blockExpiryAvailable"
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
:is-mute="false"
|
||||
/>
|
||||
</teleport>
|
||||
</basic-user-card>
|
||||
</BasicUserCard>
|
||||
</template>
|
||||
|
||||
<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 '../emoji_picker/emoji_picker.vue'
|
||||
import EmojiPicker from 'src/components/emoji_picker/emoji_picker.vue'
|
||||
|
||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
|
||||
import { fetchBookmarkFolders } from 'src/api/user.js'
|
||||
|
||||
const BookmarkFolderEdit = {
|
||||
data() {
|
||||
|
|
@ -22,8 +24,10 @@ const BookmarkFolderEdit = {
|
|||
},
|
||||
created() {
|
||||
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)
|
||||
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'
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ const BookmarkFolders = {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
BookmarkFolderCard,
|
||||
FolderCard,
|
||||
},
|
||||
computed: {
|
||||
bookmarkFolders() {
|
||||
|
|
|
|||
|
|
@ -12,14 +12,28 @@
|
|||
</router-link>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<BookmarkFolderCard
|
||||
:all-bookmarks="true"
|
||||
class="list-item"
|
||||
/>
|
||||
<BookmarkFolderCard
|
||||
<div class="list-item FolderCard">
|
||||
<router-link
|
||||
:to="{ name: 'bookmarks' }"
|
||||
class="folder-name"
|
||||
>
|
||||
<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()"
|
||||
: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"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
import Timeline from 'src/components/timeline/timeline.vue'
|
||||
|
||||
const Bookmarks = {
|
||||
created() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
import Timeline from 'src/components/timeline/timeline.vue'
|
||||
|
||||
const BubbleTimeline = {
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import _ from 'lodash'
|
||||
import { throttle } from 'lodash'
|
||||
import { mapState as mapPiniaState } from 'pinia'
|
||||
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 { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
|
||||
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
|
||||
import ChatMessage from '../chat_message/chat_message.vue'
|
||||
import ChatTitle from '../chat_title/chat_title.vue'
|
||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import {
|
||||
getNewTopPosition,
|
||||
getScrollPosition,
|
||||
|
|
@ -17,6 +16,14 @@ import {
|
|||
} from './chat_layout_utils.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 { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
|
||||
|
|
@ -115,7 +122,6 @@ const Chat = {
|
|||
mobileLayout: (store) => store.layoutType === 'mobile',
|
||||
}),
|
||||
...mapState({
|
||||
backendInteractor: (state) => state.api.backendInteractor,
|
||||
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
}),
|
||||
|
|
@ -224,7 +230,7 @@ const Chat = {
|
|||
}
|
||||
}, 5000)
|
||||
},
|
||||
handleScroll: _.throttle(function () {
|
||||
handleScroll: throttle(function () {
|
||||
this.lastScrollPosition = getScrollPosition()
|
||||
if (!this.currentChat) {
|
||||
return
|
||||
|
|
@ -267,43 +273,48 @@ const Chat = {
|
|||
const fetchOlderMessages = !!maxId
|
||||
const sinceId = fetchLatest && chatMessageService.maxId
|
||||
|
||||
return this.backendInteractor
|
||||
.chatMessages({ id: chatId, maxId, sinceId })
|
||||
.then((messages) => {
|
||||
// Clear the current chat in case we're recovering from a ws connection loss.
|
||||
if (isFirstFetch) {
|
||||
chatService.clear(chatMessageService)
|
||||
}
|
||||
return chatMessages({
|
||||
id: chatId,
|
||||
maxId,
|
||||
sinceId,
|
||||
credentials: useOAuthStore().token,
|
||||
}).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()
|
||||
this.$store
|
||||
.dispatch('addChatMessages', { chatId, messages })
|
||||
.then(() => {
|
||||
this.$nextTick(() => {
|
||||
if (fetchOlderMessages) {
|
||||
this.handleScrollUp(positionBeforeUpdate)
|
||||
}
|
||||
const positionBeforeUpdate = getScrollPosition()
|
||||
this.$store
|
||||
.dispatch('addChatMessages', { chatId, messages })
|
||||
.then(() => {
|
||||
this.$nextTick(() => {
|
||||
if (fetchOlderMessages) {
|
||||
this.handleScrollUp(positionBeforeUpdate)
|
||||
}
|
||||
|
||||
// In vertical screens, the first batch of fetched messages may not always take the
|
||||
// full height of 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.
|
||||
if (!isScrollable() && messages.length > 0) {
|
||||
this.fetchChat({
|
||||
maxId: this.currentChatMessageService.minId,
|
||||
})
|
||||
}
|
||||
})
|
||||
// In vertical screens, the first batch of fetched messages may not always take the
|
||||
// full height of 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.
|
||||
if (!isScrollable() && messages.length > 0) {
|
||||
this.fetchChat({
|
||||
maxId: this.currentChatMessageService.minId,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
async startFetching() {
|
||||
let chat = this.findOpenedChatByRecipientId(this.recipientId)
|
||||
if (!chat) {
|
||||
try {
|
||||
chat = await this.backendInteractor.getOrCreateChat({
|
||||
const { data } = await getOrCreateChat({
|
||||
accountId: this.recipientId,
|
||||
credentials: useOAuthStore().token,
|
||||
})
|
||||
chat = data
|
||||
} catch (e) {
|
||||
console.error('Error creating or getting a chat', e)
|
||||
this.errorLoadingChat = true
|
||||
|
|
@ -369,9 +380,11 @@ const Chat = {
|
|||
doSendMessage({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
|
||||
if (retriesLeft <= 0) return
|
||||
|
||||
this.backendInteractor
|
||||
.sendChatMessage(params)
|
||||
.then((data) => {
|
||||
sendChatMessage({
|
||||
params,
|
||||
credentials: useOAuthStore().token,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
this.$store.dispatch('addChatMessages', {
|
||||
chatId: this.currentChat.id,
|
||||
updateMaxId: false,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
import ChatListItem from '../chat_list_item/chat_list_item.vue'
|
||||
import ChatNew from '../chat_new/chat_new.vue'
|
||||
import List from '../list/list.vue'
|
||||
import ChatListItem from 'src/components/chat_list_item/chat_list_item.vue'
|
||||
import ChatNew from 'src/components/chat_new/chat_new.vue'
|
||||
import List from 'src/components/list/list.vue'
|
||||
|
||||
const ChatList = {
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
v-if="sortedChatList.length > 0"
|
||||
class="timeline"
|
||||
>
|
||||
<List :items="sortedChatList">
|
||||
<List :external-items="sortedChatList">
|
||||
<template #item="{item}">
|
||||
<ChatListItem
|
||||
:key="item.id"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
import AvatarList from '../avatar_list/avatar_list.vue'
|
||||
import ChatTitle from '../chat_title/chat_title.vue'
|
||||
import StatusBody from '../status_content/status_content.vue'
|
||||
import Timeago from '../timeago/timeago.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import AvatarList from 'src/components/avatar_list/avatar_list.vue'
|
||||
import ChatTitle from 'src/components/chat_title/chat_title.vue'
|
||||
import StatusBody from 'src/components/status_content/status_content.vue'
|
||||
import Timeago from 'src/components/timeago/timeago.vue'
|
||||
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||
|
||||
const ChatListItem = {
|
||||
name: 'ChatListItem',
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
import Attachment from '../attachment/attachment.vue'
|
||||
import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
|
||||
import Gallery from '../gallery/gallery.vue'
|
||||
import LinkPreview from '../link-preview/link-preview.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import StatusContent from '../status_content/status_content.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import Attachment from 'src/components/attachment/attachment.vue'
|
||||
import ChatMessageDate from 'src/components/chat_message_date/chat_message_date.vue'
|
||||
import Gallery from 'src/components/gallery/gallery.vue'
|
||||
import LinkPreview from 'src/components/link-preview/link-preview.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import StatusContent from 'src/components/status_content/status_content.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 { useInterfaceStore } from 'src/stores/interface'
|
||||
|
|
@ -37,9 +37,7 @@ const ChatMessage = {
|
|||
Gallery,
|
||||
LinkPreview,
|
||||
ChatMessageDate,
|
||||
UserPopover: defineAsyncComponent(
|
||||
() => import('../user_popover/user_popover.vue'),
|
||||
),
|
||||
UserPopover,
|
||||
},
|
||||
computed: {
|
||||
// Returns HH:MM (hours and minutes) in local time.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.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 { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||
|
|
@ -22,7 +24,9 @@ const chatNew = {
|
|||
}
|
||||
},
|
||||
async created() {
|
||||
const { chats } = await this.backendInteractor.chats()
|
||||
const { chats } = await chats({
|
||||
credentials: useOAuthStore().token,
|
||||
})
|
||||
chats.forEach((chat) => this.suggestions.push(chat.account))
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -38,7 +42,6 @@ const chatNew = {
|
|||
},
|
||||
...mapState({
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
backendInteractor: (state) => state.api.backendInteractor,
|
||||
}),
|
||||
...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 UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
||||
export default {
|
||||
name: 'ChatTitle',
|
||||
components: {
|
||||
UserAvatar,
|
||||
RichContent,
|
||||
UserPopover: defineAsyncComponent(
|
||||
() => import('../user_popover/user_popover.vue'),
|
||||
),
|
||||
|
||||
UserPopover,
|
||||
},
|
||||
props: ['user', 'withAvatar'],
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -66,8 +66,8 @@
|
|||
<script>
|
||||
import { throttle } from 'lodash'
|
||||
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faEyeDropper } from '@fortawesome/free-solid-svg-icons'
|
||||
|
|
@ -88,6 +88,7 @@ export default {
|
|||
label: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// use unstyled, uh, style
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -8,8 +8,20 @@
|
|||
<span v-text="title" />
|
||||
</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>
|
||||
<slot name="footerLeft" />
|
||||
<button
|
||||
|
|
@ -29,3 +41,43 @@
|
|||
</template>
|
||||
|
||||
<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 { defineAsyncComponent } from 'vue'
|
||||
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import ConfirmModal from './confirm_modal.vue'
|
||||
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
||||
|
|
@ -12,7 +12,10 @@ export default {
|
|||
showing: false,
|
||||
}),
|
||||
components: {
|
||||
ConfirmModal,
|
||||
ConfirmModal: defineAsyncComponent(
|
||||
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||
),
|
||||
|
||||
Select,
|
||||
},
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<confirm-modal
|
||||
<ConfirmModal
|
||||
v-if="showing"
|
||||
:title="$t('user_card.mute_confirm_title')"
|
||||
:confirm-text="$t('user_card.mute_confirm_accept_button')"
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
<span v-text="user.screen_name_ui" />
|
||||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
<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 = {
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
<template>
|
||||
<conversation
|
||||
:collapsable="false"
|
||||
is-page="true"
|
||||
<Conversation
|
||||
:status-id="statusId"
|
||||
is-page
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,16 @@ import { clone, filter, findIndex, get, reduce } from 'lodash'
|
|||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
import { WSConnectionStatus } from '../../services/api/api.service.js'
|
||||
import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.vue'
|
||||
import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue'
|
||||
import Status from '../status/status.vue'
|
||||
import ThreadTree from '../thread_tree/thread_tree.vue'
|
||||
import QuickFilterSettings from 'src/components/quick_filter_settings/quick_filter_settings.vue'
|
||||
import QuickViewSettings from 'src/components/quick_view_settings/quick_view_settings.vue'
|
||||
import ThreadTree from 'src/components/thread_tree/thread_tree.vue'
|
||||
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
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 {
|
||||
|
|
@ -53,25 +55,56 @@ const sortAndFilterConversation = (conversation, statusoid) => {
|
|||
}
|
||||
|
||||
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() {
|
||||
return {
|
||||
highlight: null,
|
||||
focused: null,
|
||||
expanded: false,
|
||||
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
|
||||
statusContentPropertiesObject: {},
|
||||
inlineDivePosition: null,
|
||||
loadStatusError: null,
|
||||
unsuspendibleIds: new Set(),
|
||||
}
|
||||
},
|
||||
props: [
|
||||
'statusId',
|
||||
'collapsable',
|
||||
'isPage',
|
||||
'pinnedStatusIdsObject',
|
||||
'inProfile',
|
||||
'profileUserId',
|
||||
'virtualHidden',
|
||||
],
|
||||
created() {
|
||||
if (this.isPage) {
|
||||
this.fetchConversation()
|
||||
|
|
@ -116,16 +149,7 @@ const conversation = {
|
|||
return this.otherRepliesButtonPosition === 'inside'
|
||||
},
|
||||
suspendable() {
|
||||
if (this.isTreeView) {
|
||||
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
|
||||
}
|
||||
return this.unsuspendibleIds.size > 0
|
||||
},
|
||||
hideStatus() {
|
||||
return this.virtualHidden && this.suspendable
|
||||
|
|
@ -362,36 +386,11 @@ const conversation = {
|
|||
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() {
|
||||
return this.isTreeView && this.isExpanded
|
||||
},
|
||||
maybeHighlight() {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
maybeFocused() {
|
||||
return this.isExpanded ? this.focused : null
|
||||
},
|
||||
...mapPiniaState(useMergedConfigStore, ['mergedConfig']),
|
||||
...mapState({
|
||||
|
|
@ -402,7 +401,6 @@ const conversation = {
|
|||
}),
|
||||
},
|
||||
components: {
|
||||
Status,
|
||||
ThreadTree,
|
||||
QuickFilterSettings,
|
||||
QuickViewSettings,
|
||||
|
|
@ -416,7 +414,7 @@ const conversation = {
|
|||
oldConversationId &&
|
||||
newConversationId === oldConversationId
|
||||
) {
|
||||
this.setHighlight(this.originalStatusId)
|
||||
this.setFocused(this.originalStatusId)
|
||||
} else {
|
||||
this.fetchConversation()
|
||||
}
|
||||
|
|
@ -438,38 +436,36 @@ const conversation = {
|
|||
methods: {
|
||||
fetchConversation() {
|
||||
if (this.status) {
|
||||
this.$store.state.api.backendInteractor
|
||||
.fetchConversation({ id: this.statusId })
|
||||
.then(({ ancestors, descendants }) => {
|
||||
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
|
||||
this.$store.dispatch('addNewStatuses', { statuses: descendants })
|
||||
this.setHighlight(this.originalStatusId)
|
||||
})
|
||||
fetchConversation({
|
||||
id: this.statusId,
|
||||
credentials: useOAuthStore().token,
|
||||
}).then(({ data: { ancestors, descendants } }) => {
|
||||
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
|
||||
this.$store.dispatch('addNewStatuses', { statuses: descendants })
|
||||
this.setFocused(this.originalStatusId)
|
||||
})
|
||||
} else {
|
||||
this.loadStatusError = null
|
||||
this.$store.state.api.backendInteractor
|
||||
.fetchStatus({ id: this.statusId })
|
||||
.then((status) => {
|
||||
fetchStatus({
|
||||
id: this.statusId,
|
||||
credentials: useOAuthStore().token,
|
||||
})
|
||||
.then(({ data: status }) => {
|
||||
this.$store.dispatch('addNewStatuses', { statuses: [status] })
|
||||
this.fetchConversation()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
this.loadStatusError = error
|
||||
})
|
||||
}
|
||||
},
|
||||
isFocused(id) {
|
||||
return this.isExpanded && id === this.highlight
|
||||
},
|
||||
getReplies(id) {
|
||||
return this.replies[id] || []
|
||||
},
|
||||
getHighlight() {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
},
|
||||
setHighlight(id) {
|
||||
setFocused(id) {
|
||||
if (!id) return
|
||||
this.highlight = id
|
||||
this.focused = id
|
||||
|
||||
if (!this.streamingEnabled) {
|
||||
this.$store.dispatch('fetchStatus', id)
|
||||
|
|
@ -509,22 +505,6 @@ const conversation = {
|
|||
showThreadRecursively(id) {
|
||||
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) {
|
||||
let cur = id
|
||||
let parent = this.parentOf(cur)
|
||||
|
|
@ -550,7 +530,7 @@ const conversation = {
|
|||
// only used when we are not on a page
|
||||
undive() {
|
||||
this.inlineDivePosition = null
|
||||
this.setHighlight(this.statusId)
|
||||
this.setFocused(this.statusId)
|
||||
},
|
||||
tryScrollTo(id) {
|
||||
if (!id) {
|
||||
|
|
@ -568,7 +548,7 @@ const conversation = {
|
|||
// contain scrolling calls, as we do not want the page to jump
|
||||
// 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 linear views, all statuses are rendered at all times, but
|
||||
// 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
|
||||
// different).
|
||||
// Here, let the components be rendered first, in order to trigger
|
||||
// the `highlight` watcher.
|
||||
// the `focused` watcher.
|
||||
this.$nextTick(() => {
|
||||
this.setHighlight(id)
|
||||
this.setFocused(id)
|
||||
})
|
||||
},
|
||||
goToCurrent() {
|
||||
|
|
@ -624,6 +604,13 @@ const conversation = {
|
|||
this.undive()
|
||||
this.threadDisplayStatusObject = {}
|
||||
},
|
||||
onStatusSuspendStateChange({ id, suspend }) {
|
||||
if (!suspend) {
|
||||
this.unsuspendibleIds.add(id)
|
||||
} else {
|
||||
this.unsuspendibleIds.delete(id)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -96,8 +96,7 @@
|
|||
:replies="getReplies(status.id)"
|
||||
|
||||
:expandable="!isExpanded"
|
||||
:focused="isFocused(status.id)"
|
||||
:highlight="getHighlight()"
|
||||
:focused="maybeFocused === status.id"
|
||||
:inline-expanded="collapsable && isExpanded"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:in-profile="inProfile"
|
||||
|
|
@ -105,21 +104,11 @@
|
|||
:profile-user-id="profileUserId"
|
||||
:simple-tree="treeViewIsSimple"
|
||||
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
||||
:dive="() => diveIntoStatus(status.id)"
|
||||
can-dive
|
||||
|
||||
:controlled-showing-tall="statusContentProperties[status.id].showingTall"
|
||||
:controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')"
|
||||
:controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
|
||||
: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"
|
||||
@goto="setFocused"
|
||||
@dive="() => diveIntoStatus(status.id)"
|
||||
@suspendable-state-change="onStatusSuspendStateChange"
|
||||
/>
|
||||
<div
|
||||
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
||||
|
|
@ -150,7 +139,7 @@
|
|||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<thread-tree
|
||||
<ThreadTree
|
||||
v-for="status in showingTopLevel"
|
||||
:key="status.id"
|
||||
ref="statusComponent"
|
||||
|
|
@ -164,22 +153,20 @@
|
|||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||
:profile-user-id="profileUserId"
|
||||
|
||||
:is-focused-function="isFocused"
|
||||
:get-replies="getReplies"
|
||||
:highlight="maybeHighlight"
|
||||
:set-highlight="setHighlight"
|
||||
:focused="maybeFocused"
|
||||
:toggle-expanded="toggleExpanded"
|
||||
|
||||
:simple="treeViewIsSimple"
|
||||
:toggle-thread-display="toggleThreadDisplay"
|
||||
:thread-display-status="threadDisplayStatus"
|
||||
:show-thread-recursively="showThreadRecursively"
|
||||
:total-reply-count="totalReplyCount"
|
||||
:total-reply-depth="totalReplyDepth"
|
||||
:status-content-properties="statusContentProperties"
|
||||
:set-status-content-property="setStatusContentProperty"
|
||||
:toggle-status-content-property="toggleStatusContentProperty"
|
||||
:dive="canDive ? diveIntoStatus : undefined"
|
||||
:can-dive="canDive"
|
||||
|
||||
@goto="setFocused"
|
||||
@dive="diveIntoStatus"
|
||||
@suspendable-state-change="onStatusSuspendStateChange"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -187,7 +174,7 @@
|
|||
class="thread-body"
|
||||
>
|
||||
<article>
|
||||
<status
|
||||
<Status
|
||||
v-for="status in conversation"
|
||||
:key="status.id"
|
||||
ref="statusComponent"
|
||||
|
|
@ -196,16 +183,16 @@
|
|||
:replies="getReplies(status.id)"
|
||||
|
||||
:expandable="!isExpanded"
|
||||
:focused="isFocused(status.id)"
|
||||
:highlight="getHighlight()"
|
||||
:focused="maybeFocused === status.id || maybeFocused === status.retweeted_status?.id"
|
||||
:inline-expanded="collapsable && isExpanded"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:in-profile="inProfile"
|
||||
:in-conversation="isExpanded"
|
||||
:profile-user-id="profileUserId"
|
||||
|
||||
@goto="setHighlight"
|
||||
@goto="setFocused"
|
||||
@toggle-expanded="toggleExpanded"
|
||||
@suspendable-state-change="onStatusSuspendStateChange"
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import SearchBar from 'components/search_bar/search_bar.vue'
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
|
@ -39,7 +38,9 @@ library.add(
|
|||
export default {
|
||||
components: {
|
||||
SearchBar,
|
||||
ConfirmModal,
|
||||
ConfirmModal: defineAsyncComponent(
|
||||
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||
),
|
||||
},
|
||||
data: () => ({
|
||||
searchBarHidden: true,
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<teleport to="#modal">
|
||||
<confirm-modal
|
||||
<ConfirmModal
|
||||
v-if="showingConfirmLogout"
|
||||
:title="$t('login.logout_confirm_title')"
|
||||
:confirm-danger="true"
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
@cancelled="hideConfirmLogout"
|
||||
>
|
||||
{{ $t('login.logout_confirm') }}
|
||||
</confirm-modal>
|
||||
</ConfirmModal>
|
||||
</teleport>
|
||||
</nav>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
import Timeline from 'src/components/timeline/timeline.vue'
|
||||
|
||||
const DMs = {
|
||||
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 = {
|
||||
props: ['domain'],
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
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 PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
|
||||
import StatusContent from 'src/components/status_content/status_content.vue'
|
||||
|
|
@ -16,8 +15,12 @@ library.add(faPollH)
|
|||
const Draft = {
|
||||
components: {
|
||||
PostStatusForm,
|
||||
EditStatusForm,
|
||||
ConfirmModal,
|
||||
EditStatusForm: defineAsyncComponent(
|
||||
() => import('src/components/edit_status_form/edit_status_form.vue'),
|
||||
),
|
||||
ConfirmModal: defineAsyncComponent(
|
||||
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||
),
|
||||
StatusContent,
|
||||
Gallery,
|
||||
},
|
||||
|
|
@ -66,15 +69,6 @@ const Draft = {
|
|||
localCollapseSubjectDefault() {
|
||||
return useMergedConfigStore().mergedConfig.collapseMessageWithSubject
|
||||
},
|
||||
nsfwClickthrough() {
|
||||
if (!this.draft.nsfw) {
|
||||
return false
|
||||
}
|
||||
if (this.draft.summary && this.localCollapseSubjectDefault) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
editing(newVal) {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
class="faint"
|
||||
>{{ $t('drafts.empty') }}</p>
|
||||
</span>
|
||||
<gallery
|
||||
<Gallery
|
||||
v-if="draft.files?.length !== 0"
|
||||
class="attachments media-body"
|
||||
:compact="true"
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
/>
|
||||
</div>
|
||||
<teleport to="#modal">
|
||||
<confirm-modal
|
||||
<ConfirmModal
|
||||
v-if="showingConfirmDialog"
|
||||
:title="$t('drafts.abandon_confirm_title')"
|
||||
:confirm-text="$t('drafts.abandon_confirm_accept_button')"
|
||||
|
|
@ -86,7 +86,7 @@
|
|||
@cancelled="hideConfirmDialog"
|
||||
>
|
||||
{{ $t('drafts.abandon_confirm') }}
|
||||
</confirm-modal>
|
||||
</ConfirmModal>
|
||||
</teleport>
|
||||
<div class="actions">
|
||||
<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'
|
||||
|
||||
|
|
@ -9,7 +9,9 @@ const DraftCloser = {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
DialogModal,
|
||||
DialogModal: defineAsyncComponent(
|
||||
() => import('src/components/dialog_modal/dialog_modal.vue'),
|
||||
),
|
||||
},
|
||||
emits: ['save', 'discard'],
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<teleport to="#modal">
|
||||
<dialog-modal
|
||||
<DialogModal
|
||||
v-if="showing"
|
||||
v-body-scroll-lock="true"
|
||||
class="confirm-modal"
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
{{ $t('post_status.close_confirm_continue_composing_button') }}
|
||||
</button>
|
||||
</template>
|
||||
</dialog-modal>
|
||||
</DialogModal>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import Draft from 'src/components/draft/draft.vue'
|
||||
import List from 'src/components/list/list.vue'
|
||||
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
|
||||
|
||||
const Drafts = {
|
||||
components: {
|
||||
Draft,
|
||||
List,
|
||||
ConfirmModal,
|
||||
ConfirmModal: defineAsyncComponent(
|
||||
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||
),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showingConfirmDialog: false
|
||||
showingConfirmDialog: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -23,12 +26,14 @@ const Drafts = {
|
|||
this.showingConfirmDialog = true
|
||||
},
|
||||
doAbandonAll() {
|
||||
this.$store.dispatch('abandonAllDrafts').then(() => this.hideConfirmDialog())
|
||||
this.$store
|
||||
.dispatch('abandonAllDrafts')
|
||||
.then(() => this.hideConfirmDialog())
|
||||
},
|
||||
hideConfirmDialog() {
|
||||
this.showingConfirmDialog = false
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default Drafts
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
</div>
|
||||
<template v-else>
|
||||
<List
|
||||
:items="drafts"
|
||||
:external-items="drafts"
|
||||
:non-interactive="true"
|
||||
>
|
||||
<template #item="{ item: draft }">
|
||||
|
|
@ -58,6 +58,7 @@
|
|||
.Drafts {
|
||||
.draft {
|
||||
margin: 1em 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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 PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
|
||||
const EditStatusForm = {
|
||||
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 '../modal/modal.vue'
|
||||
import Modal from 'src/components/modal/modal.vue'
|
||||
|
||||
import { useEditStatusStore } from 'src/stores/editStatus.js'
|
||||
|
||||
const EditStatusModal = {
|
||||
components: {
|
||||
EditStatusForm,
|
||||
EditStatusForm: defineAsyncComponent(
|
||||
() => import('src/components/edit_status_form/edit_status_form.vue'),
|
||||
),
|
||||
Modal,
|
||||
},
|
||||
data() {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
:params="params"
|
||||
@posted="doCloseModal"
|
||||
@draft-done="doCloseModal"
|
||||
@can-close="doCloseModal"
|
||||
@close-accepted="doCloseModal"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -333,7 +333,6 @@ const EmojiInput = {
|
|||
if (!this.pickerShown) {
|
||||
this.scrollIntoView()
|
||||
this.$refs.picker.showPicker()
|
||||
this.$refs.picker.startEmojiLoad()
|
||||
} else {
|
||||
this.$refs.picker.hidePicker()
|
||||
}
|
||||
|
|
@ -590,7 +589,7 @@ const EmojiInput = {
|
|||
setCaret({ target: { selectionStart } }) {
|
||||
this.caret = selectionStart
|
||||
this.$nextTick(() => {
|
||||
this.$refs.suggestorPopover.updateStyles()
|
||||
this.$refs.suggestorPopover?.updateStyles()
|
||||
})
|
||||
},
|
||||
autoCompleteItemLabel(suggestion) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div
|
||||
ref="root"
|
||||
class="input emoji-input"
|
||||
:class="{ '-with-picker': !hideEmojiButton, '-textarea': this.input?.tagName === 'TEXTAREA' }"
|
||||
:class="{ '-with-picker': !hideEmojiButton, '-textarea': input?.tagName === 'TEXTAREA' }"
|
||||
>
|
||||
<slot
|
||||
:id="'textbox-' + randomSeed"
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
:title="$t('emoji.add_emoji')"
|
||||
@click.prevent="togglePicker"
|
||||
>
|
||||
<FAIcon :icon="['far', 'smile-beam']" />
|
||||
<FAIcon :icon="['far', 'face-smile-beam']" />
|
||||
</button>
|
||||
<EmojiPicker
|
||||
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
|
||||
* data: object providing source information for specific types of suggestions:
|
||||
|
|
@ -28,14 +26,14 @@ export default (data) => {
|
|||
}
|
||||
|
||||
export const suggestEmoji = (emojis) => (input, nameKeywordLocalizer) => {
|
||||
const noPrefix = input.toLowerCase().substr(1)
|
||||
const noPrefix = input.toLowerCase().substring(1)
|
||||
return emojis
|
||||
.map((emoji) => ({ ...emoji, ...nameKeywordLocalizer(emoji) }))
|
||||
.filter(
|
||||
(emoji) =>
|
||||
emoji.names
|
||||
.concat(emoji.keywords)
|
||||
.filter((kw) => kw.toLowerCase().match(noPrefix)).length,
|
||||
.filter((kw) => kw.toLowerCase().includes(noPrefix)).length,
|
||||
)
|
||||
.map((k) => {
|
||||
let score = 0
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import { chunk, debounce, trim } from 'lodash'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
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 { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
|
|
@ -129,6 +127,7 @@ const EmojiPicker = {
|
|||
hideCustomEmojiInPicker: false,
|
||||
// Lazy-load only after the first time `showing` becomes true.
|
||||
contentLoaded: false,
|
||||
popoverShown: false,
|
||||
groupRefs: {},
|
||||
emojiRefs: {},
|
||||
filteredEmojiGroups: [],
|
||||
|
|
@ -138,10 +137,10 @@ const EmojiPicker = {
|
|||
},
|
||||
components: {
|
||||
StickerPicker: defineAsyncComponent(
|
||||
() => import('../sticker_picker/sticker_picker.vue'),
|
||||
() => import('src/components/sticker_picker/sticker_picker.vue'),
|
||||
),
|
||||
Checkbox,
|
||||
StillImage,
|
||||
|
||||
Popover,
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -176,6 +175,13 @@ const EmojiPicker = {
|
|||
const fullEmojiSize = emojiSizeReal + 2 * 0.2 * fontSizeMultiplier * 14
|
||||
this.emojiSize = fullEmojiSize
|
||||
},
|
||||
togglePicker() {
|
||||
if (this.popoverShown) {
|
||||
this.hidePicker()
|
||||
} else {
|
||||
this.showPicker()
|
||||
}
|
||||
},
|
||||
showPicker() {
|
||||
this.$refs.popover.showPopover()
|
||||
this.$nextTick(() => {
|
||||
|
|
@ -194,10 +200,10 @@ const EmojiPicker = {
|
|||
}
|
||||
},
|
||||
onPopoverShown() {
|
||||
this.$emit('show')
|
||||
this.popoverShown = true
|
||||
},
|
||||
onPopoverClosed() {
|
||||
this.$emit('close')
|
||||
this.popoverShown = false
|
||||
},
|
||||
onStickerUploaded(e) {
|
||||
this.$emit('sticker-uploaded', e)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
trigger="click"
|
||||
popover-class="emoji-picker popover-default"
|
||||
:hide-trigger="true"
|
||||
placement="bottom"
|
||||
@show="onPopoverShown"
|
||||
@close="onPopoverClosed"
|
||||
>
|
||||
|
|
@ -48,7 +49,7 @@
|
|||
v-if="group.image"
|
||||
class="emoji-picker-header-image"
|
||||
>
|
||||
<still-image
|
||||
<StillImage
|
||||
:alt="group.text"
|
||||
:src="group.image"
|
||||
/>
|
||||
|
|
@ -130,7 +131,7 @@
|
|||
v-if="!emoji.imageUrl"
|
||||
class="emoji-picker-emoji -unicode"
|
||||
>{{ emoji.replacement }}</span>
|
||||
<still-image
|
||||
<StillImage
|
||||
v-else
|
||||
class="emoji-picker-emoji -custom"
|
||||
loading="lazy"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import StillImage from 'src/components/still-image/still-image.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserListPopover from '../user_list_popover/user_list_popover.vue'
|
||||
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||
import UserListPopover from 'src/components/user_list_popover/user_list_popover.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
|
@ -17,7 +16,6 @@ const EmojiReactions = {
|
|||
components: {
|
||||
UserAvatar,
|
||||
UserListPopover,
|
||||
StillImage,
|
||||
},
|
||||
props: ['status'],
|
||||
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 {
|
||||
requestFollow,
|
||||
requestUnfollow,
|
||||
} from '../../services/follow_manipulate/follow_manipulate'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
export default {
|
||||
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
|
||||
components: {
|
||||
ConfirmModal,
|
||||
ConfirmModal: defineAsyncComponent(
|
||||
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||
),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
>
|
||||
{{ label }}
|
||||
<teleport to="#modal">
|
||||
<confirm-modal
|
||||
<ConfirmModal
|
||||
v-if="showingConfirmUnfollow"
|
||||
:title="$t('user_card.unfollow_confirm_title')"
|
||||
:confirm-text="$t('user_card.unfollow_confirm_accept_button')"
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
/>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
</ConfirmModal>
|
||||
</teleport>
|
||||
</button>
|
||||
</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