Merge remote-tracking branch 'origin/main' into add-nip-05-modal
103
.gitlab-ci.yml
|
@ -1,86 +1,27 @@
|
||||||
image: node:21
|
image: node:22
|
||||||
|
|
||||||
variables:
|
|
||||||
NODE_ENV: test
|
|
||||||
DS_EXCLUDED_ANALYZERS: gemnasium-python
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
interruptible: true
|
interruptible: true
|
||||||
|
|
||||||
cache: &cache
|
|
||||||
key:
|
|
||||||
files:
|
|
||||||
- yarn.lock
|
|
||||||
paths:
|
|
||||||
- node_modules/
|
|
||||||
policy: pull
|
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- deps
|
- build
|
||||||
- test
|
|
||||||
- deploy
|
- deploy
|
||||||
- release
|
|
||||||
|
|
||||||
deps:
|
|
||||||
stage: deps
|
|
||||||
script: yarn install --ignore-scripts
|
|
||||||
only:
|
|
||||||
changes:
|
|
||||||
- yarn.lock
|
|
||||||
cache:
|
|
||||||
<<: *cache
|
|
||||||
policy: push
|
|
||||||
|
|
||||||
lint:
|
|
||||||
stage: test
|
|
||||||
script: yarn lint
|
|
||||||
only:
|
|
||||||
changes:
|
|
||||||
- "**/*.js"
|
|
||||||
- "**/*.jsx"
|
|
||||||
- "**/*.cjs"
|
|
||||||
- "**/*.mjs"
|
|
||||||
- "**/*.ts"
|
|
||||||
- "**/*.tsx"
|
|
||||||
- "**/*.scss"
|
|
||||||
- "**/*.css"
|
|
||||||
- ".eslintignore"
|
|
||||||
- ".eslintrc.json"
|
|
||||||
- ".stylelintrc.json"
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
stage: test
|
stage: build
|
||||||
before_script:
|
before_script:
|
||||||
|
- yarn install --ignore-scripts
|
||||||
- apt-get update -y && apt-get install -y zip
|
- apt-get update -y && apt-get install -y zip
|
||||||
script:
|
script:
|
||||||
- yarn build
|
- yarn lint
|
||||||
|
- yarn i18n && git diff --quiet || (echo "Locale files are out of date. Please run `yarn i18n`" && exit 1)
|
||||||
|
- NODE_ENV=production yarn build
|
||||||
- cp dist/index.html dist/404.html
|
- cp dist/index.html dist/404.html
|
||||||
- cd dist && zip -r ../soapbox.zip . && cd ..
|
- cd dist && zip -r ../soapbox.zip . && cd ..
|
||||||
variables:
|
|
||||||
NODE_ENV: production
|
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- soapbox.zip
|
- soapbox.zip
|
||||||
|
|
||||||
i18n:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- yarn i18n
|
|
||||||
- git diff --quiet || (echo "Locale files are out of date. Please run `yarn i18n`" && exit 1)
|
|
||||||
|
|
||||||
docs-deploy:
|
|
||||||
stage: deploy
|
|
||||||
image: alpine:latest
|
|
||||||
before_script:
|
|
||||||
- apk add curl
|
|
||||||
script:
|
|
||||||
- curl -X POST -F"token=$CI_JOB_TOKEN" -F'ref=master' https://gitlab.com/api/v4/projects/15685485/trigger/pipeline
|
|
||||||
only:
|
|
||||||
variables:
|
|
||||||
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
|
|
||||||
changes:
|
|
||||||
- "docs/**/*"
|
|
||||||
|
|
||||||
review:
|
review:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
environment:
|
environment:
|
||||||
|
@ -92,6 +33,7 @@ review:
|
||||||
- unzip soapbox.zip -d dist
|
- unzip soapbox.zip -d dist
|
||||||
- npx -y surge dist $CI_COMMIT_REF_SLUG.git.soapbox.pub
|
- npx -y surge dist $CI_COMMIT_REF_SLUG.git.soapbox.pub
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
|
when: manual
|
||||||
|
|
||||||
pages:
|
pages:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
|
@ -107,31 +49,4 @@ pages:
|
||||||
- public
|
- public
|
||||||
only:
|
only:
|
||||||
variables:
|
variables:
|
||||||
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
|
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
|
||||||
|
|
||||||
docker:
|
|
||||||
stage: deploy
|
|
||||||
image: docker:25.0.3
|
|
||||||
services:
|
|
||||||
- docker:25.0.3-dind
|
|
||||||
tags:
|
|
||||||
- dind
|
|
||||||
# https://medium.com/devops-with-valentine/how-to-build-a-docker-image-and-push-it-to-the-gitlab-container-registry-from-a-gitlab-ci-pipeline-acac0d1f26df
|
|
||||||
script:
|
|
||||||
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin
|
|
||||||
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG .
|
|
||||||
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG
|
|
||||||
interruptible: false
|
|
||||||
|
|
||||||
release:
|
|
||||||
stage: release
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG
|
|
||||||
script:
|
|
||||||
- npx tsx ./scripts/do-release.ts
|
|
||||||
interruptible: false
|
|
||||||
|
|
||||||
include:
|
|
||||||
- template: Jobs/Dependency-Scanning.gitlab-ci.yml
|
|
30
package.json
|
@ -41,7 +41,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@akryum/flexsearch-es": "^0.7.32",
|
"@akryum/flexsearch-es": "^0.7.32",
|
||||||
"@emoji-mart/data": "^1.1.2",
|
"@emoji-mart/data": "^1.2.1",
|
||||||
"@floating-ui/react": "^0.26.0",
|
"@floating-ui/react": "^0.26.0",
|
||||||
"@fontsource/inter": "^5.0.0",
|
"@fontsource/inter": "^5.0.0",
|
||||||
"@fontsource/noto-sans-javanese": "^5.0.16",
|
"@fontsource/noto-sans-javanese": "^5.0.16",
|
||||||
|
@ -65,13 +65,13 @@
|
||||||
"@sentry/browser": "^8.34.0",
|
"@sentry/browser": "^8.34.0",
|
||||||
"@sentry/react": "^8.34.0",
|
"@sentry/react": "^8.34.0",
|
||||||
"@sentry/types": "^8.34.0",
|
"@sentry/types": "^8.34.0",
|
||||||
"@soapbox.pub/wasmboy": "^0.8.0",
|
|
||||||
"@soapbox/weblock": "npm:@jsr/soapbox__weblock",
|
"@soapbox/weblock": "npm:@jsr/soapbox__weblock",
|
||||||
"@tabler/icons": "^3.1.0",
|
"@tabler/icons": "^3.19.0",
|
||||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.59.13",
|
||||||
|
"@twemoji/svg": "^15.0.0",
|
||||||
"@types/escape-html": "^1.0.1",
|
"@types/escape-html": "^1.0.1",
|
||||||
"@types/http-link-header": "^1.0.3",
|
"@types/http-link-header": "^1.0.3",
|
||||||
"@types/leaflet": "^1.8.0",
|
"@types/leaflet": "^1.8.0",
|
||||||
|
@ -80,7 +80,6 @@
|
||||||
"@types/path-browserify": "^1.0.0",
|
"@types/path-browserify": "^1.0.0",
|
||||||
"@types/react": "^18.3.9",
|
"@types/react": "^18.3.9",
|
||||||
"@types/react-color": "^3.0.6",
|
"@types/react-color": "^3.0.6",
|
||||||
"@types/react-datepicker": "^4.4.2",
|
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/react-helmet": "^6.1.5",
|
"@types/react-helmet": "^6.1.5",
|
||||||
"@types/react-motion": "^0.0.40",
|
"@types/react-motion": "^0.0.40",
|
||||||
|
@ -99,11 +98,10 @@
|
||||||
"browserslist": "^4.16.6",
|
"browserslist": "^4.16.6",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"comlink": "^4.4.1",
|
"comlink": "^4.4.1",
|
||||||
"cryptocurrency-icons": "^0.18.1",
|
|
||||||
"cssnano": "^6.0.0",
|
"cssnano": "^6.0.0",
|
||||||
"detect-passive-events": "^2.0.0",
|
"detect-passive-events": "^2.0.0",
|
||||||
"emoji-datasource": "14.0.0",
|
"emoji-datasource": "15.0.1",
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.6.0",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"eslint-plugin-formatjs": "^4.12.2",
|
"eslint-plugin-formatjs": "^4.12.2",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
|
@ -128,7 +126,6 @@
|
||||||
"qrcode.react": "^3.1.0",
|
"qrcode.react": "^3.1.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-datepicker": "^4.8.0",
|
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-error-boundary": "^4.0.11",
|
"react-error-boundary": "^4.0.11",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
|
@ -149,10 +146,9 @@
|
||||||
"redux": "^5.0.0",
|
"redux": "^5.0.0",
|
||||||
"redux-thunk": "^3.1.0",
|
"redux-thunk": "^3.1.0",
|
||||||
"reselect": "^5.0.0",
|
"reselect": "^5.0.0",
|
||||||
"sass": "^1.69.5",
|
"sass": "^1.79.5",
|
||||||
"semver": "^7.3.8",
|
"semver": "^7.3.8",
|
||||||
"stringz": "^2.0.0",
|
"stringz": "^2.0.0",
|
||||||
"twemoji": "https://github.com/twitter/twemoji#v14.0.2",
|
|
||||||
"type-fest": "^4.0.0",
|
"type-fest": "^4.0.0",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"vite": "^5.4.8",
|
"vite": "^5.4.8",
|
||||||
|
@ -181,15 +177,15 @@
|
||||||
"eslint-plugin-promise": "^6.0.0",
|
"eslint-plugin-promise": "^6.0.0",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-tailwindcss": "^3.13.0",
|
"eslint-plugin-tailwindcss": "^3.17.5",
|
||||||
"fake-indexeddb": "^5.0.0",
|
"fake-indexeddb": "^5.0.0",
|
||||||
"husky": "^9.0.0",
|
"husky": "^9.0.0",
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"lint-staged": ">=10",
|
"lint-staged": ">=10",
|
||||||
"rollup-plugin-visualizer": "^5.9.2",
|
"rollup-plugin-visualizer": "^5.9.2",
|
||||||
"stylelint": "^16.0.2",
|
"stylelint": "^16.10.0",
|
||||||
"stylelint-config-standard-scss": "^12.0.0",
|
"stylelint-config-standard-scss": "^13.1.0",
|
||||||
"tailwindcss": "^3.4.0",
|
"tailwindcss": "^3.4.13",
|
||||||
"vite-plugin-checker": "^0.8.0",
|
"vite-plugin-checker": "^0.8.0",
|
||||||
"vite-plugin-pwa": "^0.20.5",
|
"vite-plugin-pwa": "^0.20.5",
|
||||||
"vitest": "^2.1.1"
|
"vitest": "^2.1.1"
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { fetchRelationships } from 'soapbox/actions/accounts';
|
||||||
import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer';
|
import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer';
|
||||||
import { accountIdsToAccts } from 'soapbox/selectors';
|
import { accountIdsToAccts } from 'soapbox/selectors';
|
||||||
import { filterBadges, getTagDiff } from 'soapbox/utils/badges';
|
import { filterBadges, getTagDiff } from 'soapbox/utils/badges';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
|
||||||
|
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
|
@ -71,16 +70,6 @@ const ADMIN_REMOVE_PERMISSION_GROUP_REQUEST = 'ADMIN_REMOVE_PERMISSION_GROUP_REQ
|
||||||
const ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS = 'ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS';
|
const ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS = 'ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS';
|
||||||
const ADMIN_REMOVE_PERMISSION_GROUP_FAIL = 'ADMIN_REMOVE_PERMISSION_GROUP_FAIL';
|
const ADMIN_REMOVE_PERMISSION_GROUP_FAIL = 'ADMIN_REMOVE_PERMISSION_GROUP_FAIL';
|
||||||
|
|
||||||
const ADMIN_USER_INDEX_EXPAND_FAIL = 'ADMIN_USER_INDEX_EXPAND_FAIL';
|
|
||||||
const ADMIN_USER_INDEX_EXPAND_REQUEST = 'ADMIN_USER_INDEX_EXPAND_REQUEST';
|
|
||||||
const ADMIN_USER_INDEX_EXPAND_SUCCESS = 'ADMIN_USER_INDEX_EXPAND_SUCCESS';
|
|
||||||
|
|
||||||
const ADMIN_USER_INDEX_FETCH_FAIL = 'ADMIN_USER_INDEX_FETCH_FAIL';
|
|
||||||
const ADMIN_USER_INDEX_FETCH_REQUEST = 'ADMIN_USER_INDEX_FETCH_REQUEST';
|
|
||||||
const ADMIN_USER_INDEX_FETCH_SUCCESS = 'ADMIN_USER_INDEX_FETCH_SUCCESS';
|
|
||||||
|
|
||||||
const ADMIN_USER_INDEX_QUERY_SET = 'ADMIN_USER_INDEX_QUERY_SET';
|
|
||||||
|
|
||||||
const fetchConfig = () =>
|
const fetchConfig = () =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST });
|
dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST });
|
||||||
|
@ -118,100 +107,52 @@ const updateSoapboxConfig = (data: Record<string, any>) =>
|
||||||
return dispatch(updateConfig(params));
|
return dispatch(updateConfig(params));
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchMastodonReports = (params: Record<string, any>) =>
|
function fetchReports(params: Record<string, any> = {}) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
return async (dispatch: AppDispatch, getState: () => RootState): Promise<void> => {
|
||||||
api(getState)
|
|
||||||
.get('/api/v1/admin/reports', { params })
|
|
||||||
.then(({ data: reports }) => {
|
|
||||||
reports.forEach((report: APIEntity) => {
|
|
||||||
dispatch(importFetchedAccount(report.account?.account));
|
|
||||||
dispatch(importFetchedAccount(report.target_account?.account));
|
|
||||||
dispatch(importFetchedStatuses(report.statuses));
|
|
||||||
});
|
|
||||||
dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params });
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchPleromaReports = (params: Record<string, any>) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
|
||||||
api(getState)
|
|
||||||
.get('/api/v1/pleroma/admin/reports', { params })
|
|
||||||
.then(({ data: { reports } }) => {
|
|
||||||
reports.forEach((report: APIEntity) => {
|
|
||||||
dispatch(importFetchedAccount(report.account));
|
|
||||||
dispatch(importFetchedAccount(report.actor));
|
|
||||||
dispatch(importFetchedStatuses(report.statuses));
|
|
||||||
});
|
|
||||||
dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params });
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchReports = (params: Record<string, any> = {}) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params });
|
dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params });
|
||||||
|
|
||||||
if (features.mastodonAdmin) {
|
try {
|
||||||
return dispatch(fetchMastodonReports(params));
|
const { data: reports } = await api(getState).get('/api/v1/admin/reports', { params });
|
||||||
} else {
|
reports.forEach((report: APIEntity) => {
|
||||||
const { resolved } = params;
|
dispatch(importFetchedAccount(report.account?.account));
|
||||||
|
dispatch(importFetchedAccount(report.target_account?.account));
|
||||||
return dispatch(fetchPleromaReports({
|
dispatch(importFetchedStatuses(report.statuses));
|
||||||
state: resolved === false ? 'open' : (resolved ? 'resolved' : null),
|
});
|
||||||
}));
|
dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params });
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const patchMastodonReports = (reports: { id: string; state: string }[]) =>
|
function patchReports(ids: string[], reportState: string) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
return (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
Promise.all(reports.map(({ id, state }) => api(getState)
|
|
||||||
.post(`/api/v1/admin/reports/${id}/${state === 'resolved' ? 'reopen' : 'resolve'}`)
|
|
||||||
.then(() => {
|
|
||||||
dispatch({ type: ADMIN_REPORTS_PATCH_SUCCESS, reports });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_REPORTS_PATCH_FAIL, error, reports });
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
|
|
||||||
const patchPleromaReports = (reports: { id: string; state: string }[]) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
|
||||||
api(getState)
|
|
||||||
.patch('/api/v1/pleroma/admin/reports', { reports })
|
|
||||||
.then(() => {
|
|
||||||
dispatch({ type: ADMIN_REPORTS_PATCH_SUCCESS, reports });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_REPORTS_PATCH_FAIL, error, reports });
|
|
||||||
});
|
|
||||||
|
|
||||||
const patchReports = (ids: string[], reportState: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
const reports = ids.map(id => ({ id, state: reportState }));
|
const reports = ids.map(id => ({ id, state: reportState }));
|
||||||
|
|
||||||
dispatch({ type: ADMIN_REPORTS_PATCH_REQUEST, reports });
|
dispatch({ type: ADMIN_REPORTS_PATCH_REQUEST, reports });
|
||||||
|
|
||||||
if (features.mastodonAdmin) {
|
return Promise.all(
|
||||||
return dispatch(patchMastodonReports(reports));
|
reports.map(async ({ id, state }) => {
|
||||||
} else {
|
try {
|
||||||
return dispatch(patchPleromaReports(reports));
|
await api(getState).post(`/api/v1/admin/reports/${id}/${state === 'resolved' ? 'reopen' : 'resolve'}`);
|
||||||
}
|
dispatch({ type: ADMIN_REPORTS_PATCH_SUCCESS, reports });
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({ type: ADMIN_REPORTS_PATCH_FAIL, error, reports });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const closeReports = (ids: string[]) =>
|
function closeReports(ids: string[]) {
|
||||||
patchReports(ids, 'closed');
|
return patchReports(ids, 'closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchUsers(filters: string[] = [], page = 1, query?: string | null, pageSize = 50, url?: string | null) {
|
||||||
|
return async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch({ type: ADMIN_USERS_FETCH_REQUEST, filters, page, pageSize });
|
||||||
|
|
||||||
const fetchMastodonUsers = (filters: string[], page: number, query: string | null | undefined, pageSize: number, next?: string | null) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const params: Record<string, any> = {
|
const params: Record<string, any> = {
|
||||||
username: query,
|
username: query,
|
||||||
};
|
};
|
||||||
|
@ -220,106 +161,49 @@ const fetchMastodonUsers = (filters: string[], page: number, query: string | nul
|
||||||
if (filters.includes('active')) params.active = true;
|
if (filters.includes('active')) params.active = true;
|
||||||
if (filters.includes('need_approval')) params.pending = true;
|
if (filters.includes('need_approval')) params.pending = true;
|
||||||
|
|
||||||
return api(getState)
|
try {
|
||||||
.get(next || '/api/v1/admin/accounts', { params })
|
const { data: accounts, ...response } = await api(getState).get(url || '/api/v1/admin/accounts', { params });
|
||||||
.then(({ data: accounts, ...response }) => {
|
const next = getLinks(response as AxiosResponse<any, any>).refs.find(link => link.rel === 'next')?.uri;
|
||||||
const next = getLinks(response as AxiosResponse<any, any>).refs.find(link => link.rel === 'next');
|
|
||||||
|
|
||||||
const count = next
|
dispatch(importFetchedAccounts(accounts.map(({ account }: APIEntity) => account)));
|
||||||
? page * pageSize + 1
|
dispatch(fetchRelationships(accounts.map((account_1: APIEntity) => account_1.id)));
|
||||||
: (page - 1) * pageSize + accounts.length;
|
dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, accounts, pageSize, filters, page, next });
|
||||||
|
return { accounts, next };
|
||||||
dispatch(importFetchedAccounts(accounts.map(({ account }: APIEntity) => account)));
|
} catch (error) {
|
||||||
dispatch(fetchRelationships(accounts.map((account: APIEntity) => account.id)));
|
return dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize });
|
||||||
dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users: accounts, count, pageSize, filters, page, next: next?.uri || false });
|
|
||||||
return { users: accounts, count, pageSize, next: next?.uri || false };
|
|
||||||
}).catch(error =>
|
|
||||||
dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize }),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchPleromaUsers = (filters: string[], page: number, query?: string | null, pageSize?: number) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const params: Record<string, any> = { filters: filters.join(), page, page_size: pageSize };
|
|
||||||
if (query) params.query = query;
|
|
||||||
|
|
||||||
return api(getState)
|
|
||||||
.get('/api/v1/pleroma/admin/users', { params })
|
|
||||||
.then(({ data: { users, count, page_size: pageSize } }) => {
|
|
||||||
dispatch(fetchRelationships(users.map((user: APIEntity) => user.id)));
|
|
||||||
dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users, count, pageSize, filters, page });
|
|
||||||
return { users, count, pageSize };
|
|
||||||
}).catch(error =>
|
|
||||||
dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize }),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchUsers = (filters: string[] = [], page = 1, query?: string | null, pageSize = 50, next?: string | null) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USERS_FETCH_REQUEST, filters, page, pageSize });
|
|
||||||
|
|
||||||
if (features.mastodonAdmin) {
|
|
||||||
return dispatch(fetchMastodonUsers(filters, page, query, pageSize, next));
|
|
||||||
} else {
|
|
||||||
return dispatch(fetchPleromaUsers(filters, page, query, pageSize));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const revokeName = (accountId: string, reportId?: string) =>
|
function revokeName(accountId: string, reportId?: string) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
return (_dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
api(getState)
|
const params = {
|
||||||
.post(`/api/v1/admin/accounts/${accountId}/action`, {
|
type: 'revoke_name',
|
||||||
type: 'revoke_name',
|
report_id: reportId,
|
||||||
report_id: reportId,
|
};
|
||||||
});
|
|
||||||
|
|
||||||
const deactivateMastodonUsers = (accountIds: string[], reportId?: string) =>
|
return api(getState).post(`/api/v1/admin/accounts/${accountId}/action`, params);
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
};
|
||||||
Promise.all(accountIds.map(accountId => {
|
}
|
||||||
api(getState)
|
|
||||||
.post(`/api/v1/admin/accounts/${accountId}/action`, {
|
function deactivateUsers(accountIds: string[], reportId?: string) {
|
||||||
|
return (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
return Promise.all(
|
||||||
|
accountIds.map(async (accountId) => {
|
||||||
|
const params = {
|
||||||
type: 'disable',
|
type: 'disable',
|
||||||
report_id: reportId,
|
report_id: reportId,
|
||||||
})
|
};
|
||||||
.then(() => {
|
try {
|
||||||
|
await api(getState).post(`/api/v1/admin/accounts/${accountId}/action`, params);
|
||||||
dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, accountIds: [accountId] });
|
dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, accountIds: [accountId] });
|
||||||
}).catch(error => {
|
} catch (error) {
|
||||||
dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, accountIds: [accountId] });
|
dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, accountIds: [accountId] });
|
||||||
});
|
}
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
const deactivatePleromaUsers = (accountIds: string[]) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const nicknames = accountIdsToAccts(getState(), accountIds);
|
|
||||||
return api(getState)
|
|
||||||
.patch('/api/v1/pleroma/admin/users/deactivate', { nicknames })
|
|
||||||
.then(({ data: { users } }) => {
|
|
||||||
dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, users, accountIds });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, accountIds });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deactivateUsers = (accountIds: string[], reportId?: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USERS_DEACTIVATE_REQUEST, accountIds });
|
|
||||||
|
|
||||||
if (features.mastodonAdmin) {
|
|
||||||
return dispatch(deactivateMastodonUsers(accountIds, reportId));
|
|
||||||
} else {
|
|
||||||
return dispatch(deactivatePleromaUsers(accountIds));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const deleteUser = (accountId: string) =>
|
const deleteUser = (accountId: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
@ -334,69 +218,31 @@ const deleteUser = (accountId: string) =>
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const approveMastodonUser = (accountId: string) =>
|
function approveUser(accountId: string) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
return async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
api(getState)
|
|
||||||
.post(`/api/v1/admin/accounts/${accountId}/approve`)
|
|
||||||
.then(({ data: user }) => {
|
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, user, accountId });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountId });
|
|
||||||
});
|
|
||||||
|
|
||||||
const approvePleromaUser = (accountId: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const nicknames = accountIdsToAccts(getState(), [accountId]);
|
|
||||||
return api(getState)
|
|
||||||
.patch('/api/v1/pleroma/admin/users/approve', { nicknames })
|
|
||||||
.then(({ data: { users } }) => {
|
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, user: users[0], accountId });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountId });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const rejectMastodonUser = (accountId: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
|
||||||
api(getState)
|
|
||||||
.post(`/api/v1/admin/accounts/${accountId}/reject`)
|
|
||||||
.then(({ data: user }) => {
|
|
||||||
dispatch({ type: ADMIN_USERS_REJECT_SUCCESS, user, accountId });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_USERS_REJECT_FAIL, error, accountId });
|
|
||||||
});
|
|
||||||
|
|
||||||
const approveUser = (accountId: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, accountId });
|
dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, accountId });
|
||||||
|
try {
|
||||||
if (features.mastodonAdmin) {
|
const { data: user } = await api(getState)
|
||||||
return dispatch(approveMastodonUser(accountId));
|
.post(`/api/v1/admin/accounts/${accountId}/approve`);
|
||||||
} else {
|
dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, user, accountId });
|
||||||
return dispatch(approvePleromaUser(accountId));
|
} catch (error) {
|
||||||
|
dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountId });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const rejectUser = (accountId: string) =>
|
function rejectUser(accountId: string) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
return async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USERS_REJECT_REQUEST, accountId });
|
dispatch({ type: ADMIN_USERS_REJECT_REQUEST, accountId });
|
||||||
|
try {
|
||||||
if (features.mastodonAdmin) {
|
const { data: user } = await api(getState)
|
||||||
return dispatch(rejectMastodonUser(accountId));
|
.post(`/api/v1/admin/accounts/${accountId}/reject`);
|
||||||
} else {
|
dispatch({ type: ADMIN_USERS_REJECT_SUCCESS, user, accountId });
|
||||||
return dispatch(deleteUser(accountId));
|
} catch (error) {
|
||||||
|
dispatch({ type: ADMIN_USERS_REJECT_FAIL, error, accountId });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const deleteStatus = (id: string) =>
|
const deleteStatus = (id: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
@ -531,51 +377,6 @@ const setRole = (accountId: string, role: 'user' | 'moderator' | 'admin') =>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setUserIndexQuery = (query: string) => ({ type: ADMIN_USER_INDEX_QUERY_SET, query });
|
|
||||||
|
|
||||||
const fetchUserIndex = () =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const { filters, page, query, pageSize, isLoading } = getState().admin_user_index;
|
|
||||||
|
|
||||||
if (isLoading) return;
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_REQUEST });
|
|
||||||
|
|
||||||
dispatch(fetchUsers(filters.toJS() as string[], page + 1, query, pageSize))
|
|
||||||
.then((data: any) => {
|
|
||||||
if (data.error) {
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
|
|
||||||
} else {
|
|
||||||
const { users, count, next } = (data);
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_SUCCESS, users, count, next });
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const expandUserIndex = () =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const { filters, page, query, pageSize, isLoading, next, loaded } = getState().admin_user_index;
|
|
||||||
|
|
||||||
if (!loaded || isLoading) return;
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_REQUEST });
|
|
||||||
|
|
||||||
dispatch(fetchUsers(filters.toJS() as string[], page + 1, query, pageSize, next))
|
|
||||||
.then((data: any) => {
|
|
||||||
if (data.error) {
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
|
|
||||||
} else {
|
|
||||||
const { users, count, next } = (data);
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_SUCCESS, users, count, next });
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ADMIN_CONFIG_FETCH_REQUEST,
|
ADMIN_CONFIG_FETCH_REQUEST,
|
||||||
ADMIN_CONFIG_FETCH_SUCCESS,
|
ADMIN_CONFIG_FETCH_SUCCESS,
|
||||||
|
@ -622,13 +423,6 @@ export {
|
||||||
ADMIN_REMOVE_PERMISSION_GROUP_REQUEST,
|
ADMIN_REMOVE_PERMISSION_GROUP_REQUEST,
|
||||||
ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS,
|
ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS,
|
||||||
ADMIN_REMOVE_PERMISSION_GROUP_FAIL,
|
ADMIN_REMOVE_PERMISSION_GROUP_FAIL,
|
||||||
ADMIN_USER_INDEX_EXPAND_FAIL,
|
|
||||||
ADMIN_USER_INDEX_EXPAND_REQUEST,
|
|
||||||
ADMIN_USER_INDEX_EXPAND_SUCCESS,
|
|
||||||
ADMIN_USER_INDEX_FETCH_FAIL,
|
|
||||||
ADMIN_USER_INDEX_FETCH_REQUEST,
|
|
||||||
ADMIN_USER_INDEX_FETCH_SUCCESS,
|
|
||||||
ADMIN_USER_INDEX_QUERY_SET,
|
|
||||||
fetchConfig,
|
fetchConfig,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
updateSoapboxConfig,
|
updateSoapboxConfig,
|
||||||
|
@ -652,7 +446,4 @@ export {
|
||||||
promoteToModerator,
|
promoteToModerator,
|
||||||
demoteToUser,
|
demoteToUser,
|
||||||
setRole,
|
setRole,
|
||||||
setUserIndexQuery,
|
|
||||||
fetchUserIndex,
|
|
||||||
expandUserIndex,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { AppDispatch, RootState } from 'soapbox/store';
|
||||||
|
import { getFeatures, parseVersion } from 'soapbox/utils/features';
|
||||||
|
|
||||||
|
import type { Status } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS' as const;
|
||||||
|
|
||||||
|
export interface ComposeSetStatusAction {
|
||||||
|
type: typeof COMPOSE_SET_STATUS;
|
||||||
|
id: string;
|
||||||
|
status: Status;
|
||||||
|
rawText: string;
|
||||||
|
explicitAddressing: boolean;
|
||||||
|
spoilerText?: string;
|
||||||
|
contentType?: string | false;
|
||||||
|
v: ReturnType<typeof parseVersion>;
|
||||||
|
withRedraft?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setComposeToStatus = (status: Status, rawText: string, spoilerText?: string, contentType?: string | false, withRedraft?: boolean) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
const { instance } = getState();
|
||||||
|
const { explicitAddressing } = getFeatures(instance);
|
||||||
|
|
||||||
|
const action: ComposeSetStatusAction = {
|
||||||
|
type: COMPOSE_SET_STATUS,
|
||||||
|
id: 'compose-modal',
|
||||||
|
status,
|
||||||
|
rawText,
|
||||||
|
explicitAddressing,
|
||||||
|
spoilerText,
|
||||||
|
contentType,
|
||||||
|
v: parseVersion(instance.version),
|
||||||
|
withRedraft,
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(action);
|
||||||
|
};
|
|
@ -11,8 +11,9 @@ import { selectAccount, selectOwnAccount } from 'soapbox/selectors';
|
||||||
import { tagHistory } from 'soapbox/settings';
|
import { tagHistory } from 'soapbox/settings';
|
||||||
import toast from 'soapbox/toast';
|
import toast from 'soapbox/toast';
|
||||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||||
import { getFeatures, parseVersion } from 'soapbox/utils/features';
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
|
||||||
|
import { ComposeSetStatusAction } from './compose-status';
|
||||||
import { chooseEmoji } from './emojis';
|
import { chooseEmoji } from './emojis';
|
||||||
import { importFetchedAccounts } from './importer';
|
import { importFetchedAccounts } from './importer';
|
||||||
import { uploadFile, updateMedia } from './media';
|
import { uploadFile, updateMedia } from './media';
|
||||||
|
@ -85,8 +86,6 @@ const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE' as const;
|
||||||
const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS' as const;
|
const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS' as const;
|
||||||
const COMPOSE_REMOVE_FROM_MENTIONS = 'COMPOSE_REMOVE_FROM_MENTIONS' as const;
|
const COMPOSE_REMOVE_FROM_MENTIONS = 'COMPOSE_REMOVE_FROM_MENTIONS' as const;
|
||||||
|
|
||||||
const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS' as const;
|
|
||||||
|
|
||||||
const COMPOSE_EDITOR_STATE_SET = 'COMPOSE_EDITOR_STATE_SET' as const;
|
const COMPOSE_EDITOR_STATE_SET = 'COMPOSE_EDITOR_STATE_SET' as const;
|
||||||
|
|
||||||
const COMPOSE_CHANGE_MEDIA_ORDER = 'COMPOSE_CHANGE_MEDIA_ORDER' as const;
|
const COMPOSE_CHANGE_MEDIA_ORDER = 'COMPOSE_CHANGE_MEDIA_ORDER' as const;
|
||||||
|
@ -102,38 +101,6 @@ const messages = defineMessages({
|
||||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ComposeSetStatusAction {
|
|
||||||
type: typeof COMPOSE_SET_STATUS;
|
|
||||||
id: string;
|
|
||||||
status: Status;
|
|
||||||
rawText: string;
|
|
||||||
explicitAddressing: boolean;
|
|
||||||
spoilerText?: string;
|
|
||||||
contentType?: string | false;
|
|
||||||
v: ReturnType<typeof parseVersion>;
|
|
||||||
withRedraft?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const setComposeToStatus = (status: Status, rawText: string, spoilerText?: string, contentType?: string | false, withRedraft?: boolean) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const { instance } = getState();
|
|
||||||
const { explicitAddressing } = getFeatures(instance);
|
|
||||||
|
|
||||||
const action: ComposeSetStatusAction = {
|
|
||||||
type: COMPOSE_SET_STATUS,
|
|
||||||
id: 'compose-modal',
|
|
||||||
status,
|
|
||||||
rawText,
|
|
||||||
explicitAddressing,
|
|
||||||
spoilerText,
|
|
||||||
contentType,
|
|
||||||
v: parseVersion(instance.version),
|
|
||||||
withRedraft,
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch(action);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeCompose = (composeId: string, text: string) => ({
|
const changeCompose = (composeId: string, text: string) => ({
|
||||||
type: COMPOSE_CHANGE,
|
type: COMPOSE_CHANGE,
|
||||||
id: composeId,
|
id: composeId,
|
||||||
|
@ -952,11 +919,9 @@ export {
|
||||||
COMPOSE_SCHEDULE_REMOVE,
|
COMPOSE_SCHEDULE_REMOVE,
|
||||||
COMPOSE_ADD_TO_MENTIONS,
|
COMPOSE_ADD_TO_MENTIONS,
|
||||||
COMPOSE_REMOVE_FROM_MENTIONS,
|
COMPOSE_REMOVE_FROM_MENTIONS,
|
||||||
COMPOSE_SET_STATUS,
|
|
||||||
COMPOSE_EDITOR_STATE_SET,
|
COMPOSE_EDITOR_STATE_SET,
|
||||||
COMPOSE_SET_GROUP_TIMELINE_VISIBLE,
|
COMPOSE_SET_GROUP_TIMELINE_VISIBLE,
|
||||||
COMPOSE_CHANGE_MEDIA_ORDER,
|
COMPOSE_CHANGE_MEDIA_ORDER,
|
||||||
setComposeToStatus,
|
|
||||||
changeCompose,
|
changeCompose,
|
||||||
replyCompose,
|
replyCompose,
|
||||||
cancelReplyCompose,
|
cancelReplyCompose,
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { register, saveSettings } from './registerer';
|
|
||||||
import {
|
|
||||||
SET_BROWSER_SUPPORT,
|
|
||||||
SET_SUBSCRIPTION,
|
|
||||||
CLEAR_SUBSCRIPTION,
|
|
||||||
SET_ALERTS,
|
|
||||||
setAlerts,
|
|
||||||
} from './setter';
|
|
||||||
|
|
||||||
import type { AppDispatch } from 'soapbox/store';
|
|
||||||
|
|
||||||
export {
|
|
||||||
SET_BROWSER_SUPPORT,
|
|
||||||
SET_SUBSCRIPTION,
|
|
||||||
CLEAR_SUBSCRIPTION,
|
|
||||||
SET_ALERTS,
|
|
||||||
register,
|
|
||||||
changeAlerts,
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeAlerts = (path: Array<string>, value: any) =>
|
|
||||||
(dispatch: AppDispatch) => {
|
|
||||||
dispatch(setAlerts(path, value));
|
|
||||||
dispatch(saveSettings() as any);
|
|
||||||
};
|
|
|
@ -1,159 +1,108 @@
|
||||||
import { createPushSubscription, updatePushSubscription } from 'soapbox/actions/push-subscriptions';
|
/* eslint-disable compat/compat */
|
||||||
import { pushNotificationsSetting } from 'soapbox/settings';
|
import { HTTPError } from 'soapbox/api/HTTPError';
|
||||||
import { getVapidKey } from 'soapbox/utils/auth';
|
import { MastodonClient } from 'soapbox/api/MastodonClient';
|
||||||
import { decode as decodeBase64 } from 'soapbox/utils/base64';
|
import { WebPushSubscription, webPushSubscriptionSchema } from 'soapbox/schemas/web-push';
|
||||||
|
import { decodeBase64Url } from 'soapbox/utils/base64';
|
||||||
import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
|
|
||||||
|
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
|
||||||
import type { Me } from 'soapbox/types/soapbox';
|
|
||||||
|
|
||||||
// Taken from https://www.npmjs.com/package/web-push
|
|
||||||
const urlBase64ToUint8Array = (base64String: string) => {
|
|
||||||
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
|
||||||
const base64 = (base64String + padding)
|
|
||||||
.replace(/-/g, '+')
|
|
||||||
.replace(/_/g, '/');
|
|
||||||
|
|
||||||
return decodeBase64(base64);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRegistration = () => {
|
|
||||||
if (navigator.serviceWorker) {
|
|
||||||
return navigator.serviceWorker.ready;
|
|
||||||
} else {
|
|
||||||
throw 'Your browser does not support Service Workers.';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPushSubscription = (registration: ServiceWorkerRegistration) =>
|
|
||||||
registration.pushManager.getSubscription()
|
|
||||||
.then(subscription => ({ registration, subscription }));
|
|
||||||
|
|
||||||
const subscribe = (registration: ServiceWorkerRegistration, getState: () => RootState) =>
|
|
||||||
registration.pushManager.subscribe({
|
|
||||||
userVisibleOnly: true,
|
|
||||||
applicationServerKey: urlBase64ToUint8Array(getVapidKey(getState())),
|
|
||||||
});
|
|
||||||
|
|
||||||
const unsubscribe = ({ registration, subscription }: {
|
|
||||||
registration: ServiceWorkerRegistration;
|
|
||||||
subscription: PushSubscription | null;
|
|
||||||
}) =>
|
|
||||||
subscription ? subscription.unsubscribe().then(() => registration) : new Promise<ServiceWorkerRegistration>(r => r(registration));
|
|
||||||
|
|
||||||
const sendSubscriptionToBackend = (subscription: PushSubscription, me: Me) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const alerts = getState().push_notifications.alerts.toJS();
|
|
||||||
const params = { subscription: subscription.toJSON(), data: { alerts } };
|
|
||||||
|
|
||||||
if (me) {
|
|
||||||
const data = pushNotificationsSetting.get(me);
|
|
||||||
if (data) {
|
|
||||||
params.data = data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dispatch(createPushSubscription(params));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
|
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
|
||||||
// eslint-disable-next-line compat/compat
|
|
||||||
const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
|
const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
|
||||||
|
|
||||||
const register = () =>
|
/**
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
* Register web push notifications.
|
||||||
const me = getState().me;
|
* This function creates a subscription if one hasn't been created already, and syncronizes it with the backend.
|
||||||
const vapidKey = getVapidKey(getState());
|
*/
|
||||||
|
export async function registerPushNotifications(api: MastodonClient, vapidKey: string) {
|
||||||
|
if (!supportsPushNotifications) {
|
||||||
|
console.warn('Your browser does not support Web Push Notifications.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(setBrowserSupport(supportsPushNotifications));
|
const { subscription, created } = await getOrCreateSubscription(vapidKey);
|
||||||
|
|
||||||
if (!supportsPushNotifications) {
|
if (created) {
|
||||||
console.warn('Your browser does not support Web Push Notifications.');
|
await sendSubscriptionToBackend(api, subscription);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have a subscription, check if it is still valid.
|
||||||
|
const backend = await getBackendSubscription(api);
|
||||||
|
|
||||||
|
// If the VAPID public key did not change and the endpoint corresponds
|
||||||
|
// to the endpoint saved in the backend, the subscription is valid.
|
||||||
|
if (backend && subscriptionMatchesBackend(subscription, backend)) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Something went wrong, try to subscribe again.
|
||||||
|
await subscription.unsubscribe();
|
||||||
|
const newSubscription = await createSubscription(vapidKey);
|
||||||
|
await sendSubscriptionToBackend(api, newSubscription);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get an existing subscription object from the browser if it exists, or ask the browser to create one. */
|
||||||
|
async function getOrCreateSubscription(vapidKey: string): Promise<{ subscription: PushSubscription; created: boolean }> {
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
const subscription = await registration.pushManager.getSubscription();
|
||||||
|
|
||||||
|
if (subscription) {
|
||||||
|
return { subscription, created: false };
|
||||||
|
} else {
|
||||||
|
const subscription = await createSubscription(vapidKey);
|
||||||
|
return { subscription, created: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Request a subscription object from the web browser. */
|
||||||
|
async function createSubscription(vapidKey: string): Promise<PushSubscription> {
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
|
||||||
|
return registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: decodeBase64Url(vapidKey),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fetch the API for an existing subscription saved in the backend, if any. */
|
||||||
|
async function getBackendSubscription(api: MastodonClient): Promise<WebPushSubscription | null> {
|
||||||
|
try {
|
||||||
|
const response = await api.get('/api/v1/push/subscription');
|
||||||
|
const data = await response.json();
|
||||||
|
return webPushSubscriptionSchema.parse(data);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof HTTPError && e.response.status === 404) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!vapidKey) {
|
/** Publish a new subscription to the backend. */
|
||||||
console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.');
|
async function sendSubscriptionToBackend(api: MastodonClient, subscription: PushSubscription): Promise<WebPushSubscription> {
|
||||||
return;
|
const params = {
|
||||||
}
|
subscription: subscription.toJSON(),
|
||||||
|
|
||||||
getRegistration()
|
|
||||||
.then(getPushSubscription)
|
|
||||||
// @ts-ignore
|
|
||||||
.then(({ registration, subscription }: {
|
|
||||||
registration: ServiceWorkerRegistration;
|
|
||||||
subscription: PushSubscription | null;
|
|
||||||
}) => {
|
|
||||||
if (subscription !== null) {
|
|
||||||
// We have a subscription, check if it is still valid
|
|
||||||
const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey!)).toString();
|
|
||||||
const subscriptionServerKey = urlBase64ToUint8Array(vapidKey).toString();
|
|
||||||
const serverEndpoint = getState().push_notifications.subscription?.endpoint;
|
|
||||||
|
|
||||||
// If the VAPID public key did not change and the endpoint corresponds
|
|
||||||
// to the endpoint saved in the backend, the subscription is valid
|
|
||||||
if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) {
|
|
||||||
return { subscription };
|
|
||||||
} else {
|
|
||||||
// Something went wrong, try to subscribe again
|
|
||||||
return unsubscribe({ registration, subscription }).then((registration: ServiceWorkerRegistration) => {
|
|
||||||
return subscribe(registration, getState);
|
|
||||||
}).then(
|
|
||||||
(subscription: PushSubscription) => dispatch(sendSubscriptionToBackend(subscription, me) as any));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No subscription, try to subscribe
|
|
||||||
return subscribe(registration, getState)
|
|
||||||
.then(subscription => dispatch(sendSubscriptionToBackend(subscription, me) as any));
|
|
||||||
})
|
|
||||||
.then(({ subscription }: { subscription: PushSubscription | Record<string, any> }) => {
|
|
||||||
// If we got a PushSubscription (and not a subscription object from the backend)
|
|
||||||
// it means that the backend subscription is valid (and was set during hydration)
|
|
||||||
if (!(subscription instanceof PushSubscription)) {
|
|
||||||
dispatch(setSubscription(subscription as PushSubscription));
|
|
||||||
if (me) {
|
|
||||||
pushNotificationsSetting.set(me, { alerts: subscription.alerts });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
if (error.code === 20 && error.name === 'AbortError') {
|
|
||||||
console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.');
|
|
||||||
} else if (error.code === 5 && error.name === 'InvalidCharacterError') {
|
|
||||||
console.error('The VAPID public key seems to be invalid:', vapidKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear alerts and hide UI settings
|
|
||||||
dispatch(clearSubscription());
|
|
||||||
|
|
||||||
if (me) {
|
|
||||||
pushNotificationsSetting.remove(me);
|
|
||||||
}
|
|
||||||
|
|
||||||
return getRegistration()
|
|
||||||
.then(getPushSubscription)
|
|
||||||
.then(unsubscribe);
|
|
||||||
})
|
|
||||||
.catch(console.warn);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveSettings = () =>
|
const response = await api.post('/api/v1/push/subscription', params);
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
const data = await response.json();
|
||||||
const state = getState().push_notifications;
|
|
||||||
const alerts = state.alerts;
|
|
||||||
const data = { alerts };
|
|
||||||
const me = getState().me;
|
|
||||||
|
|
||||||
return dispatch(updatePushSubscription({ data })).then(() => {
|
return webPushSubscriptionSchema.parse(data);
|
||||||
if (me) {
|
}
|
||||||
pushNotificationsSetting.set(me, data);
|
|
||||||
}
|
|
||||||
}).catch(console.warn);
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
/** Check if the VAPID key and endpoint of the subscription match the data in the backend. */
|
||||||
register,
|
function subscriptionMatchesBackend(subscription: PushSubscription, backend: WebPushSubscription): boolean {
|
||||||
saveSettings,
|
const { applicationServerKey } = subscription.options;
|
||||||
};
|
|
||||||
|
if (subscription.endpoint !== backend.endpoint) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!applicationServerKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backendKeyBytes: Uint8Array = decodeBase64Url(backend.server_key);
|
||||||
|
const subscriptionKeyBytes: Uint8Array = new Uint8Array(applicationServerKey);
|
||||||
|
|
||||||
|
return backendKeyBytes.toString() === subscriptionKeyBytes.toString();
|
||||||
|
}
|
|
@ -1,39 +0,0 @@
|
||||||
import type { AnyAction } from 'redux';
|
|
||||||
|
|
||||||
const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT';
|
|
||||||
const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
|
|
||||||
const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION';
|
|
||||||
const SET_ALERTS = 'PUSH_NOTIFICATIONS_SET_ALERTS';
|
|
||||||
|
|
||||||
const setBrowserSupport = (value: boolean) => ({
|
|
||||||
type: SET_BROWSER_SUPPORT,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
|
|
||||||
const setSubscription = (subscription: PushSubscription) => ({
|
|
||||||
type: SET_SUBSCRIPTION,
|
|
||||||
subscription,
|
|
||||||
});
|
|
||||||
|
|
||||||
const clearSubscription = () => ({
|
|
||||||
type: CLEAR_SUBSCRIPTION,
|
|
||||||
});
|
|
||||||
|
|
||||||
const setAlerts = (path: Array<string>, value: any) =>
|
|
||||||
(dispatch: React.Dispatch<AnyAction>) =>
|
|
||||||
dispatch({
|
|
||||||
type: SET_ALERTS,
|
|
||||||
path,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
|
|
||||||
export {
|
|
||||||
SET_BROWSER_SUPPORT,
|
|
||||||
SET_SUBSCRIPTION,
|
|
||||||
CLEAR_SUBSCRIPTION,
|
|
||||||
SET_ALERTS,
|
|
||||||
setBrowserSupport,
|
|
||||||
setSubscription,
|
|
||||||
clearSubscription,
|
|
||||||
setAlerts,
|
|
||||||
};
|
|
|
@ -1,86 +0,0 @@
|
||||||
import api from '../api';
|
|
||||||
|
|
||||||
const PUSH_SUBSCRIPTION_CREATE_REQUEST = 'PUSH_SUBSCRIPTION_CREATE_REQUEST';
|
|
||||||
const PUSH_SUBSCRIPTION_CREATE_SUCCESS = 'PUSH_SUBSCRIPTION_CREATE_SUCCESS';
|
|
||||||
const PUSH_SUBSCRIPTION_CREATE_FAIL = 'PUSH_SUBSCRIPTION_CREATE_FAIL';
|
|
||||||
|
|
||||||
const PUSH_SUBSCRIPTION_FETCH_REQUEST = 'PUSH_SUBSCRIPTION_FETCH_REQUEST';
|
|
||||||
const PUSH_SUBSCRIPTION_FETCH_SUCCESS = 'PUSH_SUBSCRIPTION_FETCH_SUCCESS';
|
|
||||||
const PUSH_SUBSCRIPTION_FETCH_FAIL = 'PUSH_SUBSCRIPTION_FETCH_FAIL';
|
|
||||||
|
|
||||||
const PUSH_SUBSCRIPTION_UPDATE_REQUEST = 'PUSH_SUBSCRIPTION_UPDATE_REQUEST';
|
|
||||||
const PUSH_SUBSCRIPTION_UPDATE_SUCCESS = 'PUSH_SUBSCRIPTION_UPDATE_SUCCESS';
|
|
||||||
const PUSH_SUBSCRIPTION_UPDATE_FAIL = 'PUSH_SUBSCRIPTION_UPDATE_FAIL';
|
|
||||||
|
|
||||||
const PUSH_SUBSCRIPTION_DELETE_REQUEST = 'PUSH_SUBSCRIPTION_DELETE_REQUEST';
|
|
||||||
const PUSH_SUBSCRIPTION_DELETE_SUCCESS = 'PUSH_SUBSCRIPTION_DELETE_SUCCESS';
|
|
||||||
const PUSH_SUBSCRIPTION_DELETE_FAIL = 'PUSH_SUBSCRIPTION_DELETE_FAIL';
|
|
||||||
|
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
|
||||||
|
|
||||||
interface CreatePushSubscriptionParams {
|
|
||||||
subscription: PushSubscriptionJSON;
|
|
||||||
data?: {
|
|
||||||
alerts?: Record<string, boolean>;
|
|
||||||
policy?: 'all' | 'followed' | 'follower' | 'none';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const createPushSubscription = (params: CreatePushSubscriptionParams) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
dispatch({ type: PUSH_SUBSCRIPTION_CREATE_REQUEST, params });
|
|
||||||
return api(getState).post('/api/v1/push/subscription', params).then(({ data: subscription }) =>
|
|
||||||
dispatch({ type: PUSH_SUBSCRIPTION_CREATE_SUCCESS, params, subscription }),
|
|
||||||
).catch(error =>
|
|
||||||
dispatch({ type: PUSH_SUBSCRIPTION_CREATE_FAIL, params, error }),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchPushSubscription = () =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
dispatch({ type: PUSH_SUBSCRIPTION_FETCH_REQUEST });
|
|
||||||
return api(getState).get('/api/v1/push/subscription').then(({ data: subscription }) =>
|
|
||||||
dispatch({ type: PUSH_SUBSCRIPTION_FETCH_SUCCESS, subscription }),
|
|
||||||
).catch(error =>
|
|
||||||
dispatch({ type: PUSH_SUBSCRIPTION_FETCH_FAIL, error }),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatePushSubscription = (params: Record<string, any>) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
dispatch({ type: PUSH_SUBSCRIPTION_UPDATE_REQUEST, params });
|
|
||||||
return api(getState).put('/api/v1/push/subscription', params).then(({ data: subscription }) =>
|
|
||||||
dispatch({ type: PUSH_SUBSCRIPTION_UPDATE_SUCCESS, params, subscription }),
|
|
||||||
).catch(error =>
|
|
||||||
dispatch({ type: PUSH_SUBSCRIPTION_UPDATE_FAIL, params, error }),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deletePushSubscription = () =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
dispatch({ type: PUSH_SUBSCRIPTION_DELETE_REQUEST });
|
|
||||||
return api(getState).delete('/api/v1/push/subscription').then(() =>
|
|
||||||
dispatch({ type: PUSH_SUBSCRIPTION_DELETE_SUCCESS }),
|
|
||||||
).catch(error =>
|
|
||||||
dispatch({ type: PUSH_SUBSCRIPTION_DELETE_FAIL, error }),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
PUSH_SUBSCRIPTION_CREATE_REQUEST,
|
|
||||||
PUSH_SUBSCRIPTION_CREATE_SUCCESS,
|
|
||||||
PUSH_SUBSCRIPTION_CREATE_FAIL,
|
|
||||||
PUSH_SUBSCRIPTION_FETCH_REQUEST,
|
|
||||||
PUSH_SUBSCRIPTION_FETCH_SUCCESS,
|
|
||||||
PUSH_SUBSCRIPTION_FETCH_FAIL,
|
|
||||||
PUSH_SUBSCRIPTION_UPDATE_REQUEST,
|
|
||||||
PUSH_SUBSCRIPTION_UPDATE_SUCCESS,
|
|
||||||
PUSH_SUBSCRIPTION_UPDATE_FAIL,
|
|
||||||
PUSH_SUBSCRIPTION_DELETE_REQUEST,
|
|
||||||
PUSH_SUBSCRIPTION_DELETE_SUCCESS,
|
|
||||||
PUSH_SUBSCRIPTION_DELETE_FAIL,
|
|
||||||
createPushSubscription,
|
|
||||||
fetchPushSubscription,
|
|
||||||
updatePushSubscription,
|
|
||||||
deletePushSubscription,
|
|
||||||
};
|
|
|
@ -4,7 +4,7 @@ import { shouldHaveCard } from 'soapbox/utils/status';
|
||||||
|
|
||||||
import api, { getNextLink } from '../api';
|
import api, { getNextLink } from '../api';
|
||||||
|
|
||||||
import { setComposeToStatus } from './compose';
|
import { setComposeToStatus } from './compose-status';
|
||||||
import { fetchGroupRelationships } from './groups';
|
import { fetchGroupRelationships } from './groups';
|
||||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||||
import { openModal } from './modals';
|
import { openModal } from './modals';
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
import { APIEntity } from 'soapbox/types/entities';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
|
||||||
import api from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
import { importFetchedStatuses } from './importer';
|
import { importFetchedStatuses } from './importer';
|
||||||
|
|
||||||
|
@ -9,6 +10,8 @@ import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
const TRENDING_STATUSES_FETCH_REQUEST = 'TRENDING_STATUSES_FETCH_REQUEST';
|
const TRENDING_STATUSES_FETCH_REQUEST = 'TRENDING_STATUSES_FETCH_REQUEST';
|
||||||
const TRENDING_STATUSES_FETCH_SUCCESS = 'TRENDING_STATUSES_FETCH_SUCCESS';
|
const TRENDING_STATUSES_FETCH_SUCCESS = 'TRENDING_STATUSES_FETCH_SUCCESS';
|
||||||
const TRENDING_STATUSES_FETCH_FAIL = 'TRENDING_STATUSES_FETCH_FAIL';
|
const TRENDING_STATUSES_FETCH_FAIL = 'TRENDING_STATUSES_FETCH_FAIL';
|
||||||
|
const TRENDING_STATUSES_EXPAND_FAIL = 'TRENDING_STATUSES_EXPAND_FAIL';
|
||||||
|
const TRENDING_STATUSES_EXPAND_SUCCESS = 'TRENDING_STATUSES_EXPAND_SUCCESS';
|
||||||
|
|
||||||
const fetchTrendingStatuses = () =>
|
const fetchTrendingStatuses = () =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
@ -20,18 +23,62 @@ const fetchTrendingStatuses = () =>
|
||||||
if (!features.trendingStatuses) return;
|
if (!features.trendingStatuses) return;
|
||||||
|
|
||||||
dispatch({ type: TRENDING_STATUSES_FETCH_REQUEST });
|
dispatch({ type: TRENDING_STATUSES_FETCH_REQUEST });
|
||||||
return api(getState).get('/api/v1/trends/statuses').then(({ data: statuses }) => {
|
return api(getState).get('/api/v1/trends/statuses').then((response) => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
const statuses = response.data;
|
||||||
|
|
||||||
dispatch(importFetchedStatuses(statuses));
|
dispatch(importFetchedStatuses(statuses));
|
||||||
dispatch({ type: TRENDING_STATUSES_FETCH_SUCCESS, statuses });
|
dispatch(fetchTrendingStatusesSuccess(statuses, next ? next.uri : null));
|
||||||
return statuses;
|
return statuses;
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch({ type: TRENDING_STATUSES_FETCH_FAIL, error });
|
dispatch(fetchTrendingStatusesFail(error));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchTrendingStatusesSuccess = (statuses: APIEntity[], next: string | null) => ({
|
||||||
|
type: TRENDING_STATUSES_FETCH_SUCCESS,
|
||||||
|
statuses,
|
||||||
|
next,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const fetchTrendingStatusesFail = (error: unknown) => ({
|
||||||
|
type: TRENDING_STATUSES_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandTrendingStatuses = (path: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
api(getState).get(path).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
const statuses = response.data;
|
||||||
|
|
||||||
|
dispatch(importFetchedStatuses(statuses));
|
||||||
|
dispatch(expandTrendingStatusesSuccess(statuses, next ? next.uri : null));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(expandTrendingStatusesFail(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandTrendingStatusesSuccess = (statuses: APIEntity[], next: string | null) => ({
|
||||||
|
type: TRENDING_STATUSES_EXPAND_SUCCESS,
|
||||||
|
statuses,
|
||||||
|
next,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandTrendingStatusesFail = (error: unknown) => ({
|
||||||
|
type: TRENDING_STATUSES_EXPAND_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
TRENDING_STATUSES_FETCH_REQUEST,
|
TRENDING_STATUSES_FETCH_REQUEST,
|
||||||
TRENDING_STATUSES_FETCH_SUCCESS,
|
TRENDING_STATUSES_FETCH_SUCCESS,
|
||||||
TRENDING_STATUSES_FETCH_FAIL,
|
TRENDING_STATUSES_FETCH_FAIL,
|
||||||
|
TRENDING_STATUSES_EXPAND_SUCCESS,
|
||||||
|
TRENDING_STATUSES_EXPAND_FAIL,
|
||||||
fetchTrendingStatuses,
|
fetchTrendingStatuses,
|
||||||
|
expandTrendingStatuses,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { HTTPError } from './HTTPError';
|
import { HTTPError } from './HTTPError';
|
||||||
|
|
||||||
interface Opts {
|
interface Opts {
|
||||||
searchParams?: Record<string, string | number | boolean>;
|
searchParams?: URLSearchParams | Record<string, string | number | boolean>;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
}
|
}
|
||||||
|
@ -51,9 +51,11 @@ export class MastodonClient {
|
||||||
const url = new URL(path, this.baseUrl);
|
const url = new URL(path, this.baseUrl);
|
||||||
|
|
||||||
if (opts.searchParams) {
|
if (opts.searchParams) {
|
||||||
const params = Object
|
const params = opts.searchParams instanceof URLSearchParams
|
||||||
.entries(opts.searchParams)
|
? opts.searchParams
|
||||||
.map(([key, value]) => ([key, String(value)]));
|
: Object
|
||||||
|
.entries(opts.searchParams)
|
||||||
|
.map(([key, value]) => ([key, String(value)]));
|
||||||
|
|
||||||
url.search = new URLSearchParams(params).toString();
|
url.search = new URLSearchParams(params).toString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ function useAccount(accountId?: string, opts: UseAccountOpts = {}) {
|
||||||
isLoading: isRelationshipLoading,
|
isLoading: isRelationshipLoading,
|
||||||
} = useRelationship(accountId, { enabled: withRelationship });
|
} = useRelationship(accountId, { enabled: withRelationship });
|
||||||
|
|
||||||
const isBlocked = entity?.relationship?.blocked_by === true;
|
const isBlocked = relationship?.blocked_by === true;
|
||||||
const isUnavailable = (me === entity?.id) ? false : (isBlocked && !features.blockersVisible);
|
const isUnavailable = (me === entity?.id) ? false : (isBlocked && !features.blockersVisible);
|
||||||
|
|
||||||
const account = useMemo(
|
const account = useMemo(
|
||||||
|
|
|
@ -31,7 +31,7 @@ function useAccountLookup(acct: string | undefined, opts: UseAccountLookupOpts =
|
||||||
isLoading: isRelationshipLoading,
|
isLoading: isRelationshipLoading,
|
||||||
} = useRelationship(account?.id, { enabled: withRelationship });
|
} = useRelationship(account?.id, { enabled: withRelationship });
|
||||||
|
|
||||||
const isBlocked = account?.relationship?.blocked_by === true;
|
const isBlocked = relationship?.blocked_by === true;
|
||||||
const isUnavailable = (me === account?.id) ? false : (isBlocked && !features.blockersVisible);
|
const isUnavailable = (me === account?.id) ? false : (isBlocked && !features.blockersVisible);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi } from 'soapbox/hooks';
|
||||||
|
import { adminAccountSchema } from 'soapbox/schemas/admin-account';
|
||||||
|
|
||||||
|
type Filter = 'local' | 'remote' | 'active' | 'pending' | 'disabled' | 'silenced' | 'suspended' | 'sensitized';
|
||||||
|
|
||||||
|
/** https://docs.joinmastodon.org/methods/admin/accounts/#v1 */
|
||||||
|
export function useAdminAccounts(filters: Filter[] = [], limit?: number) {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
|
for (const filter of filters) {
|
||||||
|
searchParams.append(filter, 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof limit === 'number') {
|
||||||
|
searchParams.append('limit', limit.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entities, ...rest } = useEntities(
|
||||||
|
[Entities.ACCOUNTS, searchParams.toString()],
|
||||||
|
() => api.get('/api/v1/admin/accounts', { searchParams }),
|
||||||
|
{ schema: adminAccountSchema.transform(({ account }) => account) },
|
||||||
|
);
|
||||||
|
|
||||||
|
return { accounts: entities, ...rest };
|
||||||
|
}
|
|
@ -1,11 +1,14 @@
|
||||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { useApi } from 'soapbox/hooks';
|
import { useApi } from 'soapbox/hooks';
|
||||||
import { InstanceV1, instanceV1Schema } from 'soapbox/schemas/instance';
|
import { InstanceV1, instanceV1Schema } from 'soapbox/schemas/instance';
|
||||||
|
|
||||||
interface Opts extends Pick<UseQueryOptions<unknown>, 'enabled' | 'retryOnMount' | 'staleTime'> {
|
interface Opts {
|
||||||
/** The base URL of the instance. */
|
/** The base URL of the instance. */
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
retryOnMount?: boolean;
|
||||||
|
staleTime?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get the Instance for the current backend. */
|
/** Get the Instance for the current backend. */
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { useApi } from 'soapbox/hooks';
|
import { useApi } from 'soapbox/hooks';
|
||||||
import { InstanceV2, instanceV2Schema } from 'soapbox/schemas/instance';
|
import { InstanceV2, instanceV2Schema } from 'soapbox/schemas/instance';
|
||||||
|
|
||||||
interface Opts extends Pick<UseQueryOptions<unknown>, 'enabled' | 'retryOnMount' | 'staleTime'> {
|
interface Opts {
|
||||||
/** The base URL of the instance. */
|
/** The base URL of the instance. */
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
retryOnMount?: boolean;
|
||||||
|
staleTime?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get the Instance for the current backend. */
|
/** Get the Instance for the current backend. */
|
||||||
|
|
|
@ -18,9 +18,9 @@ interface SplitValue {
|
||||||
*
|
*
|
||||||
* @param {StatusEntity | undefined} status - The current status entity.
|
* @param {StatusEntity | undefined} status - The current status entity.
|
||||||
* @param {AccountEntity} account - The account for which the zap split calculation is done.
|
* @param {AccountEntity} account - The account for which the zap split calculation is done.
|
||||||
*
|
*
|
||||||
* @returns {Object} An object containing the zap split arrays, zap split data, and a function to calculate the received amount.
|
* @returns {Object} An object containing the zap split arrays, zap split data, and a function to calculate the received amount.
|
||||||
*
|
*
|
||||||
* @property {ZapSplitData[]} zapArrays - Array of zap split data returned from the API.
|
* @property {ZapSplitData[]} zapArrays - Array of zap split data returned from the API.
|
||||||
* @property {Object} zapSplitData - Contains the total split amount, amount to receive, and individual split values.
|
* @property {Object} zapSplitData - Contains the total split amount, amount to receive, and individual split values.
|
||||||
* @property {Function} receiveAmount - A function to calculate the zap amount based on the split configuration.
|
* @property {Function} receiveAmount - A function to calculate the zap amount based on the split configuration.
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
Creative Commons Legal Code
|
||||||
|
|
||||||
|
CC0 1.0 Universal
|
||||||
|
|
||||||
|
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||||
|
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||||
|
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||||
|
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||||
|
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||||
|
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||||
|
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||||
|
HEREUNDER.
|
||||||
|
|
||||||
|
Statement of Purpose
|
||||||
|
|
||||||
|
The laws of most jurisdictions throughout the world automatically confer
|
||||||
|
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||||
|
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||||
|
authorship and/or a database (each, a "Work").
|
||||||
|
|
||||||
|
Certain owners wish to permanently relinquish those rights to a Work for
|
||||||
|
the purpose of contributing to a commons of creative, cultural and
|
||||||
|
scientific works ("Commons") that the public can reliably and without fear
|
||||||
|
of later claims of infringement build upon, modify, incorporate in other
|
||||||
|
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||||
|
and for any purposes, including without limitation commercial purposes.
|
||||||
|
These owners may contribute to the Commons to promote the ideal of a free
|
||||||
|
culture and the further production of creative, cultural and scientific
|
||||||
|
works, or to gain reputation or greater distribution for their Work in
|
||||||
|
part through the use and efforts of others.
|
||||||
|
|
||||||
|
For these and/or other purposes and motivations, and without any
|
||||||
|
expectation of additional consideration or compensation, the person
|
||||||
|
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||||
|
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||||
|
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||||
|
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||||
|
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||||
|
|
||||||
|
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||||
|
protected by copyright and related or neighboring rights ("Copyright and
|
||||||
|
Related Rights"). Copyright and Related Rights include, but are not
|
||||||
|
limited to, the following:
|
||||||
|
|
||||||
|
i. the right to reproduce, adapt, distribute, perform, display,
|
||||||
|
communicate, and translate a Work;
|
||||||
|
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||||
|
iii. publicity and privacy rights pertaining to a person's image or
|
||||||
|
likeness depicted in a Work;
|
||||||
|
iv. rights protecting against unfair competition in regards to a Work,
|
||||||
|
subject to the limitations in paragraph 4(a), below;
|
||||||
|
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||||
|
in a Work;
|
||||||
|
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||||
|
European Parliament and of the Council of 11 March 1996 on the legal
|
||||||
|
protection of databases, and under any national implementation
|
||||||
|
thereof, including any amended or successor version of such
|
||||||
|
directive); and
|
||||||
|
vii. other similar, equivalent or corresponding rights throughout the
|
||||||
|
world based on applicable law or treaty, and any national
|
||||||
|
implementations thereof.
|
||||||
|
|
||||||
|
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||||
|
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||||
|
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||||
|
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||||
|
of action, whether now known or unknown (including existing as well as
|
||||||
|
future claims and causes of action), in the Work (i) in all territories
|
||||||
|
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||||
|
treaty (including future time extensions), (iii) in any current or future
|
||||||
|
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||||
|
including without limitation commercial, advertising or promotional
|
||||||
|
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||||
|
member of the public at large and to the detriment of Affirmer's heirs and
|
||||||
|
successors, fully intending that such Waiver shall not be subject to
|
||||||
|
revocation, rescission, cancellation, termination, or any other legal or
|
||||||
|
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||||
|
as contemplated by Affirmer's express Statement of Purpose.
|
||||||
|
|
||||||
|
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||||
|
be judged legally invalid or ineffective under applicable law, then the
|
||||||
|
Waiver shall be preserved to the maximum extent permitted taking into
|
||||||
|
account Affirmer's express Statement of Purpose. In addition, to the
|
||||||
|
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||||
|
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||||
|
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||||
|
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||||
|
maximum duration provided by applicable law or treaty (including future
|
||||||
|
time extensions), (iii) in any current or future medium and for any number
|
||||||
|
of copies, and (iv) for any purpose whatsoever, including without
|
||||||
|
limitation commercial, advertising or promotional purposes (the
|
||||||
|
"License"). The License shall be deemed effective as of the date CC0 was
|
||||||
|
applied by Affirmer to the Work. Should any part of the License for any
|
||||||
|
reason be judged legally invalid or ineffective under applicable law, such
|
||||||
|
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||||
|
of the License, and in such case Affirmer hereby affirms that he or she
|
||||||
|
will not (i) exercise any of his or her remaining Copyright and Related
|
||||||
|
Rights in the Work or (ii) assert any associated claims and causes of
|
||||||
|
action with respect to the Work, in either case contrary to Affirmer's
|
||||||
|
express Statement of Purpose.
|
||||||
|
|
||||||
|
4. Limitations and Disclaimers.
|
||||||
|
|
||||||
|
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||||
|
surrendered, licensed or otherwise affected by this document.
|
||||||
|
b. Affirmer offers the Work as-is and makes no representations or
|
||||||
|
warranties of any kind concerning the Work, express, implied,
|
||||||
|
statutory or otherwise, including without limitation warranties of
|
||||||
|
title, merchantability, fitness for a particular purpose, non
|
||||||
|
infringement, or the absence of latent or other defects, accuracy, or
|
||||||
|
the present or absence of errors, whether or not discoverable, all to
|
||||||
|
the greatest extent permissible under applicable law.
|
||||||
|
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||||
|
that may apply to the Work or any use thereof, including without
|
||||||
|
limitation any person's Copyright and Related Rights in the Work.
|
||||||
|
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||||
|
consents, permissions or other rights required for any use of the
|
||||||
|
Work.
|
||||||
|
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||||
|
party to this document and has no duty or obligation with respect to
|
||||||
|
this CC0 or use of the Work.
|
|
@ -0,0 +1 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><g fill="none"><circle fill="#2EBAC6" cx="16" cy="16" r="16"/><path d="M22.934 21.574l-5.35-13.532C17.28 7.342 16.834 7 16.243 7h-.473c-.592 0-1.039.343-1.341 1.042l-2.327 5.896h-1.761c-.528.002-.956.448-.96 1v.014c.004.553.432.999.96 1.001h.946l-2.221 5.621a1.235 1.235 0 00-.066.384c0 .315.092.562.263.754.17.192.407.288.71.288a.933.933 0 00.552-.192c.17-.123.289-.302.38-.507l2.446-6.348h1.696c.527-.002.955-.449.96-1.001v-.027c-.005-.553-.433-1-.96-1.001h-.907l1.866-4.867L21.093 22.3c.092.205.21.384.381.507.161.122.354.19.553.192.302 0 .539-.096.71-.288.17-.192.262-.439.262-.754a.944.944 0 00-.065-.384z" fill="#FFF"/></g></svg>
|
After Width: | Height: | Size: 718 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><filter x="-7.5%" y="-6%" width="115.5%" height="117.2%" filterUnits="objectBoundingBox" id="a"><feOffset dy=".5" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation=".5" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.204257246 0" in="shadowBlurOuter1"/></filter><path d="M15.725 6.06c.479-.247 1.064.324.81.795-.149.384-.71.486-.996.193-.303-.28-.204-.836.186-.989zm-5.155.546c.291-.118.66.144.63.457.03.338-.39.588-.687.427-.393-.15-.348-.778.057-.884zm10.558.893c-.455-.054-.527-.758-.09-.9.34-.162.652.143.702.46-.072.27-.302.518-.612.44zm-9.385 1.265c.487-.303 1.181.148 1.106.705-.025.561-.783.887-1.211.507-.414-.298-.351-.982.105-1.212zm7.43.322c.217-.55 1.097-.568 1.344-.032.245.417-.056.934-.491 1.076-.577.106-1.124-.508-.853-1.044zm-4.069 1.013c-.005-.474.433-.826.89-.859.304.06.634.187.764.488.243.416.027.987-.41 1.178-.2.11-.438.069-.656.056-.333-.16-.614-.477-.588-.863zm-7.666.69c.445-.27 1.045.22.876.696-.092.411-.654.578-.975.316-.343-.246-.289-.837.1-1.013zm16.462-.002c.377-.288 1 .043.954.511.026.427-.513.75-.887.53-.412-.183-.455-.807-.067-1.04zm-6.64.851c.622-.22 1.362.043 1.716.59.468.667.22 1.683-.507 2.066-.752.453-1.851.07-2.13-.758-.315-.74.145-1.666.92-1.898zm-3.653.073c.69-.32 1.619-.052 1.952.642.392.676.089 1.617-.612 1.966-.702.393-1.693.095-2.032-.63-.381-.702-.043-1.655.692-1.978zM9.95 12.94c.053-.437.472-.722.895-.752a.98.98 0 01.87.857c-.03.45-.383.888-.867.886-.533.045-1-.477-.898-.991zm10.802-.656c.547-.313 1.306.142 1.282.76.037.655-.803 1.116-1.347.732-.566-.32-.522-1.22.065-1.492zm-8.63 2.307c.638-.173 1.37.123 1.683.701.343.582.203 1.39-.33 1.818-.685.626-1.946.374-2.31-.48-.419-.783.09-1.833.956-2.04zm6.927-.003c.621-.175 1.351.06 1.685.617.442.637.231 1.588-.426 1.998-.69.477-1.756.227-2.136-.519-.46-.771.003-1.861.877-2.096zm-11.04.726c.552-.205 1.164.394.94.933-.136.49-.839.672-1.202.31-.425-.34-.268-1.095.262-1.243zm14.969.782a.836.836 0 01.788-.874c.378.06.746.36.716.765.035.535-.62.898-1.084.647-.217-.109-.328-.328-.42-.538zM5.294 15.58c.332-.143.743.14.667.503-.018.411-.635.57-.861.226-.2-.239-.08-.606.194-.73zm20.949-.009c.234-.163.61-.046.702.223.157.294-.131.696-.467.647-.472.042-.624-.665-.235-.87zm-12.317 1.973c.874-.223 1.814.494 1.82 1.38.056.895-.87 1.688-1.764 1.482-.692-.11-1.235-.766-1.212-1.453-.002-.658.502-1.27 1.156-1.409zm3.462-.001c.887-.244 1.855.486 1.841 1.392.047.878-.85 1.645-1.726 1.47-.825-.104-1.433-.995-1.203-1.783.116-.524.562-.95 1.088-1.08zm-6.676.545c.614-.103 1.19.57.941 1.144-.182.612-1.086.777-1.486.278-.468-.48-.118-1.356.545-1.422zm10.154.027c.548-.226 1.22.24 1.178.825.022.643-.808 1.087-1.343.711-.607-.337-.496-1.33.165-1.536zm2.838 2.8c-.214-.393.175-.914.62-.841.22-.004.375.167.516.311.029.233.078.511-.119.69-.267.333-.872.238-1.017-.16zm-16.268-.732c.415-.271 1.012.134.918.61-.05.423-.59.664-.945.424-.382-.217-.368-.836.027-1.034zm8.193.883c.543-.235 1.235.23 1.183.818.04.65-.815 1.1-1.346.71-.59-.335-.491-1.321.163-1.528zm-3.794.871c.462-.239 1.082.174 1.04.684.014.418-.4.774-.82.712-.347-.007-.573-.314-.685-.605.006-.317.139-.67.465-.79zm7.686.008c.476-.29 1.152.126 1.107.67.012.57-.752.934-1.195.56-.428-.293-.376-.997.088-1.23zm1.337 3.25c-.212-.314.037-.693.38-.765.277.055.57.26.511.574-.04.427-.674.557-.891.192zm-10.611-.273c.084-.25.288-.497.587-.432.435.03.564.676.183.875-.342.227-.74-.084-.77-.443zm5.12.287c.083-.37.568-.549.888-.353.212.09.274.322.328.52a8.822 8.822 0 00-.08.31c-.131.152-.3.305-.518.3-.405.047-.771-.404-.619-.777z" id="b"/></defs><g fill="none"><circle cx="16" cy="16" r="16" fill="#0D1E30"/><use fill="#000" filter="url(#a)" xlink:href="#b"/><use fill="#FFF" xlink:href="#b"/></g></svg>
|
After Width: | Height: | Size: 3.8 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><g fill="none"><circle fill="#000" cx="16" cy="16" r="16"/><path d="M10.25 22.916l2.303-3.986 2.301-3.972 2.288-3.986.38-.632.167.632.702 2.624-.786 1.362-2.301 3.972-2.288 3.986h2.75l2.302-3.986 1.193-2.063.562 2.063 1.066 3.986h2.47l-1.066-3.986-1.067-3.972-.28-1.025 1.712-2.961H20.16l-.085-.295-.87-3.256L19.093 7h-2.4l-.056.084-2.246 3.888-2.302 3.986-2.287 3.972L7.5 22.916z" fill="#FFF"/></g></svg>
|
After Width: | Height: | Size: 488 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><g fill="none"><circle cx="16" cy="16" r="16" fill="#2E3148"/><g transform="translate(6 5)" fill="#FFF"><path d="M10.02.53c-1.295 0-2.345 4.697-2.345 10.49s1.05 10.49 2.345 10.49c1.294 0 2.344-4.697 2.344-10.49S11.314.53 10.02.53zm.162 20.387c-.148.198-.297.05-.297.05-.596-.692-.894-1.975-.894-1.975-1.043-3.357-.795-10.564-.795-10.564.49-5.721 1.382-7.073 1.685-7.373a.185.185 0 01.238-.019c.44.313.81 1.617.81 1.617 1.09 4.048.991 7.848.991 7.848.099 3.308-.546 7.01-.546 7.01-.497 2.814-1.192 3.406-1.192 3.406z"/><path d="M19.118 5.8c-.645-1.124-5.24.303-10.267 3.186-5.027 2.883-8.573 6.13-7.93 7.254.645 1.124 5.241-.303 10.268-3.186 5.027-2.883 8.574-6.131 7.93-7.254zM1.515 16.085c-.246-.03-.19-.234-.19-.234.302-.86 1.266-1.758 1.266-1.758 2.393-2.575 8.769-5.946 8.769-5.946 5.206-2.422 6.823-2.32 7.233-2.208a.185.185 0 01.135.198c-.05.537-1 1.507-1 1.507-2.966 2.961-6.312 4.768-6.312 4.768-2.82 1.732-6.353 3.013-6.353 3.013-2.688.968-3.548.66-3.548.66z"/><path d="M19.095 16.277c.65-1.12-2.887-4.383-7.898-7.288C6.187 6.085 1.593 4.641.944 5.763c-.65 1.123 2.888 4.383 7.9 7.288 5.013 2.904 9.602 4.348 10.251 3.226zM1.375 6.196c-.097-.228.106-.283.106-.283.897-.17 2.157.217 2.157.217 3.427.78 9.538 4.608 9.538 4.608 4.705 3.292 5.427 4.743 5.535 5.154a.185.185 0 01-.103.215c-.49.225-1.805-.11-1.805-.11-4.05-1.086-7.289-3.075-7.289-3.075-2.91-1.57-5.788-3.985-5.788-3.985-2.187-1.842-2.35-2.74-2.35-2.74l-.002-.001z"/><circle cx="9.995" cy="10.995" r="1.234"/><circle cx="15.055" cy="6.256" r="1"/><circle cx="3.306" cy="8.774" r="1"/><circle cx="8.539" cy="17.856" r="1"/></g></g></svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle fill="#E84142" fill-rule="nonzero" cx="16" cy="16" r="16"/><path d="M11.518 22.75H8.49c-.636 0-.95 0-1.142-.123A.77.77 0 017 22.025c-.012-.226.145-.503.46-1.055l7.472-13.193c.318-.56.48-.84.682-.944a.77.77 0 01.698 0c.203.104.364.384.682.944l1.536 2.686.008.014c.343.6.517.906.593 1.226a2.26 2.26 0 010 1.066c-.076.323-.249.63-.597 1.24l-3.926 6.95-.01.017c-.346.606-.52.913-.764 1.145a2.284 2.284 0 01-.93.54c-.319.089-.675.089-1.387.089zm7.643 0h4.336c.64 0 .962 0 1.154-.126a.768.768 0 00.348-.607c.011-.219-.142-.484-.443-1.005l-.032-.054-2.172-3.722-.025-.042c-.305-.517-.46-.778-.657-.879a.762.762 0 00-.693 0c-.2.104-.36.377-.678.925l-2.165 3.722-.007.013c-.317.548-.476.821-.464 1.046a.777.777 0 00.348.606c.188.123.51.123 1.15.123z" fill="#FFF"/></g></svg>
|
After Width: | Height: | Size: 891 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" fill="#8dc351" r="16"/><path d="M21.207 10.534c-.776-1.972-2.722-2.15-4.988-1.71l-.807-2.813-1.712.491.786 2.74c-.45.128-.908.27-1.363.41l-.79-2.758-1.711.49.805 2.813c-.368.114-.73.226-1.085.328l-.003-.01-2.362.677.525 1.83s1.258-.388 1.243-.358c.694-.199 1.035.139 1.2.468l.92 3.204c.047-.013.11-.029.184-.04l-.181.052 1.287 4.49c.032.227.004.612-.48.752.027.013-1.246.356-1.246.356l.247 2.143 2.228-.64c.415-.117.825-.227 1.226-.34l.817 2.845 1.71-.49-.807-2.815a65.74 65.74 0 001.372-.38l.802 2.803 1.713-.491-.814-2.84c2.831-.991 4.638-2.294 4.113-5.07-.422-2.234-1.724-2.912-3.471-2.836.848-.79 1.213-1.858.642-3.3zm-.65 6.77c.61 2.127-3.1 2.929-4.26 3.263l-1.081-3.77c1.16-.333 4.704-1.71 5.34.508zm-2.322-5.09c.554 1.935-2.547 2.58-3.514 2.857l-.98-3.419c.966-.277 3.915-1.455 4.494.563z" fill="#fff" fill-rule="nonzero"/></g></svg>
|
After Width: | Height: | Size: 959 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#F7931A"/><path fill="#FFF" fill-rule="nonzero" d="M23.189 14.02c.314-2.096-1.283-3.223-3.465-3.975l.708-2.84-1.728-.43-.69 2.765c-.454-.114-.92-.22-1.385-.326l.695-2.783L15.596 6l-.708 2.839c-.376-.086-.746-.17-1.104-.26l.002-.009-2.384-.595-.46 1.846s1.283.294 1.256.312c.7.175.826.638.805 1.006l-.806 3.235c.048.012.11.03.18.057l-.183-.045-1.13 4.532c-.086.212-.303.531-.793.41.018.025-1.256-.313-1.256-.313l-.858 1.978 2.25.561c.418.105.828.215 1.231.318l-.715 2.872 1.727.43.708-2.84c.472.127.93.245 1.378.357l-.706 2.828 1.728.43.715-2.866c2.948.558 5.164.333 6.097-2.333.752-2.146-.037-3.385-1.588-4.192 1.13-.26 1.98-1.003 2.207-2.538zm-3.95 5.538c-.533 2.147-4.148.986-5.32.695l.95-3.805c1.172.293 4.929.872 4.37 3.11zm.535-5.569c-.487 1.953-3.495.96-4.47.717l.86-3.45c.975.243 4.118.696 3.61 2.733z"/></g></svg>
|
After Width: | Height: | Size: 953 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><g fill="none"><circle fill="#E6007A" cx="16" cy="16" r="16"/><path d="M16.272 6.625c-3.707 0-6.736 3.012-6.736 6.736 0 .749.124 1.48.356 2.192a.95.95 0 001.194.589.95.95 0 00.588-1.194 4.745 4.745 0 01-.267-1.73c.071-2.512 2.103-4.58 4.616-4.704a4.86 4.86 0 015.115 4.847 4.862 4.862 0 01-4.58 4.848s-.945.053-1.408.125c-.232.035-.41.071-.535.089-.054.018-.107-.036-.09-.09l.161-.783.873-4.028a.934.934 0 00-.712-1.105.934.934 0 00-1.105.713s-2.103 9.802-2.121 9.909a.934.934 0 00.713 1.105.934.934 0 001.105-.713c.017-.107.303-1.408.303-1.408a2.367 2.367 0 011.996-1.854 21.43 21.43 0 011.051-.089 6.744 6.744 0 006.22-6.719c0-3.724-3.03-6.736-6.737-6.736zm.481 15.505a1.122 1.122 0 00-1.336.873c-.125.606.25 1.212.873 1.337a1.122 1.122 0 001.337-.874c.124-.623-.25-1.212-.874-1.336z" fill="#FFF"/></g></svg>
|
After Width: | Height: | Size: 893 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#000" fill-rule="nonzero"/><path stroke="#FFF" stroke-linecap="round" stroke-linejoin="round" stroke-width=".64" d="M10.886 11.61L16 27.667l-7.588-4.754 2.474-11.303L16 4.624v4.9L8.412 22.913h15.183L16.007 9.524v-4.9l5.113 6.986 2.475 11.303-7.588 4.754L21.12 11.61"/></g></svg>
|
After Width: | Height: | Size: 410 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#328332"/><g fill="#FFF"><path fill-rule="nonzero" d="M15.989 16.553l-6.721-.577 6.72-3.802v4.379zm0 4.46v6.94C13.652 24.315 11.076 20.311 9 17.07c2.45 1.38 5.008 2.823 6.989 3.944zm0-10.068L9 14.845 15.989 4v6.945z"/><path fill-opacity=".601" fill-rule="nonzero" d="M22.71 15.976l-6.721.577v-4.379l6.72 3.802zm-6.721 5.038c1.98-1.12 4.537-2.564 6.988-3.944-2.076 3.242-4.652 7.246-6.988 10.882v-6.938zm0-10.069V4l6.988 10.845-6.988-3.9z"/><path opacity=".2" d="M15.989 16.553l6.72-.577-6.72 3.775z"/><path opacity=".603" d="M15.988 16.553l-6.721-.577 6.721 3.775z"/></g></g></svg>
|
After Width: | Height: | Size: 736 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#627EEA"/><g fill="#FFF" fill-rule="nonzero"><path fill-opacity=".602" d="M16.498 4v8.87l7.497 3.35z"/><path d="M16.498 4L9 16.22l7.498-3.35z"/><path fill-opacity=".602" d="M16.498 21.968v6.027L24 17.616z"/><path d="M16.498 27.995v-6.028L9 17.616z"/><path fill-opacity=".2" d="M16.498 20.573l7.497-4.353-7.497-3.348z"/><path fill-opacity=".602" d="M9 16.22l7.498 4.353v-7.701z"/></g></g></svg>
|
After Width: | Height: | Size: 525 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g fill="none"><circle cx="16" cy="16" r="16" fill="#42C1CA"/><path fill="#FFF" d="M15.932 12.908c.372-1.563.82-2.968 1.296-3.885.175-.438.885-1.487 1.664-2.18 1.297-1.155 2.752-1.448 4.267-.497l-.133.211.133-.211c.773.485 1.083.984.947 1.454-.1.35-.483.63-.688.601-.3.03-.602-.03-.89-.242a1.685 1.685 0 01-.541-.721c-.212-.5-.49-.67-.831-.63-.247.028-.534.186-.625.292l-.235.26a3.894 3.894 0 00-.484.635c-.476.793-.915 2.246-1.524 5.257l4.036.591-.222 1.617-4.096-.6-.175 1.064-.045.266c-.024.138-.05.288-.08.448l4.136.606-.237 1.615-4.233-.62c-.489 2.078-1.133 4.305-1.588 5.184-.176.439-.885 1.486-1.664 2.18-1.297 1.154-2.752 1.448-4.267.497-.773-.485-1.083-.985-.947-1.455.1-.35.483-.629.688-.6.3-.03.602.03.89.241.222.164.406.402.541.722.212.499.49.67.831.63.247-.029.534-.187.625-.293.907-1.01 1.626-2.956 2.535-7.45l-4.036-.592.222-1.617 4.096.6.176-1.063a31.19 31.19 0 01.125-.715l-4.12-.603.236-1.615 4.217.618z"/></g></svg>
|
After Width: | Height: | Size: 994 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" fill="#efb914" fill-rule="nonzero" r="16"/><path d="M21.002 9.855A7.947 7.947 0 0124 15.278l-2.847-.708a5.357 5.357 0 00-3.86-3.667c-2.866-.713-5.76.991-6.465 3.806s1.05 5.675 3.917 6.388a5.373 5.373 0 005.134-1.43l2.847.707a7.974 7.974 0 01-5.2 3.385L16.716 27l-2.596-.645.644-2.575a8.28 8.28 0 01-1.298-.323l-.643 2.575-2.596-.646.81-3.241c-2.378-1.875-3.575-4.996-2.804-8.081s3.297-5.281 6.28-5.823L15.323 5l2.596.645-.644 2.575a8.28 8.28 0 011.298.323l.643-2.575 2.596.646z" fill="#fff"/></g></svg>
|
After Width: | Height: | Size: 644 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><g fill="none"><circle fill="#292A2E" cx="16" cy="16" r="16"/><g fill="#FFF"><path d="M22.17 10.5c-1.34 0-2.803.667-4.348 1.982a19.032 19.032 0 00-1.843 1.824l.002.002.002-.002s.752.794 1.578 1.643a17.474 17.474 0 011.827-1.842c1.374-1.169 2.272-1.414 2.783-1.414 1.927 0 3.495 1.484 3.495 3.307 0 1.814-1.569 3.296-3.498 3.308-.088 0-.2-.01-.34-.04.562.235 1.166.405 1.742.405 3.533 0 4.223-2.237 4.27-2.396.105-.41.16-.837.16-1.276 0-3.034-2.615-5.501-5.83-5.501zm-12.34 11c1.34 0 2.803-.667 4.348-1.982a19.032 19.032 0 001.843-1.824l-.002-.002a.025.025 0 01-.002.002s-.752-.794-1.578-1.643a17.474 17.474 0 01-1.827 1.842c-1.374 1.169-2.272 1.414-2.783 1.414-1.927 0-3.495-1.484-3.495-3.307 0-1.814 1.569-3.296 3.498-3.308.088 0 .2.01.34.04-.562-.235-1.166-.405-1.742-.405-3.533 0-4.223 2.237-4.27 2.396-.105.41-.16.837-.16 1.276C4 19.033 6.615 21.5 9.83 21.5z"/><path d="M23.563 19.617c-1.809-.043-3.689-1.427-4.072-1.771-.991-.89-3.278-3.297-3.457-3.486-1.676-1.822-3.948-3.86-6.205-3.86h-.005c-2.744.013-5.05 1.817-5.663 4.224.047-.159.948-2.439 4.267-2.36 1.809.044 3.698 1.447 4.081 1.79.991.89 3.279 3.298 3.457 3.487 1.676 1.821 3.948 3.859 6.205 3.859h.005c2.744-.013 5.05-1.817 5.663-4.224-.046.159-.957 2.42-4.276 2.341z"/></g></g></svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><g fill="none"><circle fill="#2A5ADA" cx="16" cy="16" r="16"/><path d="M16 6l-1.799 1.055L9.3 9.945 7.5 11v10l1.799 1.055 4.947 2.89L16.045 26l1.799-1.055 4.857-2.89L24.5 21V11l-1.799-1.055-4.902-2.89L16 6zm-4.902 12.89v-5.78L16 10.22l4.902 2.89v5.78L16 21.78l-4.902-2.89z" fill="#FFF"/></g></svg>
|
After Width: | Height: | Size: 380 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#BFBBBB"/><path fill="#FFF" d="M10.427 19.214L9 19.768l.688-2.759 1.444-.58L13.213 8h5.129l-1.519 6.196 1.41-.571-.68 2.75-1.427.571-.848 3.483H23L22.127 24H9.252z"/></g></svg>
|
After Width: | Height: | Size: 331 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><g fill="none"><circle fill="#6F41D8" cx="16" cy="16" r="16"/><path d="M21.092 12.693c-.369-.215-.848-.215-1.254 0l-2.879 1.654-1.955 1.078-2.879 1.653c-.369.216-.848.216-1.254 0l-2.288-1.294c-.369-.215-.627-.61-.627-1.042V12.19c0-.431.221-.826.627-1.042l2.25-1.258c.37-.216.85-.216 1.256 0l2.25 1.258c.37.216.628.611.628 1.042v1.654l1.955-1.115v-1.653a1.16 1.16 0 00-.627-1.042l-4.17-2.372c-.369-.216-.848-.216-1.254 0l-4.244 2.372A1.16 1.16 0 006 11.076v4.78c0 .432.221.827.627 1.043l4.244 2.372c.369.215.849.215 1.254 0l2.879-1.618 1.955-1.114 2.879-1.617c.369-.216.848-.216 1.254 0l2.251 1.258c.37.215.627.61.627 1.042v2.552c0 .431-.22.826-.627 1.042l-2.25 1.294c-.37.216-.85.216-1.255 0l-2.251-1.258c-.37-.216-.628-.611-.628-1.042v-1.654l-1.955 1.115v1.653c0 .431.221.827.627 1.042l4.244 2.372c.369.216.848.216 1.254 0l4.244-2.372c.369-.215.627-.61.627-1.042v-4.78a1.16 1.16 0 00-.627-1.042l-4.28-2.409z" fill="#FFF"/></g></svg>
|
After Width: | Height: | Size: 1016 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><g fill="none"><circle fill="#66F9A1" cx="16" cy="16" r="16"/><path d="M9.925 19.687a.59.59 0 01.415-.17h14.366a.29.29 0 01.207.497l-2.838 2.815a.59.59 0 01-.415.171H7.294a.291.291 0 01-.207-.498l2.838-2.815zm0-10.517A.59.59 0 0110.34 9h14.366c.261 0 .392.314.207.498l-2.838 2.815a.59.59 0 01-.415.17H7.294a.291.291 0 01-.207-.497L9.925 9.17zm12.15 5.225a.59.59 0 00-.415-.17H7.294a.291.291 0 00-.207.498l2.838 2.815c.11.109.26.17.415.17h14.366a.291.291 0 00.207-.498l-2.838-2.815z" fill="#FFF"/></g></svg>
|
After Width: | Height: | Size: 589 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#15BDFF"/><path fill="#FFF" fill-rule="nonzero" d="M14.738 24.734L7.04 9.046a.38.38 0 01.34-.546h2.668c.143 0 .277.08.34.206l5.622 11.381c.5 1.02 1.951 1.02 2.452 0l5.604-11.372a.382.382 0 01.34-.206h.332c.197 0 .322.206.233.376l-7.78 15.85c-.501 1.02-1.951 1.02-2.453 0z"/></g></svg>
|
After Width: | Height: | Size: 416 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><g fill="none"><circle cx="16" cy="16" r="16" fill="#000"/><path d="M23.13 9.292l-2.4 1.224-11.598 5.907A6.909 6.909 0 0119.35 9.498l1.374-.7.205-.105a8.439 8.439 0 00-13.371 7.472 1.535 1.535 0 01-.834 1.484l-.725.37v1.724l2.134-1.088.691-.353.681-.347 12.226-6.23 1.374-.699 2.84-1.447V7.856L23.13 9.292zm2.816 2.012L10.201 19.32l-1.374.7L6 21.463v1.723l2.808-1.43 2.401-1.224 11.61-5.916a6.909 6.909 0 01-10.229 6.93l-.085.045-1.49.76a8.439 8.439 0 0013.372-7.475 1.536 1.536 0 01.833-1.483l.726-.37v-1.718z" fill="#FFF"/></g></svg>
|
After Width: | Height: | Size: 618 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#F60"/><path fill="#FFF" fill-rule="nonzero" d="M15.97 5.235c5.985 0 10.825 4.84 10.825 10.824a11.07 11.07 0 01-.558 3.432h-3.226v-9.094l-7.04 7.04-7.04-7.04v9.094H5.704a11.07 11.07 0 01-.557-3.432c0-5.984 4.84-10.824 10.824-10.824zM14.358 19.02L16 20.635l1.613-1.614 3.051-3.08v5.72h4.547a10.806 10.806 0 01-9.24 5.192c-3.902 0-7.334-2.082-9.24-5.192h4.546v-5.72l3.08 3.08z"/></g></svg>
|
After Width: | Height: | Size: 519 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><g fill="none"><circle cx="16" cy="16" r="16" fill="#23292F"/><path d="M23.07 8h2.89l-6.015 5.957a5.621 5.621 0 01-7.89 0L6.035 8H8.93l4.57 4.523a3.556 3.556 0 004.996 0L23.07 8zM8.895 24.563H6l6.055-5.993a5.621 5.621 0 017.89 0L26 24.562h-2.895L18.5 20a3.556 3.556 0 00-4.996 0l-4.61 4.563z" fill="#FFF"/></g></svg>
|
After Width: | Height: | Size: 399 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#A6E000"/><path fill="#FFF" d="M18.19 26c-1.46 0-2.524-.344-3.192-1.03-.667-.688-1.001-1.429-1.001-2.223 0-.29.058-.535.175-.733a1.27 1.27 0 01.477-.47c.202-.114.45-.172.745-.172s.544.058.746.172c.202.115.36.271.477.47.117.198.175.443.175.733 0 .351-.085.637-.256.859a1.184 1.184 0 01-.606.435c.202.275.52.47.955.584.435.123.87.184 1.304.184a2.93 2.93 0 001.643-.481c.489-.321.85-.795 1.082-1.42.233-.627.35-1.337.35-2.131 0-.863-.128-1.6-.384-2.211-.249-.619-.618-1.077-1.107-1.375a2.99 2.99 0 00-1.584-.446c-.372 0-.838.152-1.397.458l-1.025.504v-.504l4.612-6.048h-6.382v6.277c0 .52.116.947.35 1.283.232.336.59.504 1.07.504.373 0 .73-.122 1.072-.367a3.76 3.76 0 00.885-.893.342.342 0 01.117-.15.236.236 0 01.151-.056c.086 0 .186.042.303.125a.619.619 0 01.163.424 2.92 2.92 0 01-.058.321c-.264.58-.63 1.023-1.095 1.329a2.748 2.748 0 01-1.537.458c-1.382 0-2.337-.267-2.865-.802-.528-.534-.792-1.26-.792-2.176v-6.277H8.5V9.986h3.26V7.33l-.744-.734V6h2.166l.815.412v3.574l8.431-.023.84.825-5.172 5.086a3.51 3.51 0 01.978-.23c.56 0 1.188.176 1.887.528.707.343 1.25.817 1.63 1.42.381.596.626 1.169.734 1.719a7.16 7.16 0 01.175 1.466 6.02 6.02 0 01-.629 2.726 4.037 4.037 0 01-1.91 1.878A6.291 6.291 0 0118.19 26z"/></g></svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#ECB244"/><path fill="#FFF" fill-rule="nonzero" d="M15.096 19.846h6.297v3.35h-3.875c.064.958.097 1.847.161 2.804h-3.261v-2.77h-3.876c0-1.093-.129-2.187.065-3.213.097-.547.678-1.026 1.033-1.504a462.137 462.137 0 013.714-4.581c.485-.582.969-1.129 1.518-1.778h-6.04v-3.35h3.586V6h3.132v2.735h3.908c0 1.128.129 2.222-.065 3.248-.097.547-.678 1.026-1.065 1.504a462.138 462.138 0 01-3.714 4.581 37.083 37.083 0 01-1.518 1.778z"/></g></svg>
|
After Width: | Height: | Size: 565 B |
|
@ -77,12 +77,12 @@ const AccountSearch: React.FC<IAccountSearch> = ({ onSelected, ...rest }) => {
|
||||||
>
|
>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
src={require('@tabler/icons/outline/search.svg')}
|
src={require('@tabler/icons/outline/search.svg')}
|
||||||
className={clsx('h-4 w-4 text-gray-400', { hidden: !isEmpty() })}
|
className={clsx('size-4 text-gray-400', { hidden: !isEmpty() })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
src={require('@tabler/icons/outline/x.svg')}
|
src={require('@tabler/icons/outline/x.svg')}
|
||||||
className={clsx('h-4 w-4 text-gray-400', { hidden: isEmpty() })}
|
className={clsx('size-4 text-gray-400', { hidden: isEmpty() })}
|
||||||
aria-label={intl.formatMessage(messages.placeholder)}
|
aria-label={intl.formatMessage(messages.placeholder)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -50,7 +50,7 @@ const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className='h-4 w-4 flex-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2'
|
className='size-4 flex-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2'
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
|
@ -208,7 +208,7 @@ const Account = ({
|
||||||
<Avatar src={account.avatar} size={avatarSize} />
|
<Avatar src={account.avatar} size={avatarSize} />
|
||||||
{emoji && (
|
{emoji && (
|
||||||
<Emoji
|
<Emoji
|
||||||
className='absolute -right-1.5 bottom-0 h-5 w-5'
|
className='absolute -right-1.5 bottom-0 size-5'
|
||||||
emoji={emoji}
|
emoji={emoji}
|
||||||
src={emojiUrl}
|
src={emojiUrl}
|
||||||
/>
|
/>
|
||||||
|
@ -275,7 +275,7 @@ const Account = ({
|
||||||
<>
|
<>
|
||||||
<Text tag='span' theme='muted' size='sm'>·</Text>
|
<Text tag='span' theme='muted' size='sm'>·</Text>
|
||||||
|
|
||||||
<Icon className='h-5 w-5 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/outline/pencil.svg')} />
|
<Icon className='size-5 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/outline/pencil.svg')} />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ const Reaction: React.FC<IReaction> = ({ announcementId, reaction, emojiMap, sty
|
||||||
title={`:${shortCode}:`}
|
title={`:${shortCode}:`}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
<span className='block h-4 w-4'>
|
<span className='block size-4'>
|
||||||
<Emoji hovered={hovered} emoji={reaction.name} emojiMap={emojiMap} />
|
<Emoji hovered={hovered} emoji={reaction.name} emojiMap={emojiMap} />
|
||||||
</span>
|
</span>
|
||||||
<span className='block min-w-[9px] text-center text-xs font-medium text-primary-600 dark:text-white'>
|
<span className='block min-w-[9px] text-center text-xs font-medium text-primary-600 dark:text-white'>
|
||||||
|
|
|
@ -165,8 +165,8 @@ const AuthorizeRejectButton: React.FC<IAuthorizeRejectButton> = ({ theme, icon,
|
||||||
src={isLoading ? require('@tabler/icons/filled/player-stop.svg') : icon}
|
src={isLoading ? require('@tabler/icons/filled/player-stop.svg') : icon}
|
||||||
onClick={action}
|
onClick={action}
|
||||||
theme='seamless'
|
theme='seamless'
|
||||||
className='h-10 w-10 items-center justify-center bg-white focus:!ring-0 dark:!bg-gray-900'
|
className='size-10 items-center justify-center bg-white focus:!ring-0 dark:!bg-gray-900'
|
||||||
iconClassName={clsx('h-6 w-6', {
|
iconClassName={clsx('size-6', {
|
||||||
'text-primary-500': theme === 'primary',
|
'text-primary-500': theme === 'primary',
|
||||||
'text-danger-600': theme === 'danger',
|
'text-danger-600': theme === 'danger',
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -16,9 +16,9 @@ const BigCard: React.FC<IBigCard> = ({ title, subtitle, children, onClose }) =>
|
||||||
return (
|
return (
|
||||||
<Card variant='rounded' size='xl'>
|
<Card variant='rounded' size='xl'>
|
||||||
<CardBody className='relative'>
|
<CardBody className='relative'>
|
||||||
<div className='-mx-4 mb-4 border-b border-solid border-gray-200 pb-4 sm:-mx-10 sm:pb-10 dark:border-gray-800'>
|
<div className='-mx-4 mb-4 border-b border-solid border-gray-200 pb-4 dark:border-gray-800 sm:-mx-10 sm:pb-10'>
|
||||||
<Stack space={2}>
|
<Stack space={2}>
|
||||||
{onClose && (<IconButton src={closeIcon} className='absolute right-[2%] text-gray-500 hover:text-gray-700 rtl:rotate-180 dark:text-gray-300 dark:hover:text-gray-200' onClick={onClose} />)}
|
{onClose && (<IconButton src={closeIcon} className='absolute right-[2%] text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-200 rtl:rotate-180' onClick={onClose} />)}
|
||||||
<Text size='2xl' align='center' weight='bold'>{title}</Text>
|
<Text size='2xl' align='center' weight='bold'>{title}</Text>
|
||||||
{subtitle && <Text theme='muted' align='center'>{subtitle}</Text>}
|
{subtitle && <Text theme='muted' align='center'>{subtitle}</Text>}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import IconButton from 'soapbox/components/icon-button';
|
|
||||||
import { DatePicker } from 'soapbox/features/ui/util/async-components';
|
|
||||||
import { useInstance, useFeatures } from 'soapbox/hooks';
|
import { useInstance, useFeatures } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import { Datetime } from './ui/datetime/datetime';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
birthdayPlaceholder: { id: 'edit_profile.fields.birthday_placeholder', defaultMessage: 'Your birthday' },
|
birthdayPlaceholder: { id: 'edit_profile.fields.birthday_placeholder', defaultMessage: 'Your birthday' },
|
||||||
previousMonth: { id: 'datepicker.previous_month', defaultMessage: 'Previous month' },
|
previousMonth: { id: 'datepicker.previous_month', defaultMessage: 'Previous month' },
|
||||||
|
@ -28,7 +28,7 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
|
||||||
const minAge = instance.pleroma.metadata.birthday_min_age;
|
const minAge = instance.pleroma.metadata.birthday_min_age;
|
||||||
|
|
||||||
const maxDate = useMemo(() => {
|
const maxDate = useMemo(() => {
|
||||||
if (!supportsBirthdays) return null;
|
if (!supportsBirthdays) return;
|
||||||
|
|
||||||
let maxDate = new Date();
|
let maxDate = new Date();
|
||||||
maxDate = new Date(maxDate.getTime() - minAge * 1000 * 60 * 60 * 24 + maxDate.getTimezoneOffset() * 1000 * 60);
|
maxDate = new Date(maxDate.getTime() - minAge * 1000 * 60 * 60 * 24 + maxDate.getTimezoneOffset() * 1000 * 60);
|
||||||
|
@ -36,7 +36,7 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
|
||||||
}, [minAge]);
|
}, [minAge]);
|
||||||
|
|
||||||
const selected = useMemo(() => {
|
const selected = useMemo(() => {
|
||||||
if (!supportsBirthdays || !value) return null;
|
if (!supportsBirthdays || !value) return;
|
||||||
|
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
return new Date(date.getTime() + (date.getTimezoneOffset() * 60000));
|
return new Date(date.getTime() + (date.getTimezoneOffset() * 60000));
|
||||||
|
@ -44,85 +44,17 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
|
||||||
|
|
||||||
if (!supportsBirthdays) return null;
|
if (!supportsBirthdays) return null;
|
||||||
|
|
||||||
const renderCustomHeader = ({
|
|
||||||
decreaseMonth,
|
|
||||||
increaseMonth,
|
|
||||||
prevMonthButtonDisabled,
|
|
||||||
nextMonthButtonDisabled,
|
|
||||||
decreaseYear,
|
|
||||||
increaseYear,
|
|
||||||
prevYearButtonDisabled,
|
|
||||||
nextYearButtonDisabled,
|
|
||||||
date,
|
|
||||||
}: {
|
|
||||||
decreaseMonth(): void;
|
|
||||||
increaseMonth(): void;
|
|
||||||
prevMonthButtonDisabled: boolean;
|
|
||||||
nextMonthButtonDisabled: boolean;
|
|
||||||
decreaseYear(): void;
|
|
||||||
increaseYear(): void;
|
|
||||||
prevYearButtonDisabled: boolean;
|
|
||||||
nextYearButtonDisabled: boolean;
|
|
||||||
date: Date;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<IconButton
|
|
||||||
className='datepicker__button rtl:rotate-180'
|
|
||||||
src={require('@tabler/icons/outline/chevron-left.svg')}
|
|
||||||
onClick={decreaseMonth}
|
|
||||||
disabled={prevMonthButtonDisabled}
|
|
||||||
aria-label={intl.formatMessage(messages.previousMonth)}
|
|
||||||
title={intl.formatMessage(messages.previousMonth)}
|
|
||||||
/>
|
|
||||||
{intl.formatDate(date, { month: 'long' })}
|
|
||||||
<IconButton
|
|
||||||
className='datepicker__button rtl:rotate-180'
|
|
||||||
src={require('@tabler/icons/outline/chevron-right.svg')}
|
|
||||||
onClick={increaseMonth}
|
|
||||||
disabled={nextMonthButtonDisabled}
|
|
||||||
aria-label={intl.formatMessage(messages.nextMonth)}
|
|
||||||
title={intl.formatMessage(messages.nextMonth)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<IconButton
|
|
||||||
className='datepicker__button rtl:rotate-180'
|
|
||||||
src={require('@tabler/icons/outline/chevron-left.svg')}
|
|
||||||
onClick={decreaseYear}
|
|
||||||
disabled={prevYearButtonDisabled}
|
|
||||||
aria-label={intl.formatMessage(messages.previousYear)}
|
|
||||||
title={intl.formatMessage(messages.previousYear)}
|
|
||||||
/>
|
|
||||||
{intl.formatDate(date, { year: 'numeric' })}
|
|
||||||
<IconButton
|
|
||||||
className='datepicker__button rtl:rotate-180'
|
|
||||||
src={require('@tabler/icons/outline/chevron-right.svg')}
|
|
||||||
onClick={increaseYear}
|
|
||||||
disabled={nextYearButtonDisabled}
|
|
||||||
aria-label={intl.formatMessage(messages.nextYear)}
|
|
||||||
title={intl.formatMessage(messages.nextYear)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (date: Date) => onChange(date ? new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) : '');
|
const handleChange = (date: Date) => onChange(date ? new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) : '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative mt-1 rounded-md shadow-sm'>
|
<div className='relative mt-1 rounded-md shadow-sm'>
|
||||||
<DatePicker
|
<Datetime
|
||||||
selected={selected}
|
value={selected ?? new Date()}
|
||||||
wrapperClassName='react-datepicker-wrapper'
|
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholderText={intl.formatMessage(messages.birthdayPlaceholder)}
|
placeholder={intl.formatMessage(messages.birthdayPlaceholder)}
|
||||||
minDate={new Date('1900-01-01')}
|
min={new Date('1900-01-01')}
|
||||||
maxDate={maxDate}
|
max={maxDate}
|
||||||
required={required}
|
required={required}
|
||||||
renderCustomHeader={renderCustomHeader}
|
|
||||||
isClearable={!required}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -92,12 +92,12 @@ const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{item.icon && <Icon src={item.icon} className='mr-3 h-5 w-5 flex-none rtl:ml-3 rtl:mr-0' />}
|
{item.icon && <Icon src={item.icon} className='mr-3 size-5 flex-none rtl:ml-3 rtl:mr-0' />}
|
||||||
|
|
||||||
<span className='truncate font-medium'>{item.text}</span>
|
<span className='truncate font-medium'>{item.text}</span>
|
||||||
|
|
||||||
{item.count ? (
|
{item.count ? (
|
||||||
<span className='ml-auto h-5 w-5 flex-none'>
|
<span className='ml-auto size-5 flex-none'>
|
||||||
<Counter count={item.count} />
|
<Counter count={item.count} />
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -320,7 +320,7 @@ const DropdownMenu = (props: IDropdownMenu) => {
|
||||||
<div
|
<div
|
||||||
ref={arrowRef}
|
ref={arrowRef}
|
||||||
style={arrowProps}
|
style={arrowProps}
|
||||||
className='pointer-events-none absolute z-[-1] h-3 w-3 bg-white black:bg-black dark:bg-gray-900'
|
className='pointer-events-none absolute z-[-1] size-3 bg-white black:bg-black dark:bg-gray-900'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Portal>
|
</Portal>
|
||||||
|
|
|
@ -11,7 +11,7 @@ const EmojiGraphic: React.FC<IEmojiGraphic> = ({ emoji }) => {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center justify-center'>
|
<div className='flex items-center justify-center'>
|
||||||
<div className='rounded-full bg-gray-100 p-8 dark:bg-gray-800'>
|
<div className='rounded-full bg-gray-100 p-8 dark:bg-gray-800'>
|
||||||
<Emoji className='h-24 w-24' emoji={emoji} />
|
<Emoji className='size-24' emoji={emoji} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -56,7 +56,7 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction,
|
||||||
{floatingAction && action}
|
{floatingAction && action}
|
||||||
</div>
|
</div>
|
||||||
<div className='h-40 bg-primary-200 dark:bg-gray-600'>
|
<div className='h-40 bg-primary-200 dark:bg-gray-600'>
|
||||||
{banner && <img className='h-full w-full object-cover' src={banner.url} alt={intl.formatMessage(messages.eventBanner)} />}
|
{banner && <img className='size-full object-cover' src={banner.url} alt={intl.formatMessage(messages.eventBanner)} />}
|
||||||
</div>
|
</div>
|
||||||
<Stack className='p-2.5' space={2}>
|
<Stack className='p-2.5' space={2}>
|
||||||
<HStack space={2} alignItems='center' justifyContent='between'>
|
<HStack space={2} alignItems='center' justifyContent='between'>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// @ts-ignore No types available
|
// @ts-ignore No types available
|
||||||
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { WasmBoy } from '@soapbox.pub/wasmboy';
|
import { WasmBoy } from '@soapbox.pub/wasmboy';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
@ -120,7 +121,7 @@ const Gameboy: React.FC<IGameboy> = ({ className, src, aspect = 'normal', onFocu
|
||||||
<canvas
|
<canvas
|
||||||
ref={canvas}
|
ref={canvas}
|
||||||
onClick={handleCanvasClick}
|
onClick={handleCanvasClick}
|
||||||
className={clsx('h-full w-full bg-black ', {
|
className={clsx('size-full bg-black ', {
|
||||||
'object-contain': aspect === 'normal',
|
'object-contain': aspect === 'normal',
|
||||||
'object-cover': aspect === 'stretched',
|
'object-cover': aspect === 'stretched',
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -24,7 +24,7 @@ const GroupCard: React.FC<IGroupCard> = ({ group }) => {
|
||||||
<Stack grow className='relative basis-1/2 rounded-t-lg bg-primary-100 dark:bg-gray-800'>
|
<Stack grow className='relative basis-1/2 rounded-t-lg bg-primary-100 dark:bg-gray-800'>
|
||||||
<GroupHeaderImage
|
<GroupHeaderImage
|
||||||
group={group}
|
group={group}
|
||||||
className='absolute inset-0 h-full w-full rounded-t-lg object-cover'
|
className='absolute inset-0 size-full rounded-t-lg object-cover'
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ const GroupCard: React.FC<IGroupCard> = ({ group }) => {
|
||||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
||||||
|
|
||||||
{group.relationship?.pending_requests && (
|
{group.relationship?.pending_requests && (
|
||||||
<div className='h-2 w-2 rounded-full bg-secondary-500' />
|
<div className='size-2 rounded-full bg-secondary-500' />
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ const GroupPopover = (props: IGroupPopoverContainer) => {
|
||||||
<Stack grow className='relative basis-1/2 rounded-t-lg bg-primary-100 dark:bg-gray-800'>
|
<Stack grow className='relative basis-1/2 rounded-t-lg bg-primary-100 dark:bg-gray-800'>
|
||||||
{group.header && (
|
{group.header && (
|
||||||
<img
|
<img
|
||||||
className='absolute inset-0 h-full w-full rounded-t-lg object-cover'
|
className='absolute inset-0 size-full rounded-t-lg object-cover'
|
||||||
src={group.header}
|
src={group.header}
|
||||||
alt=''
|
alt=''
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React, { useState, useRef, useLayoutEffect, Suspense } from 'react';
|
import React, { useState, useRef, useLayoutEffect } from 'react';
|
||||||
|
|
||||||
import Blurhash from 'soapbox/components/blurhash';
|
import Blurhash from 'soapbox/components/blurhash';
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
|
@ -15,7 +15,7 @@ import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maxi
|
||||||
import type { Property } from 'csstype';
|
import type { Property } from 'csstype';
|
||||||
import type { List as ImmutableList } from 'immutable';
|
import type { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
const Gameboy = React.lazy(() => import('./gameboy'));
|
// const Gameboy = React.lazy(() => import('./gameboy'));
|
||||||
|
|
||||||
const ATTACHMENT_LIMIT = 4;
|
const ATTACHMENT_LIMIT = 4;
|
||||||
const MAX_FILENAME_LENGTH = 45;
|
const MAX_FILENAME_LENGTH = 45;
|
||||||
|
@ -144,7 +144,7 @@ const Item: React.FC<IItem> = ({
|
||||||
let thumbnail: React.ReactNode = '';
|
let thumbnail: React.ReactNode = '';
|
||||||
const ext = attachment.url.split('.').pop()?.toLowerCase();
|
const ext = attachment.url.split('.').pop()?.toLowerCase();
|
||||||
|
|
||||||
if (attachment.type === 'unknown' && ['gb', 'gbc'].includes(ext!)) {
|
/*if (attachment.type === 'unknown' && ['gb', 'gbc'].includes(ext!)) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx('media-gallery__item', {
|
className={clsx('media-gallery__item', {
|
||||||
|
@ -159,11 +159,11 @@ const Item: React.FC<IItem> = ({
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (attachment.type === 'unknown') {
|
} else */if (attachment.type === 'unknown') {
|
||||||
const filename = truncateFilename(attachment.url, MAX_FILENAME_LENGTH);
|
const filename = truncateFilename(attachment.url, MAX_FILENAME_LENGTH);
|
||||||
const attachmentIcon = (
|
const attachmentIcon = (
|
||||||
<Icon
|
<Icon
|
||||||
className='h-16 w-16 text-gray-800 dark:text-gray-200'
|
className='size-16 text-gray-800 dark:text-gray-200'
|
||||||
src={MIMETYPE_ICONS[attachment.getIn(['pleroma', 'mime_type']) as string] || require('@tabler/icons/outline/paperclip.svg')}
|
src={MIMETYPE_ICONS[attachment.getIn(['pleroma', 'mime_type']) as string] || require('@tabler/icons/outline/paperclip.svg')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -195,7 +195,7 @@ const Item: React.FC<IItem> = ({
|
||||||
target='_blank'
|
target='_blank'
|
||||||
>
|
>
|
||||||
<StillImage
|
<StillImage
|
||||||
className='h-full w-full'
|
className='size-full'
|
||||||
src={mediaPreview ? attachment.preview_url : attachment.url}
|
src={mediaPreview ? attachment.preview_url : attachment.url}
|
||||||
alt={attachment.description}
|
alt={attachment.description}
|
||||||
letterboxed={letterboxed}
|
letterboxed={letterboxed}
|
||||||
|
|
|
@ -44,7 +44,7 @@ const PendingItemsRow: React.FC<IPendingItemsRow> = ({ to, count, size = 'md' })
|
||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
src={require('@tabler/icons/outline/chevron-right.svg')}
|
src={require('@tabler/icons/outline/chevron-right.svg')}
|
||||||
className='h-5 w-5 text-gray-600 transition-colors group-hover:text-gray-700 dark:text-gray-600 dark:group-hover:text-gray-500'
|
className='size-5 text-gray-600 transition-colors group-hover:text-gray-700 dark:text-gray-600 dark:group-hover:text-gray-500'
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { useAppDispatch } from 'soapbox/hooks';
|
||||||
import RelativeTimestamp from '../relative-timestamp';
|
import RelativeTimestamp from '../relative-timestamp';
|
||||||
import { Button, HStack, Stack, Text, Tooltip } from '../ui';
|
import { Button, HStack, Stack, Text, Tooltip } from '../ui';
|
||||||
|
|
||||||
import type { Selected } from './poll';
|
|
||||||
import type { Poll as PollEntity } from 'soapbox/types/entities';
|
import type { Poll as PollEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -18,7 +17,7 @@ const messages = defineMessages({
|
||||||
interface IPollFooter {
|
interface IPollFooter {
|
||||||
poll: PollEntity;
|
poll: PollEntity;
|
||||||
showResults: boolean;
|
showResults: boolean;
|
||||||
selected: Selected;
|
selected: Record<number, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PollFooter: React.FC<IPollFooter> = ({ poll, showResults, selected }): JSX.Element => {
|
const PollFooter: React.FC<IPollFooter> = ({ poll, showResults, selected }): JSX.Element => {
|
||||||
|
|
|
@ -75,7 +75,7 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
|
||||||
|
|
||||||
<div className='col-start-1 row-start-1 flex items-center justify-self-end'>
|
<div className='col-start-1 row-start-1 flex items-center justify-self-end'>
|
||||||
<span
|
<span
|
||||||
className={clsx('flex h-6 w-6 flex-none items-center justify-center rounded-full border border-solid', {
|
className={clsx('flex size-6 flex-none items-center justify-center rounded-full border border-solid', {
|
||||||
'bg-primary-600 border-primary-600 dark:bg-primary-300 dark:border-primary-300': active,
|
'bg-primary-600 border-primary-600 dark:bg-primary-300 dark:border-primary-300': active,
|
||||||
'border-primary-300 bg-white dark:bg-primary-900 dark:border-primary-500': !active,
|
'border-primary-300 bg-white dark:bg-primary-900 dark:border-primary-500': !active,
|
||||||
})}
|
})}
|
||||||
|
@ -86,7 +86,7 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
|
||||||
aria-label={option.title}
|
aria-label={option.title}
|
||||||
>
|
>
|
||||||
{active && (
|
{active && (
|
||||||
<Icon src={require('@tabler/icons/outline/check.svg')} className='h-4 w-4 text-white dark:text-primary-900' />
|
<Icon src={require('@tabler/icons/outline/check.svg')} className='size-4 text-white dark:text-primary-900' />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -145,7 +145,7 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
|
||||||
<Icon
|
<Icon
|
||||||
src={require('@tabler/icons/outline/circle-check.svg')}
|
src={require('@tabler/icons/outline/circle-check.svg')}
|
||||||
alt={intl.formatMessage(messages.voted)}
|
alt={intl.formatMessage(messages.voted)}
|
||||||
className='h-4 w-4 text-primary-600 dark:fill-white dark:text-primary-800'
|
className='size-4 text-primary-600 dark:fill-white dark:text-primary-800'
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className='svg-icon' />
|
<div className='svg-icon' />
|
||||||
|
|
|
@ -10,8 +10,6 @@ import { Stack, Text } from '../ui';
|
||||||
import PollFooter from './poll-footer';
|
import PollFooter from './poll-footer';
|
||||||
import PollOption from './poll-option';
|
import PollOption from './poll-option';
|
||||||
|
|
||||||
export type Selected = Record<number, boolean>;
|
|
||||||
|
|
||||||
interface IPoll {
|
interface IPoll {
|
||||||
id: string;
|
id: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
@ -28,7 +26,7 @@ const Poll: React.FC<IPoll> = ({ id, status }): JSX.Element | null => {
|
||||||
const isLoggedIn = useAppSelector((state) => state.me);
|
const isLoggedIn = useAppSelector((state) => state.me);
|
||||||
const poll = useAppSelector((state) => state.polls.get(id));
|
const poll = useAppSelector((state) => state.polls.get(id));
|
||||||
|
|
||||||
const [selected, setSelected] = useState({} as Selected);
|
const [selected, setSelected] = useState<Record<number, boolean>>({});
|
||||||
|
|
||||||
const openUnauthorizedModal = () =>
|
const openUnauthorizedModal = () =>
|
||||||
dispatch(openModal('UNAUTHORIZED', {
|
dispatch(openModal('UNAUTHORIZED', {
|
||||||
|
@ -49,7 +47,7 @@ const Poll: React.FC<IPoll> = ({ id, status }): JSX.Element | null => {
|
||||||
}
|
}
|
||||||
setSelected(tmp);
|
setSelected(tmp);
|
||||||
} else {
|
} else {
|
||||||
const tmp: Selected = {};
|
const tmp: Record<number, boolean> = {};
|
||||||
tmp[value] = true;
|
tmp[value] = true;
|
||||||
setSelected(tmp);
|
setSelected(tmp);
|
||||||
handleVote(value);
|
handleVote(value);
|
||||||
|
|
|
@ -148,7 +148,7 @@ const PreviewCard: React.FC<IPreviewCard> = ({
|
||||||
|
|
||||||
const canvas = (
|
const canvas = (
|
||||||
<Blurhash
|
<Blurhash
|
||||||
className='absolute inset-0 -z-10 h-full w-full'
|
className='absolute inset-0 -z-10 size-full'
|
||||||
hash={card.blurhash}
|
hash={card.blurhash}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -185,7 +185,7 @@ const PreviewCard: React.FC<IPreviewCard> = ({
|
||||||
<button onClick={handleEmbedClick} className='appearance-none text-gray-700 hover:text-gray-900 dark:text-gray-500 dark:hover:text-gray-100'>
|
<button onClick={handleEmbedClick} className='appearance-none text-gray-700 hover:text-gray-900 dark:text-gray-500 dark:hover:text-gray-100'>
|
||||||
<Icon
|
<Icon
|
||||||
src={iconVariant}
|
src={iconVariant}
|
||||||
className='h-6 w-6 text-inherit'
|
className='size-6 text-inherit'
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@ -199,7 +199,7 @@ const PreviewCard: React.FC<IPreviewCard> = ({
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
src={require('@tabler/icons/outline/external-link.svg')}
|
src={require('@tabler/icons/outline/external-link.svg')}
|
||||||
className='h-6 w-6 text-inherit'
|
className='size-6 text-inherit'
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
@ -220,7 +220,7 @@ const PreviewCard: React.FC<IPreviewCard> = ({
|
||||||
embed = (
|
embed = (
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'status-card__image',
|
'status-card__image',
|
||||||
'w-full flex-none rounded-l md:h-auto md:w-auto md:flex-auto',
|
'w-full flex-none rounded-l md:size-auto md:flex-auto',
|
||||||
{
|
{
|
||||||
'h-auto': horizontal,
|
'h-auto': horizontal,
|
||||||
'h-[200px]': !horizontal,
|
'h-[200px]': !horizontal,
|
||||||
|
|
|
@ -124,7 +124,7 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
|
||||||
<HStack alignItems='center' space={0.5}>
|
<HStack alignItems='center' space={0.5}>
|
||||||
<Icon
|
<Icon
|
||||||
src={require('@tabler/icons/outline/calendar.svg')}
|
src={require('@tabler/icons/outline/calendar.svg')}
|
||||||
className='h-4 w-4 text-gray-800 dark:text-gray-200'
|
className='size-4 text-gray-800 dark:text-gray-200'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text size='sm' title={intl.formatDate(account.created_at, dateFormatOptions)}>
|
<Text size='sm' title={intl.formatDate(account.created_at, dateFormatOptions)}>
|
||||||
|
|
|
@ -18,7 +18,7 @@ const QuotedStatusIndicator: React.FC<IQuotedStatusIndicator> = ({ statusId }) =
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack alignItems='center' space={1}>
|
<HStack alignItems='center' space={1}>
|
||||||
<Icon className='h-5 w-5' src={require('@tabler/icons/outline/quote.svg')} aria-hidden />
|
<Icon className='size-5' src={require('@tabler/icons/outline/quote.svg')} aria-hidden />
|
||||||
<Text truncate>{status.url}</Text>
|
<Text truncate>{status.url}</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
);
|
);
|
||||||
|
|
|
@ -31,7 +31,7 @@ const RadioItem: React.FC<IRadioItem> = ({ label, hint, checked = false, onChang
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
className='h-4 w-4 border-gray-300 text-primary-600 focus:ring-primary-500'
|
className='size-4 border-gray-300 text-primary-600 focus:ring-primary-500'
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
|
|
|
@ -91,7 +91,7 @@ const ScrollTopButton: React.FC<IScrollTopButton> = ({
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
className='h-4 w-4'
|
className='size-4'
|
||||||
src={require('@tabler/icons/outline/arrow-bar-to-up.svg')}
|
src={require('@tabler/icons/outline/arrow-bar-to-up.svg')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ const SidebarLink: React.FC<ISidebarLink> = ({ href, to, icon, text, onClick, co
|
||||||
const body = (
|
const body = (
|
||||||
<HStack space={2} alignItems='center'>
|
<HStack space={2} alignItems='center'>
|
||||||
<div className='relative inline-flex rounded-full bg-primary-50 p-2 dark:bg-gray-800'>
|
<div className='relative inline-flex rounded-full bg-primary-50 p-2 dark:bg-gray-800'>
|
||||||
<Icon src={icon} className='h-5 w-5 text-primary-500' count={count} />
|
<Icon src={icon} className='size-5 text-primary-500' count={count} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Text tag='span' weight='medium' theme='inherit'>{text}</Text>
|
<Text tag='span' weight='medium' theme='inherit'>{text}</Text>
|
||||||
|
@ -170,7 +170,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
||||||
className='absolute right-0 top-0 -mr-11 mt-2 text-gray-600 hover:text-gray-600 dark:text-gray-400 dark:hover:text-gray-300'
|
className='absolute right-0 top-0 -mr-11 mt-2 text-gray-600 hover:text-gray-600 dark:text-gray-400 dark:hover:text-gray-300'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='relative h-full w-full overflow-auto overflow-y-scroll'>
|
<div className='relative size-full overflow-auto overflow-y-scroll'>
|
||||||
<div className='p-4'>
|
<div className='p-4'>
|
||||||
<Stack space={4}>
|
<Stack space={4}>
|
||||||
<Link to={`/@${account.acct}`} onClick={onClose}>
|
<Link to={`/@${account.acct}`} onClick={onClose}>
|
||||||
|
@ -348,7 +348,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
src={require('@tabler/icons/outline/chevron-down.svg')}
|
src={require('@tabler/icons/outline/chevron-down.svg')}
|
||||||
className={clsx('h-4 w-4 text-gray-900 transition-transform dark:text-gray-100', {
|
className={clsx('size-4 text-gray-900 transition-transform dark:text-gray-100', {
|
||||||
'rotate-180': switcher,
|
'rotate-180': switcher,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
@ -360,7 +360,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
||||||
{otherAccounts.map(account => renderAccount(account))}
|
{otherAccounts.map(account => renderAccount(account))}
|
||||||
|
|
||||||
<NavLink className='flex items-center space-x-1 py-2' to='/login/add' onClick={handleClose}>
|
<NavLink className='flex items-center space-x-1 py-2' to='/login/add' onClick={handleClose}>
|
||||||
<Icon className='h-4 w-4 text-primary-500' src={require('@tabler/icons/outline/plus.svg')} />
|
<Icon className='size-4 text-primary-500' src={require('@tabler/icons/outline/plus.svg')} />
|
||||||
<Text size='sm' weight='medium'>{intl.formatMessage(messages.addAccount)}</Text>
|
<Text size='sm' weight='medium'>{intl.formatMessage(messages.addAccount)}</Text>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -50,7 +50,7 @@ const SidebarNavigationLink = React.forwardRef((props: ISidebarNavigationLink, r
|
||||||
src={(isActive && activeIcon) || icon}
|
src={(isActive && activeIcon) || icon}
|
||||||
count={count}
|
count={count}
|
||||||
countMax={countMax}
|
countMax={countMax}
|
||||||
className={clsx('h-5 w-5', {
|
className={clsx('size-5', {
|
||||||
'text-gray-600 black:text-white dark:text-gray-500 group-hover:text-primary-500 dark:group-hover:text-primary-400': !isActive,
|
'text-gray-600 black:text-white dark:text-gray-500 group-hover:text-primary-500 dark:group-hover:text-primary-400': !isActive,
|
||||||
'text-primary-500 dark:text-primary-400': isActive,
|
'text-primary-500 dark:text-primary-400': isActive,
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -84,7 +84,7 @@ const SiteErrorBoundary: React.FC<ISiteErrorBoundary> = ({ children }) => {
|
||||||
|
|
||||||
<div className='py-8'>
|
<div className='py-8'>
|
||||||
<div className='mx-auto max-w-xl space-y-2 text-center'>
|
<div className='mx-auto max-w-xl space-y-2 text-center'>
|
||||||
<h1 className='text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl dark:text-gray-500'>
|
<h1 className='text-3xl font-extrabold tracking-tight text-gray-900 dark:text-gray-500 sm:text-4xl'>
|
||||||
<FormattedMessage id='alert.unexpected.message' defaultMessage='Something went wrong.' />
|
<FormattedMessage id='alert.unexpected.message' defaultMessage='Something went wrong.' />
|
||||||
</h1>
|
</h1>
|
||||||
<p className='text-lg text-gray-700 dark:text-gray-600'>
|
<p className='text-lg text-gray-700 dark:text-gray-600'>
|
||||||
|
|
|
@ -741,7 +741,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
>
|
>
|
||||||
<StatusActionButton
|
<StatusActionButton
|
||||||
title={replyTitle}
|
title={replyTitle}
|
||||||
icon={require('@tabler/icons/outline/message-circle-2.svg')}
|
icon={require('@tabler/icons/outline/message-circle.svg')}
|
||||||
onClick={handleReplyClick}
|
onClick={handleReplyClick}
|
||||||
count={replyCount}
|
count={replyCount}
|
||||||
text={withLabels ? intl.formatMessage(messages.reply) : undefined}
|
text={withLabels ? intl.formatMessage(messages.reply) : undefined}
|
||||||
|
|
|
@ -44,8 +44,8 @@ const StatusActionButton = React.forwardRef<HTMLButtonElement, IStatusActionButt
|
||||||
const renderIcon = () => {
|
const renderIcon = () => {
|
||||||
if (emoji) {
|
if (emoji) {
|
||||||
return (
|
return (
|
||||||
<span className='flex h-6 w-6 items-center justify-center'>
|
<span className='flex size-6 items-center justify-center'>
|
||||||
<Emoji className='h-full w-full p-0.5' emoji={emoji.name} src={emoji.url} />
|
<Emoji className='size-full p-0.5' emoji={emoji.name} src={emoji.url} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -27,7 +27,7 @@ interface IReadMoreButton {
|
||||||
const ReadMoreButton: React.FC<IReadMoreButton> = ({ onClick }) => (
|
const ReadMoreButton: React.FC<IReadMoreButton> = ({ onClick }) => (
|
||||||
<button className='flex items-center border-0 bg-transparent p-0 pt-2 text-gray-900 hover:underline active:underline dark:text-gray-300' onClick={onClick}>
|
<button className='flex items-center border-0 bg-transparent p-0 pt-2 text-gray-900 hover:underline active:underline dark:text-gray-300' onClick={onClick}>
|
||||||
<FormattedMessage id='status.read_more' defaultMessage='Read more' />
|
<FormattedMessage id='status.read_more' defaultMessage='Read more' />
|
||||||
<Icon className='inline-block h-5 w-5' src={require('@tabler/icons/outline/chevron-right.svg')} />
|
<Icon className='inline-block size-5' src={require('@tabler/icons/outline/chevron-right.svg')} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -212,7 +212,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
return (
|
return (
|
||||||
<StatusInfo
|
<StatusInfo
|
||||||
avatarSize={avatarSize}
|
avatarSize={avatarSize}
|
||||||
icon={<Icon src={require('@tabler/icons/outline/repeat.svg')} className='h-4 w-4 text-green-600' />}
|
icon={<Icon src={require('@tabler/icons/outline/repeat.svg')} className='size-4 text-green-600' />}
|
||||||
text={
|
text={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='status.reblogged_by_with_group'
|
id='status.reblogged_by_with_group'
|
||||||
|
@ -252,7 +252,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
return (
|
return (
|
||||||
<StatusInfo
|
<StatusInfo
|
||||||
avatarSize={avatarSize}
|
avatarSize={avatarSize}
|
||||||
icon={<Icon src={require('@tabler/icons/outline/repeat.svg')} className='h-4 w-4 text-green-600' />}
|
icon={<Icon src={require('@tabler/icons/outline/repeat.svg')} className='size-4 text-green-600' />}
|
||||||
text={
|
text={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='status.reblogged_by'
|
id='status.reblogged_by'
|
||||||
|
@ -279,7 +279,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
return (
|
return (
|
||||||
<StatusInfo
|
<StatusInfo
|
||||||
avatarSize={avatarSize}
|
avatarSize={avatarSize}
|
||||||
icon={<Icon src={require('@tabler/icons/outline/pinned.svg')} className='h-4 w-4 text-gray-600 dark:text-gray-400' />}
|
icon={<Icon src={require('@tabler/icons/outline/pinned.svg')} className='size-4 text-gray-600 dark:text-gray-400' />}
|
||||||
text={
|
text={
|
||||||
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
|
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
|
||||||
}
|
}
|
||||||
|
@ -289,7 +289,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
return (
|
return (
|
||||||
<StatusInfo
|
<StatusInfo
|
||||||
avatarSize={avatarSize}
|
avatarSize={avatarSize}
|
||||||
icon={<Icon src={require('@tabler/icons/outline/circles.svg')} className='h-4 w-4 text-primary-600 dark:text-accent-blue' />}
|
icon={<Icon src={require('@tabler/icons/outline/circles.svg')} className='size-4 text-primary-600 dark:text-accent-blue' />}
|
||||||
text={
|
text={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='status.group'
|
id='status.group'
|
||||||
|
|
|
@ -25,7 +25,7 @@ const StatusInfo = (props: IStatusInfo) => {
|
||||||
<HStack
|
<HStack
|
||||||
space={3}
|
space={3}
|
||||||
alignItems='center'
|
alignItems='center'
|
||||||
className='cursor-default text-xs font-medium text-gray-700 rtl:space-x-reverse dark:text-gray-600'
|
className='cursor-default text-xs font-medium text-gray-700 dark:text-gray-600 rtl:space-x-reverse'
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className='flex justify-end'
|
className='flex justify-end'
|
||||||
|
|
|
@ -40,7 +40,7 @@ const StillImage: React.FC<IStillImage> = ({ alt, className, src, style, letterb
|
||||||
};
|
};
|
||||||
|
|
||||||
/** ClassNames shared between the `<img>` and `<canvas>` elements. */
|
/** ClassNames shared between the `<img>` and `<canvas>` elements. */
|
||||||
const baseClassName = clsx('block h-full w-full', {
|
const baseClassName = clsx('block size-full', {
|
||||||
'object-contain': letterboxed,
|
'object-contain': letterboxed,
|
||||||
'object-cover': !letterboxed,
|
'object-cover': !letterboxed,
|
||||||
});
|
});
|
||||||
|
|
|
@ -67,13 +67,13 @@ const Accordion: React.FC<IAccordion> = ({ headline, children, menu, expanded =
|
||||||
<button onClick={handleAction} title={actionLabel}>
|
<button onClick={handleAction} title={actionLabel}>
|
||||||
<Icon
|
<Icon
|
||||||
src={actionIcon}
|
src={actionIcon}
|
||||||
className='h-5 w-5 text-gray-700 dark:text-gray-600'
|
className='size-5 text-gray-700 dark:text-gray-600'
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<Icon
|
<Icon
|
||||||
src={expanded ? require('@tabler/icons/outline/chevron-up.svg') : require('@tabler/icons/outline/chevron-down.svg')}
|
src={expanded ? require('@tabler/icons/outline/chevron-up.svg') : require('@tabler/icons/outline/chevron-down.svg')}
|
||||||
className='h-5 w-5 text-gray-700 dark:text-gray-600'
|
className='size-5 text-gray-700 dark:text-gray-600'
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -36,7 +36,7 @@ const Avatar = (props: IAvatar) => {
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
src={require('@tabler/icons/outline/photo-off.svg')}
|
src={require('@tabler/icons/outline/photo-off.svg')}
|
||||||
className='h-4 w-4 text-gray-500 dark:text-gray-700'
|
className='size-4 text-gray-500 dark:text-gray-700'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -66,7 +66,7 @@ const Button = React.forwardRef<HTMLButtonElement, IButton>((props, ref): JSX.El
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Icon src={icon} className='h-4 w-4' element={iconElement} />;
|
return <Icon src={icon} className='size-4' element={iconElement} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick: React.MouseEventHandler<HTMLButtonElement> = React.useCallback((event) => {
|
const handleClick: React.MouseEventHandler<HTMLButtonElement> = React.useCallback((event) => {
|
||||||
|
|
|
@ -72,7 +72,7 @@ const CardHeader: React.FC<ICardHeader> = ({ className, children, backHref, onBa
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp {...backAttributes} className='rounded-full text-gray-900 focus:ring-2 focus:ring-primary-500 dark:text-gray-100' aria-label={intl.formatMessage(messages.back)}>
|
<Comp {...backAttributes} className='rounded-full text-gray-900 focus:ring-2 focus:ring-primary-500 dark:text-gray-100' aria-label={intl.formatMessage(messages.back)}>
|
||||||
<SvgIcon src={require('@tabler/icons/outline/arrow-left.svg')} className='h-6 w-6 rtl:rotate-180' />
|
<SvgIcon src={require('@tabler/icons/outline/arrow-left.svg')} className='size-6 rtl:rotate-180' />
|
||||||
<span className='sr-only' data-testid='back-button'>{intl.formatMessage(messages.back)}</span>
|
<span className='sr-only' data-testid='back-button'>{intl.formatMessage(messages.back)}</span>
|
||||||
</Comp>
|
</Comp>
|
||||||
);
|
);
|
||||||
|
|
|
@ -69,7 +69,7 @@ const Carousel: React.FC<ICarousel> = (props): JSX.Element => {
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
src={require('@tabler/icons/outline/chevron-left.svg')}
|
src={require('@tabler/icons/outline/chevron-left.svg')}
|
||||||
className='h-5 w-5 text-black dark:text-white'
|
className='size-5 text-black dark:text-white'
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -101,7 +101,7 @@ const Carousel: React.FC<ICarousel> = (props): JSX.Element => {
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
src={require('@tabler/icons/outline/chevron-right.svg')}
|
src={require('@tabler/icons/outline/chevron-right.svg')}
|
||||||
className='h-5 w-5 text-black dark:text-white'
|
className='size-5 text-black dark:text-white'
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,7 +9,7 @@ const Checkbox = React.forwardRef<HTMLInputElement, ICheckbox>((props, ref) => {
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
type='checkbox'
|
type='checkbox'
|
||||||
className='h-4 w-4 rounded border-2 border-gray-300 text-primary-600 focus:ring-primary-500 black:bg-black dark:border-gray-800 dark:bg-gray-900'
|
className='size-4 rounded border-2 border-gray-300 text-primary-600 focus:ring-primary-500 black:bg-black dark:border-gray-800 dark:bg-gray-900'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Input from '../input/input';
|
||||||
|
|
||||||
|
interface DatetimeProps {
|
||||||
|
value: Date;
|
||||||
|
onChange(date: Date): void;
|
||||||
|
min?: Date;
|
||||||
|
max?: Date;
|
||||||
|
placeholder?: string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Date input with time.
|
||||||
|
*/
|
||||||
|
export const Datetime: React.FC<DatetimeProps> = ({ value, onChange, min, max, ...rest }) => {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type='datetime-local'
|
||||||
|
onChange={(e) => onChange(new Date(e.target.value))}
|
||||||
|
value={formatDateLocal(value)}
|
||||||
|
min={min ? formatDateLocal(min) : undefined}
|
||||||
|
max={max ? formatDateLocal(max) : undefined}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Format a Date object to 'YYYY-MM-DDTHH:MM' format, used by `<input type="datetime-local">` elements. */
|
||||||
|
function formatDateLocal(date: Date): string {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||||
|
}
|
|
@ -32,7 +32,7 @@ const EmojiButton: React.FC<IEmojiButton> = ({ emoji, className, onClick, tabInd
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={clsx(className)} onClick={handleClick} tabIndex={tabIndex}>
|
<button className={clsx(className)} onClick={handleClick} tabIndex={tabIndex}>
|
||||||
<EmojiComponent className='h-6 w-6 duration-100 hover:scale-110' emoji={emoji} />
|
<EmojiComponent className='size-6 duration-100 hover:scale-110' emoji={emoji} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,7 @@ const messages = defineMessages({
|
||||||
/** Possible theme names for an Input. */
|
/** Possible theme names for an Input. */
|
||||||
type InputThemes = 'normal' | 'search'
|
type InputThemes = 'normal' | 'search'
|
||||||
|
|
||||||
interface IInput extends Pick<React.InputHTMLAttributes<HTMLInputElement>, 'maxLength' | 'onChange' | 'onBlur' | 'type' | 'autoComplete' | 'autoCorrect' | 'autoCapitalize' | 'required' | 'disabled' | 'onClick' | 'readOnly' | 'min' | 'pattern' | 'onKeyDown' | 'onKeyUp' | 'onFocus' | 'style' | 'id'> {
|
interface IInput extends Pick<React.InputHTMLAttributes<HTMLInputElement>, 'maxLength' | 'onChange' | 'onBlur' | 'type' | 'autoComplete' | 'autoCorrect' | 'autoCapitalize' | 'required' | 'disabled' | 'onClick' | 'readOnly' | 'min' | 'max' | 'pattern' | 'onKeyDown' | 'onKeyUp' | 'onFocus' | 'style' | 'id'> {
|
||||||
/** Put the cursor into the input on mount. */
|
/** Put the cursor into the input on mount. */
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
/** The initial text in the input. */
|
/** The initial text in the input. */
|
||||||
|
@ -73,7 +73,7 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
|
||||||
>
|
>
|
||||||
{icon ? (
|
{icon ? (
|
||||||
<div className='pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3'>
|
<div className='pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3'>
|
||||||
<Icon src={icon} className='h-4 w-4 text-gray-700 dark:text-gray-600' aria-hidden='true' />
|
<Icon src={icon} className='size-4 text-gray-700 dark:text-gray-600' aria-hidden='true' />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
|
||||||
>
|
>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
src={revealed ? require('@tabler/icons/outline/eye-off.svg') : require('@tabler/icons/outline/eye.svg')}
|
src={revealed ? require('@tabler/icons/outline/eye-off.svg') : require('@tabler/icons/outline/eye.svg')}
|
||||||
className='h-4 w-4'
|
className='size-4'
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -47,11 +47,11 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ language, setLanguage }
|
||||||
return (
|
return (
|
||||||
<DropdownMenu items={newMenu} modal>
|
<DropdownMenu items={newMenu} modal>
|
||||||
{language ? (
|
{language ? (
|
||||||
<button type='button' className='flex h-full rounded-lg border-2 border-gray-700 px-1 text-gray-700 hover:cursor-pointer hover:border-gray-500 hover:text-gray-500 sm:mr-4 dark:border-white dark:text-white dark:hover:border-gray-700' onClick={() => dispatch(openDropdownMenu())}>
|
<button type='button' className='flex h-full rounded-lg border-2 border-gray-700 px-1 text-gray-700 hover:cursor-pointer hover:border-gray-500 hover:text-gray-500 dark:border-white dark:text-white dark:hover:border-gray-700 sm:mr-4' onClick={() => dispatch(openDropdownMenu())}>
|
||||||
{language.toUpperCase()}
|
{language.toUpperCase()}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<SvgIcon src={require('@tabler/icons/outline/world.svg')} className='text-gray-700 hover:cursor-pointer hover:text-gray-500 black:absolute black:right-0 black:top-4 black:text-white black:hover:text-gray-600 sm:mr-4 dark:text-white' />
|
<SvgIcon src={require('@tabler/icons/outline/world.svg')} className='text-gray-700 hover:cursor-pointer hover:text-gray-500 black:absolute black:right-0 black:top-4 black:text-white black:hover:text-gray-600 dark:text-white sm:mr-4' />
|
||||||
)}
|
)}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|
|
@ -118,7 +118,7 @@ const Modal = React.forwardRef<HTMLDivElement, IModal>(({
|
||||||
src={require('@tabler/icons/outline/arrow-left.svg')}
|
src={require('@tabler/icons/outline/arrow-left.svg')}
|
||||||
title={intl.formatMessage(messages.back)}
|
title={intl.formatMessage(messages.back)}
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className='text-gray-500 hover:text-gray-700 rtl:rotate-180 dark:text-gray-300 dark:hover:text-gray-200'
|
className='text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-200 rtl:rotate-180'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -131,7 +131,7 @@ const Modal = React.forwardRef<HTMLDivElement, IModal>(({
|
||||||
src={closeIcon}
|
src={closeIcon}
|
||||||
title={intl.formatMessage(messages.close)}
|
title={intl.formatMessage(messages.close)}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className='text-gray-500 hover:text-gray-700 rtl:rotate-180 dark:text-gray-300 dark:hover:text-gray-200'
|
className='text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-200 rtl:rotate-180'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,7 +25,7 @@ const RadioButton: React.FC<IRadioButton> = ({ name, value, checked, onChange, l
|
||||||
value={value}
|
value={value}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
className='h-4 w-4 border-gray-300 text-primary-600 focus:ring-primary-500'
|
className='size-4 border-gray-300 text-primary-600 focus:ring-primary-500'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label htmlFor={formFieldId} className='block text-sm font-medium text-gray-700'>
|
<label htmlFor={formFieldId} className='block text-sm font-medium text-gray-700'>
|
||||||
|
|
|
@ -14,7 +14,7 @@ const Select = React.forwardRef<HTMLSelectElement, ISelect>((props, ref) => {
|
||||||
<select
|
<select
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'truncate rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-primary-500 focus:outline-none focus:ring-primary-500 disabled:opacity-50 black:bg-black sm:text-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:focus:border-primary-500 dark:focus:ring-primary-500',
|
'truncate rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-primary-500 focus:outline-none focus:ring-primary-500 disabled:opacity-50 black:bg-black dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:focus:border-primary-500 dark:focus:ring-primary-500 sm:text-sm',
|
||||||
className,
|
className,
|
||||||
{
|
{
|
||||||
'w-full': full,
|
'w-full': full,
|
||||||
|
|
|
@ -60,7 +60,7 @@ const Slider: React.FC<ISlider> = ({ value, onChange }) => {
|
||||||
<div className='absolute top-1/2 h-1 w-full -translate-y-1/2 rounded-full bg-primary-200 dark:bg-primary-700' />
|
<div className='absolute top-1/2 h-1 w-full -translate-y-1/2 rounded-full bg-primary-200 dark:bg-primary-700' />
|
||||||
<div className='absolute top-1/2 h-1 -translate-y-1/2 rounded-full bg-accent-500' style={{ width: `${value * 100}%` }} />
|
<div className='absolute top-1/2 h-1 -translate-y-1/2 rounded-full bg-accent-500' style={{ width: `${value * 100}%` }} />
|
||||||
<span
|
<span
|
||||||
className='absolute top-1/2 z-[9] -ml-1.5 h-3 w-3 -translate-y-1/2 rounded-full bg-accent-500 shadow'
|
className='absolute top-1/2 z-[9] -ml-1.5 size-3 -translate-y-1/2 rounded-full bg-accent-500 shadow'
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
style={{ left: `${value * 100}%` }}
|
style={{ left: `${value * 100}%` }}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -45,7 +45,7 @@ const TagInput: React.FC<ITagInput> = ({ tags, onChange, placeholder }) => {
|
||||||
return (
|
return (
|
||||||
<div className='relative mt-1 grow shadow-sm'>
|
<div className='relative mt-1 grow shadow-sm'>
|
||||||
<HStack
|
<HStack
|
||||||
className='block w-full rounded-md border-gray-400 bg-white p-2 pb-0 text-gray-900 placeholder:text-gray-600 focus:border-primary-500 focus:ring-primary-500 sm:text-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:placeholder:text-gray-600 dark:focus:border-primary-500 dark:focus:ring-primary-500'
|
className='block w-full rounded-md border-gray-400 bg-white p-2 pb-0 text-gray-900 placeholder:text-gray-600 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:placeholder:text-gray-600 dark:focus:border-primary-500 dark:focus:ring-primary-500 sm:text-sm'
|
||||||
space={2}
|
space={2}
|
||||||
wrap
|
wrap
|
||||||
>
|
>
|
||||||
|
|
|
@ -91,7 +91,7 @@ const Textarea = React.forwardRef(({
|
||||||
ref={ref}
|
ref={ref}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className={clsx('block h-16 w-full rounded-md text-gray-900 placeholder:text-gray-600 sm:h-10 sm:text-sm dark:text-gray-100 dark:placeholder:text-gray-600', {
|
className={clsx('block h-16 w-full rounded-md text-gray-900 placeholder:text-gray-600 dark:text-gray-100 dark:placeholder:text-gray-600 sm:h-10 sm:text-sm', {
|
||||||
'bg-white dark:bg-transparent shadow-sm border-gray-400 dark:border-gray-800 dark:ring-1 dark:ring-gray-800 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500':
|
'bg-white dark:bg-transparent shadow-sm border-gray-400 dark:border-gray-800 dark:ring-1 dark:ring-gray-800 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500':
|
||||||
theme === 'default',
|
theme === 'default',
|
||||||
'bg-transparent border-0 focus:border-0 focus:ring-0': theme === 'transparent',
|
'bg-transparent border-0 focus:border-0 focus:ring-0': theme === 'transparent',
|
||||||
|
|
|
@ -43,7 +43,7 @@ const Toast = (props: IToast) => {
|
||||||
return (
|
return (
|
||||||
<Icon
|
<Icon
|
||||||
src={require('@tabler/icons/outline/circle-check.svg')}
|
src={require('@tabler/icons/outline/circle-check.svg')}
|
||||||
className='h-6 w-6 text-success-500 dark:text-success-400'
|
className='size-6 text-success-500 dark:text-success-400'
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -51,7 +51,7 @@ const Toast = (props: IToast) => {
|
||||||
return (
|
return (
|
||||||
<Icon
|
<Icon
|
||||||
src={require('@tabler/icons/outline/info-circle.svg')}
|
src={require('@tabler/icons/outline/info-circle.svg')}
|
||||||
className='h-6 w-6 text-primary-600 dark:text-accent-blue'
|
className='size-6 text-primary-600 dark:text-accent-blue'
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -59,7 +59,7 @@ const Toast = (props: IToast) => {
|
||||||
return (
|
return (
|
||||||
<Icon
|
<Icon
|
||||||
src={require('@tabler/icons/outline/alert-circle.svg')}
|
src={require('@tabler/icons/outline/alert-circle.svg')}
|
||||||
className='h-6 w-6 text-danger-600'
|
className='size-6 text-danger-600'
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -143,7 +143,7 @@ const Toast = (props: IToast) => {
|
||||||
data-testid='toast-dismiss'
|
data-testid='toast-dismiss'
|
||||||
>
|
>
|
||||||
<span className='sr-only'><FormattedMessage id='lightbox.close' defaultMessage='Close' /></span>
|
<span className='sr-only'><FormattedMessage id='lightbox.close' defaultMessage='Close' /></span>
|
||||||
<Icon src={require('@tabler/icons/outline/x.svg')} className='h-5 w-5' />
|
<Icon src={require('@tabler/icons/outline/x.svg')} className='size-5' />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
|
@ -52,7 +52,7 @@ const Widget: React.FC<IWidget> = ({
|
||||||
<WidgetTitle title={title} />
|
<WidgetTitle title={title} />
|
||||||
{action || (onActionClick && (
|
{action || (onActionClick && (
|
||||||
<IconButton
|
<IconButton
|
||||||
className='ml-2 h-6 w-6 text-black rtl:rotate-180 dark:text-white'
|
className='ml-2 size-6 text-black dark:text-white rtl:rotate-180'
|
||||||
src={actionIcon}
|
src={actionIcon}
|
||||||
onClick={onActionClick}
|
onClick={onActionClick}
|
||||||
title={actionTitle}
|
title={actionTitle}
|
||||||
|
|
|
@ -14,7 +14,7 @@ const UploadProgress: React.FC<IUploadProgress> = ({ progress }) => {
|
||||||
<HStack alignItems='center' space={2}>
|
<HStack alignItems='center' space={2}>
|
||||||
<Icon
|
<Icon
|
||||||
src={require('@tabler/icons/outline/cloud-upload.svg')}
|
src={require('@tabler/icons/outline/cloud-upload.svg')}
|
||||||
className='h-7 w-7 text-gray-500'
|
className='size-7 text-gray-500'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack space={1}>
|
<Stack space={1}>
|
||||||
|
|
|
@ -148,7 +148,7 @@ const Upload: React.FC<IUpload> = ({
|
||||||
|
|
||||||
const uploadIcon = mediaType === 'unknown' && (
|
const uploadIcon = mediaType === 'unknown' && (
|
||||||
<Icon
|
<Icon
|
||||||
className='mx-auto my-12 h-16 w-16 text-gray-800 dark:text-gray-200'
|
className='mx-auto my-12 size-16 text-gray-800 dark:text-gray-200'
|
||||||
src={MIMETYPE_ICONS[mimeType || ''] || defaultIcon}
|
src={MIMETYPE_ICONS[mimeType || ''] || defaultIcon}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -225,7 +225,7 @@ const Upload: React.FC<IUpload> = ({
|
||||||
'opacity-100': !active,
|
'opacity-100': !active,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Icon className='h-4 w-4' src={require('@tabler/icons/outline/alert-triangle.svg')} />
|
<Icon className='size-4' src={require('@tabler/icons/outline/alert-triangle.svg')} />
|
||||||
<FormattedMessage id='upload_form.description_missing.indicator' defaultMessage='Alt' />
|
<FormattedMessage id='upload_form.description_missing.indicator' defaultMessage='Alt' />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -68,7 +68,7 @@ const MediaItem: React.FC<IMediaItem> = ({ attachment, onOpenMedia }) => {
|
||||||
src={attachment.preview_url}
|
src={attachment.preview_url}
|
||||||
alt={attachment.description}
|
alt={attachment.description}
|
||||||
style={{ objectPosition: `${x}% ${y}%` }}
|
style={{ objectPosition: `${x}% ${y}%` }}
|
||||||
className='h-full w-full overflow-hidden rounded-lg'
|
className='size-full overflow-hidden rounded-lg'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (['gifv', 'video'].indexOf(attachment.type) !== -1) {
|
} else if (['gifv', 'video'].indexOf(attachment.type) !== -1) {
|
||||||
|
|
|
@ -114,14 +114,14 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||||
return (
|
return (
|
||||||
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6'>
|
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6'>
|
||||||
<div>
|
<div>
|
||||||
<div className='relative h-32 w-full bg-gray-200 black:rounded-t-none md:rounded-t-xl lg:h-48 dark:bg-gray-900/50' />
|
<div className='relative h-32 w-full bg-gray-200 black:rounded-t-none dark:bg-gray-900/50 md:rounded-t-xl lg:h-48' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='px-4 sm:px-6'>
|
<div className='px-4 sm:px-6'>
|
||||||
<HStack alignItems='bottom' space={5} className='-mt-12'>
|
<HStack alignItems='bottom' space={5} className='-mt-12'>
|
||||||
<div className='relative flex'>
|
<div className='relative flex'>
|
||||||
<div
|
<div
|
||||||
className='h-24 w-24 rounded-full bg-gray-400 ring-4 ring-white dark:ring-gray-800'
|
className='size-24 rounded-full bg-gray-400 ring-4 ring-white dark:ring-gray-800'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
@ -660,7 +660,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className='relative isolate flex h-32 w-full flex-col justify-center overflow-hidden bg-gray-200 black:rounded-t-none md:rounded-t-xl lg:h-48 dark:bg-gray-900/50'>
|
<div className='relative isolate flex h-32 w-full flex-col justify-center overflow-hidden bg-gray-200 black:rounded-t-none dark:bg-gray-900/50 md:rounded-t-xl lg:h-48'>
|
||||||
{renderHeader()}
|
{renderHeader()}
|
||||||
|
|
||||||
<div className='absolute left-2 top-2'>
|
<div className='absolute left-2 top-2'>
|
||||||
|
@ -678,12 +678,12 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||||
<Avatar
|
<Avatar
|
||||||
src={account.avatar}
|
src={account.avatar}
|
||||||
size={96}
|
size={96}
|
||||||
className='relative h-24 w-24 rounded-full bg-white ring-4 ring-white dark:bg-primary-900 dark:ring-primary-900'
|
className='relative size-24 rounded-full bg-white ring-4 ring-white dark:bg-primary-900 dark:ring-primary-900'
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
{account.verified && (
|
{account.verified && (
|
||||||
<div className='absolute bottom-0 right-0'>
|
<div className='absolute bottom-0 right-0'>
|
||||||
<VerificationBadge className='h-6 w-6 rounded-full bg-white ring-2 ring-white dark:bg-primary-900 dark:ring-primary-900' />
|
<VerificationBadge className='size-6 rounded-full bg-white ring-2 ring-white dark:bg-primary-900 dark:ring-primary-900' />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -115,7 +115,7 @@ const Announcements: React.FC = () => {
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
itemClassName='py-3 first:pt-0 last:pb-0'
|
itemClassName='py-3 first:pt-0 last:pb-0'
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
showLoading={isLoading && !announcements?.length}
|
showLoading={isLoading}
|
||||||
>
|
>
|
||||||
{announcements!.map((announcement) => (
|
{announcements!.map((announcement) => (
|
||||||
<Announcement key={announcement.id} announcement={announcement} />
|
<Announcement key={announcement.id} announcement={announcement} />
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
import React from 'react';
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { fetchUsers } from 'soapbox/actions/admin';
|
import { useAdminAccounts } from 'soapbox/api/hooks/admin/useAdminAccounts';
|
||||||
|
import Account from 'soapbox/components/account';
|
||||||
import { Widget } from 'soapbox/components/ui';
|
import { Widget } from 'soapbox/components/ui';
|
||||||
import AccountContainer from 'soapbox/containers/account-container';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' },
|
title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' },
|
||||||
expand: { id: 'admin.latest_accounts_panel.more', defaultMessage: 'Click to see {count, plural, one {# account} other {# accounts}}' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ILatestAccountsPanel {
|
interface ILatestAccountsPanel {
|
||||||
|
@ -20,18 +17,8 @@ interface ILatestAccountsPanel {
|
||||||
const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
|
const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const accountIds = useAppSelector<ImmutableOrderedSet<string>>((state) => state.admin.get('latestUsers').take(limit));
|
|
||||||
|
|
||||||
const [total, setTotal] = useState(accountIds.size);
|
const { accounts } = useAdminAccounts(['local', 'active'], limit);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(fetchUsers(['local', 'active'], 1, null, limit))
|
|
||||||
.then((value) => {
|
|
||||||
setTotal((value as { count: number }).count);
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleAction = () => {
|
const handleAction = () => {
|
||||||
history.push('/soapbox/admin/users');
|
history.push('/soapbox/admin/users');
|
||||||
|
@ -41,10 +28,9 @@ const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
|
||||||
<Widget
|
<Widget
|
||||||
title={intl.formatMessage(messages.title)}
|
title={intl.formatMessage(messages.title)}
|
||||||
onActionClick={handleAction}
|
onActionClick={handleAction}
|
||||||
actionTitle={intl.formatMessage(messages.expand, { count: total })}
|
|
||||||
>
|
>
|
||||||
{accountIds.take(limit).map((account) => (
|
{accounts.slice(0, limit).map(account => (
|
||||||
<AccountContainer key={account} id={account} withRelationship={false} withDate />
|
<Account key={account.id} account={account} withRelationship={false} withDate />
|
||||||
))}
|
))}
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
|
|