Merge remote-tracking branch 'origin/main' into add-nip-05-modal

This commit is contained in:
Alex Gleason 2024-10-20 14:56:09 -05:00
commit b887cc010a
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
289 changed files with 1340 additions and 2275 deletions

View File

@ -1,86 +1,27 @@
image: node:21
variables:
NODE_ENV: test
DS_EXCLUDED_ANALYZERS: gemnasium-python
image: node:22
default:
interruptible: true
cache: &cache
key:
files:
- yarn.lock
paths:
- node_modules/
policy: pull
stages:
- deps
- test
- build
- 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:
stage: test
stage: build
before_script:
- yarn install --ignore-scripts
- apt-get update -y && apt-get install -y zip
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
- cd dist && zip -r ../soapbox.zip . && cd ..
variables:
NODE_ENV: production
artifacts:
paths:
- 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:
stage: deploy
environment:
@ -92,6 +33,7 @@ review:
- unzip soapbox.zip -d dist
- npx -y surge dist $CI_COMMIT_REF_SLUG.git.soapbox.pub
allow_failure: true
when: manual
pages:
stage: deploy
@ -108,30 +50,3 @@ pages:
only:
variables:
- $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

3
.madgerc Normal file
View File

@ -0,0 +1,3 @@
{
"fileExtensions": ["ts", "tsx"]
}

View File

@ -41,7 +41,7 @@
],
"dependencies": {
"@akryum/flexsearch-es": "^0.7.32",
"@emoji-mart/data": "^1.1.2",
"@emoji-mart/data": "^1.2.1",
"@floating-ui/react": "^0.26.0",
"@fontsource/inter": "^5.0.0",
"@fontsource/noto-sans-javanese": "^5.0.16",
@ -65,13 +65,13 @@
"@sentry/browser": "^8.34.0",
"@sentry/react": "^8.34.0",
"@sentry/types": "^8.34.0",
"@soapbox.pub/wasmboy": "^0.8.0",
"@soapbox/weblock": "npm:@jsr/soapbox__weblock",
"@tabler/icons": "^3.1.0",
"@tabler/icons": "^3.19.0",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@tanstack/react-query": "^5.0.0",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@tanstack/react-query": "^5.59.13",
"@twemoji/svg": "^15.0.0",
"@types/escape-html": "^1.0.1",
"@types/http-link-header": "^1.0.3",
"@types/leaflet": "^1.8.0",
@ -80,7 +80,6 @@
"@types/path-browserify": "^1.0.0",
"@types/react": "^18.3.9",
"@types/react-color": "^3.0.6",
"@types/react-datepicker": "^4.4.2",
"@types/react-dom": "^18.3.0",
"@types/react-helmet": "^6.1.5",
"@types/react-motion": "^0.0.40",
@ -99,11 +98,10 @@
"browserslist": "^4.16.6",
"clsx": "^2.0.0",
"comlink": "^4.4.1",
"cryptocurrency-icons": "^0.18.1",
"cssnano": "^6.0.0",
"detect-passive-events": "^2.0.0",
"emoji-datasource": "14.0.0",
"emoji-mart": "^5.5.2",
"emoji-datasource": "15.0.1",
"emoji-mart": "^5.6.0",
"escape-html": "^1.0.3",
"eslint-plugin-formatjs": "^4.12.2",
"exifr": "^7.1.3",
@ -128,7 +126,6 @@
"qrcode.react": "^3.1.0",
"react": "^18.3.1",
"react-color": "^2.19.3",
"react-datepicker": "^4.8.0",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.0.11",
"react-helmet": "^6.1.0",
@ -149,10 +146,9 @@
"redux": "^5.0.0",
"redux-thunk": "^3.1.0",
"reselect": "^5.0.0",
"sass": "^1.69.5",
"sass": "^1.79.5",
"semver": "^7.3.8",
"stringz": "^2.0.0",
"twemoji": "https://github.com/twitter/twemoji#v14.0.2",
"type-fest": "^4.0.0",
"typescript": "^5.6.2",
"vite": "^5.4.8",
@ -181,15 +177,15 @@
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-tailwindcss": "^3.13.0",
"eslint-plugin-tailwindcss": "^3.17.5",
"fake-indexeddb": "^5.0.0",
"husky": "^9.0.0",
"jsdom": "^24.0.0",
"lint-staged": ">=10",
"rollup-plugin-visualizer": "^5.9.2",
"stylelint": "^16.0.2",
"stylelint-config-standard-scss": "^12.0.0",
"tailwindcss": "^3.4.0",
"stylelint": "^16.10.0",
"stylelint-config-standard-scss": "^13.1.0",
"tailwindcss": "^3.4.13",
"vite-plugin-checker": "^0.8.0",
"vite-plugin-pwa": "^0.20.5",
"vitest": "^2.1.1"

View File

@ -3,7 +3,6 @@ import { fetchRelationships } from 'soapbox/actions/accounts';
import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer';
import { accountIdsToAccts } from 'soapbox/selectors';
import { filterBadges, getTagDiff } from 'soapbox/utils/badges';
import { getFeatures } from 'soapbox/utils/features';
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_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 = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST });
@ -118,100 +107,52 @@ const updateSoapboxConfig = (data: Record<string, any>) =>
return dispatch(updateConfig(params));
};
const fetchMastodonReports = (params: Record<string, any>) =>
(dispatch: AppDispatch, getState: () => RootState) =>
api(getState)
.get('/api/v1/admin/reports', { params })
.then(({ data: reports }) => {
function fetchReports(params: Record<string, any> = {}) {
return async (dispatch: AppDispatch, getState: () => RootState): Promise<void> => {
dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params });
try {
const { data: reports } = await api(getState).get('/api/v1/admin/reports', { params });
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 => {
} 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 });
if (features.mastodonAdmin) {
return dispatch(fetchMastodonReports(params));
} else {
const { resolved } = params;
return dispatch(fetchPleromaReports({
state: resolved === false ? 'open' : (resolved ? 'resolved' : null),
}));
}
};
}
const patchMastodonReports = (reports: { id: string; state: string }[]) =>
(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);
function patchReports(ids: string[], reportState: string) {
return (dispatch: AppDispatch, getState: () => RootState) => {
const reports = ids.map(id => ({ id, state: reportState }));
dispatch({ type: ADMIN_REPORTS_PATCH_REQUEST, reports });
if (features.mastodonAdmin) {
return dispatch(patchMastodonReports(reports));
} else {
return dispatch(patchPleromaReports(reports));
return Promise.all(
reports.map(async ({ id, state }) => {
try {
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[]) =>
patchReports(ids, 'closed');
function closeReports(ids: string[]) {
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> = {
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('need_approval')) params.pending = true;
return api(getState)
.get(next || '/api/v1/admin/accounts', { params })
.then(({ data: accounts, ...response }) => {
const next = getLinks(response as AxiosResponse<any, any>).refs.find(link => link.rel === 'next');
const count = next
? page * pageSize + 1
: (page - 1) * pageSize + accounts.length;
try {
const { data: accounts, ...response } = await api(getState).get(url || '/api/v1/admin/accounts', { params });
const next = getLinks(response as AxiosResponse<any, any>).refs.find(link => link.rel === 'next')?.uri;
dispatch(importFetchedAccounts(accounts.map(({ account }: APIEntity) => account)));
dispatch(fetchRelationships(accounts.map((account: APIEntity) => account.id)));
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));
dispatch(fetchRelationships(accounts.map((account_1: APIEntity) => account_1.id)));
dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, accounts, pageSize, filters, page, next });
return { accounts, next };
} catch (error) {
return dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize });
}
};
}
const revokeName = (accountId: string, reportId?: string) =>
(dispatch: AppDispatch, getState: () => RootState) =>
api(getState)
.post(`/api/v1/admin/accounts/${accountId}/action`, {
function revokeName(accountId: string, reportId?: string) {
return (_dispatch: AppDispatch, getState: () => RootState) => {
const params = {
type: 'revoke_name',
report_id: reportId,
});
};
const deactivateMastodonUsers = (accountIds: string[], reportId?: string) =>
(dispatch: AppDispatch, getState: () => RootState) =>
Promise.all(accountIds.map(accountId => {
api(getState)
.post(`/api/v1/admin/accounts/${accountId}/action`, {
return api(getState).post(`/api/v1/admin/accounts/${accountId}/action`, params);
};
}
function deactivateUsers(accountIds: string[], reportId?: string) {
return (dispatch: AppDispatch, getState: () => RootState) => {
return Promise.all(
accountIds.map(async (accountId) => {
const params = {
type: 'disable',
report_id: reportId,
})
.then(() => {
};
try {
await api(getState).post(`/api/v1/admin/accounts/${accountId}/action`, params);
dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, accountIds: [accountId] });
}).catch(error => {
} catch (error) {
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) =>
(dispatch: AppDispatch, getState: () => RootState) => {
@ -334,69 +218,31 @@ const deleteUser = (accountId: string) =>
});
};
const approveMastodonUser = (accountId: string) =>
(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);
function approveUser(accountId: string) {
return async (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, accountId });
if (features.mastodonAdmin) {
return dispatch(approveMastodonUser(accountId));
} else {
return dispatch(approvePleromaUser(accountId));
try {
const { data: user } = await api(getState)
.post(`/api/v1/admin/accounts/${accountId}/approve`);
dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, user, accountId });
} catch (error) {
dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountId });
}
};
}
const rejectUser = (accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const instance = state.instance;
const features = getFeatures(instance);
function rejectUser(accountId: string) {
return async (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: ADMIN_USERS_REJECT_REQUEST, accountId });
if (features.mastodonAdmin) {
return dispatch(rejectMastodonUser(accountId));
} else {
return dispatch(deleteUser(accountId));
try {
const { data: user } = await api(getState)
.post(`/api/v1/admin/accounts/${accountId}/reject`);
dispatch({ type: ADMIN_USERS_REJECT_SUCCESS, user, accountId });
} catch (error) {
dispatch({ type: ADMIN_USERS_REJECT_FAIL, error, accountId });
}
};
}
const deleteStatus = (id: string) =>
(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 {
ADMIN_CONFIG_FETCH_REQUEST,
ADMIN_CONFIG_FETCH_SUCCESS,
@ -622,13 +423,6 @@ export {
ADMIN_REMOVE_PERMISSION_GROUP_REQUEST,
ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS,
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,
updateConfig,
updateSoapboxConfig,
@ -652,7 +446,4 @@ export {
promoteToModerator,
demoteToUser,
setRole,
setUserIndexQuery,
fetchUserIndex,
expandUserIndex,
};

View File

@ -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);
};

View File

@ -11,8 +11,9 @@ import { selectAccount, selectOwnAccount } from 'soapbox/selectors';
import { tagHistory } from 'soapbox/settings';
import toast from 'soapbox/toast';
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 { importFetchedAccounts } from './importer';
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_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_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?' },
});
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) => ({
type: COMPOSE_CHANGE,
id: composeId,
@ -952,11 +919,9 @@ export {
COMPOSE_SCHEDULE_REMOVE,
COMPOSE_ADD_TO_MENTIONS,
COMPOSE_REMOVE_FROM_MENTIONS,
COMPOSE_SET_STATUS,
COMPOSE_EDITOR_STATE_SET,
COMPOSE_SET_GROUP_TIMELINE_VISIBLE,
COMPOSE_CHANGE_MEDIA_ORDER,
setComposeToStatus,
changeCompose,
replyCompose,
cancelReplyCompose,

View File

@ -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);
};

View File

@ -1,159 +1,108 @@
import { createPushSubscription, updatePushSubscription } from 'soapbox/actions/push-subscriptions';
import { pushNotificationsSetting } from 'soapbox/settings';
import { getVapidKey } from 'soapbox/utils/auth';
import { decode as decodeBase64 } 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));
};
/* eslint-disable compat/compat */
import { HTTPError } from 'soapbox/api/HTTPError';
import { MastodonClient } from 'soapbox/api/MastodonClient';
import { WebPushSubscription, webPushSubscriptionSchema } from 'soapbox/schemas/web-push';
import { decodeBase64Url } from 'soapbox/utils/base64';
// 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 register = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
const me = getState().me;
const vapidKey = getVapidKey(getState());
dispatch(setBrowserSupport(supportsPushNotifications));
/**
* Register web push notifications.
* This function creates a subscription if one hasn't been created already, and syncronizes it with the backend.
*/
export async function registerPushNotifications(api: MastodonClient, vapidKey: string) {
if (!supportsPushNotifications) {
console.warn('Your browser does not support Web Push Notifications.');
return;
}
if (!vapidKey) {
console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.');
const { subscription, created } = await getOrCreateSubscription(vapidKey);
if (created) {
await sendSubscriptionToBackend(api, subscription);
return;
}
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;
// 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 (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) {
return { subscription };
// 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
return unsubscribe({ registration, subscription }).then((registration: ServiceWorkerRegistration) => {
return subscribe(registration, getState);
}).then(
(subscription: PushSubscription) => dispatch(sendSubscriptionToBackend(subscription, me) as any));
}
// Something went wrong, try to subscribe again.
await subscription.unsubscribe();
const newSubscription = await createSubscription(vapidKey);
await sendSubscriptionToBackend(api, newSubscription);
}
}
// 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);
}
/** 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();
// Clear alerts and hide UI settings
dispatch(clearSubscription());
if (me) {
pushNotificationsSetting.remove(me);
if (subscription) {
return { subscription, created: false };
} else {
const subscription = await createSubscription(vapidKey);
return { subscription, created: true };
}
}
return getRegistration()
.then(getPushSubscription)
.then(unsubscribe);
})
.catch(console.warn);
/** 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;
}
}
}
/** Publish a new subscription to the backend. */
async function sendSubscriptionToBackend(api: MastodonClient, subscription: PushSubscription): Promise<WebPushSubscription> {
const params = {
subscription: subscription.toJSON(),
};
const saveSettings = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState().push_notifications;
const alerts = state.alerts;
const data = { alerts };
const me = getState().me;
const response = await api.post('/api/v1/push/subscription', params);
const data = await response.json();
return dispatch(updatePushSubscription({ data })).then(() => {
if (me) {
pushNotificationsSetting.set(me, data);
return webPushSubscriptionSchema.parse(data);
}
/** Check if the VAPID key and endpoint of the subscription match the data in the backend. */
function subscriptionMatchesBackend(subscription: PushSubscription, backend: WebPushSubscription): boolean {
const { applicationServerKey } = subscription.options;
if (subscription.endpoint !== backend.endpoint) {
return false;
}
}).catch(console.warn);
};
export {
register,
saveSettings,
};
if (!applicationServerKey) {
return false;
}
const backendKeyBytes: Uint8Array = decodeBase64Url(backend.server_key);
const subscriptionKeyBytes: Uint8Array = new Uint8Array(applicationServerKey);
return backendKeyBytes.toString() === subscriptionKeyBytes.toString();
}

View File

@ -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,
};

View File

@ -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,
};

View File

@ -4,7 +4,7 @@ import { shouldHaveCard } from 'soapbox/utils/status';
import api, { getNextLink } from '../api';
import { setComposeToStatus } from './compose';
import { setComposeToStatus } from './compose-status';
import { fetchGroupRelationships } from './groups';
import { importFetchedStatus, importFetchedStatuses } from './importer';
import { openModal } from './modals';

View File

@ -1,6 +1,7 @@
import { APIEntity } from 'soapbox/types/entities';
import { getFeatures } from 'soapbox/utils/features';
import api from '../api';
import api, { getLinks } from '../api';
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_SUCCESS = 'TRENDING_STATUSES_FETCH_SUCCESS';
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 = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
@ -20,18 +23,62 @@ const fetchTrendingStatuses = () =>
if (!features.trendingStatuses) return;
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({ type: TRENDING_STATUSES_FETCH_SUCCESS, statuses });
dispatch(fetchTrendingStatusesSuccess(statuses, next ? next.uri : null));
return statuses;
}).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 {
TRENDING_STATUSES_FETCH_REQUEST,
TRENDING_STATUSES_FETCH_SUCCESS,
TRENDING_STATUSES_FETCH_FAIL,
TRENDING_STATUSES_EXPAND_SUCCESS,
TRENDING_STATUSES_EXPAND_FAIL,
fetchTrendingStatuses,
expandTrendingStatuses,
};

View File

@ -1,7 +1,7 @@
import { HTTPError } from './HTTPError';
interface Opts {
searchParams?: Record<string, string | number | boolean>;
searchParams?: URLSearchParams | Record<string, string | number | boolean>;
headers?: Record<string, string>;
signal?: AbortSignal;
}
@ -51,7 +51,9 @@ export class MastodonClient {
const url = new URL(path, this.baseUrl);
if (opts.searchParams) {
const params = Object
const params = opts.searchParams instanceof URLSearchParams
? opts.searchParams
: Object
.entries(opts.searchParams)
.map(([key, value]) => ([key, String(value)]));

View File

@ -30,7 +30,7 @@ function useAccount(accountId?: string, opts: UseAccountOpts = {}) {
isLoading: isRelationshipLoading,
} = 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 account = useMemo(

View File

@ -31,7 +31,7 @@ function useAccountLookup(acct: string | undefined, opts: UseAccountLookupOpts =
isLoading: isRelationshipLoading,
} = 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);
useEffect(() => {

View File

@ -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 };
}

View File

@ -1,11 +1,14 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { useApi } from 'soapbox/hooks';
import { InstanceV1, instanceV1Schema } from 'soapbox/schemas/instance';
interface Opts extends Pick<UseQueryOptions<unknown>, 'enabled' | 'retryOnMount' | 'staleTime'> {
interface Opts {
/** The base URL of the instance. */
baseUrl?: string;
enabled?: boolean;
retryOnMount?: boolean;
staleTime?: number;
}
/** Get the Instance for the current backend. */

View File

@ -1,11 +1,14 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { useApi } from 'soapbox/hooks';
import { InstanceV2, instanceV2Schema } from 'soapbox/schemas/instance';
interface Opts extends Pick<UseQueryOptions<unknown>, 'enabled' | 'retryOnMount' | 'staleTime'> {
interface Opts {
/** The base URL of the instance. */
baseUrl?: string;
enabled?: boolean;
retryOnMount?: boolean;
staleTime?: number;
}
/** Get the Instance for the current backend. */

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -77,12 +77,12 @@ const AccountSearch: React.FC<IAccountSearch> = ({ onSelected, ...rest }) => {
>
<SvgIcon
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
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)}
/>
</div>

View File

@ -50,7 +50,7 @@ const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
return (
<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}
disabled={disabled}
>
@ -208,7 +208,7 @@ const Account = ({
<Avatar src={account.avatar} size={avatarSize} />
{emoji && (
<Emoji
className='absolute -right-1.5 bottom-0 h-5 w-5'
className='absolute -right-1.5 bottom-0 size-5'
emoji={emoji}
src={emojiUrl}
/>
@ -275,7 +275,7 @@ const Account = ({
<>
<Text tag='span' theme='muted' size='sm'>&middot;</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}

View File

@ -54,7 +54,7 @@ const Reaction: React.FC<IReaction> = ({ announcementId, reaction, emojiMap, sty
title={`:${shortCode}:`}
style={style}
>
<span className='block h-4 w-4'>
<span className='block size-4'>
<Emoji hovered={hovered} emoji={reaction.name} emojiMap={emojiMap} />
</span>
<span className='block min-w-[9px] text-center text-xs font-medium text-primary-600 dark:text-white'>

View File

@ -165,8 +165,8 @@ const AuthorizeRejectButton: React.FC<IAuthorizeRejectButton> = ({ theme, icon,
src={isLoading ? require('@tabler/icons/filled/player-stop.svg') : icon}
onClick={action}
theme='seamless'
className='h-10 w-10 items-center justify-center bg-white focus:!ring-0 dark:!bg-gray-900'
iconClassName={clsx('h-6 w-6', {
className='size-10 items-center justify-center bg-white focus:!ring-0 dark:!bg-gray-900'
iconClassName={clsx('size-6', {
'text-primary-500': theme === 'primary',
'text-danger-600': theme === 'danger',
})}

View File

@ -16,9 +16,9 @@ const BigCard: React.FC<IBigCard> = ({ title, subtitle, children, onClose }) =>
return (
<Card variant='rounded' size='xl'>
<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}>
{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>
{subtitle && <Text theme='muted' align='center'>{subtitle}</Text>}
</Stack>

View File

@ -1,10 +1,10 @@
import React, { useMemo } from 'react';
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 { Datetime } from './ui/datetime/datetime';
const messages = defineMessages({
birthdayPlaceholder: { id: 'edit_profile.fields.birthday_placeholder', defaultMessage: 'Your birthday' },
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 maxDate = useMemo(() => {
if (!supportsBirthdays) return null;
if (!supportsBirthdays) return;
let maxDate = new Date();
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]);
const selected = useMemo(() => {
if (!supportsBirthdays || !value) return null;
if (!supportsBirthdays || !value) return;
const date = new Date(value);
return new Date(date.getTime() + (date.getTimezoneOffset() * 60000));
@ -44,85 +44,17 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
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) : '');
return (
<div className='relative mt-1 rounded-md shadow-sm'>
<DatePicker
selected={selected}
wrapperClassName='react-datepicker-wrapper'
<Datetime
value={selected ?? new Date()}
onChange={handleChange}
placeholderText={intl.formatMessage(messages.birthdayPlaceholder)}
minDate={new Date('1900-01-01')}
maxDate={maxDate}
placeholder={intl.formatMessage(messages.birthdayPlaceholder)}
min={new Date('1900-01-01')}
max={maxDate}
required={required}
renderCustomHeader={renderCustomHeader}
isClearable={!required}
/>
</div>
);

View File

@ -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>
{item.count ? (
<span className='ml-auto h-5 w-5 flex-none'>
<span className='ml-auto size-5 flex-none'>
<Counter count={item.count} />
</span>
) : null}

View File

@ -320,7 +320,7 @@ const DropdownMenu = (props: IDropdownMenu) => {
<div
ref={arrowRef}
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>
</Portal>

View File

@ -11,7 +11,7 @@ const EmojiGraphic: React.FC<IEmojiGraphic> = ({ emoji }) => {
return (
<div className='flex items-center justify-center'>
<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>
);

View File

@ -56,7 +56,7 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction,
{floatingAction && action}
</div>
<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>
<Stack className='p-2.5' space={2}>
<HStack space={2} alignItems='center' justifyContent='between'>

View File

@ -1,4 +1,5 @@
// @ts-ignore No types available
// eslint-disable-next-line import/no-unresolved
import { WasmBoy } from '@soapbox.pub/wasmboy';
import clsx from 'clsx';
import React, { useCallback, useEffect, useRef, useState } from 'react';
@ -120,7 +121,7 @@ const Gameboy: React.FC<IGameboy> = ({ className, src, aspect = 'normal', onFocu
<canvas
ref={canvas}
onClick={handleCanvasClick}
className={clsx('h-full w-full bg-black ', {
className={clsx('size-full bg-black ', {
'object-contain': aspect === 'normal',
'object-cover': aspect === 'stretched',
})}

View File

@ -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'>
<GroupHeaderImage
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>
@ -39,7 +39,7 @@ const GroupCard: React.FC<IGroupCard> = ({ group }) => {
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
{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>

View File

@ -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'>
{group.header && (
<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}
alt=''
/>

View File

@ -1,5 +1,5 @@
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 Icon from 'soapbox/components/icon';
@ -15,7 +15,7 @@ import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maxi
import type { Property } from 'csstype';
import type { List as ImmutableList } from 'immutable';
const Gameboy = React.lazy(() => import('./gameboy'));
// const Gameboy = React.lazy(() => import('./gameboy'));
const ATTACHMENT_LIMIT = 4;
const MAX_FILENAME_LENGTH = 45;
@ -144,7 +144,7 @@ const Item: React.FC<IItem> = ({
let thumbnail: React.ReactNode = '';
const ext = attachment.url.split('.').pop()?.toLowerCase();
if (attachment.type === 'unknown' && ['gb', 'gbc'].includes(ext!)) {
/*if (attachment.type === 'unknown' && ['gb', 'gbc'].includes(ext!)) {
return (
<div
className={clsx('media-gallery__item', {
@ -159,11 +159,11 @@ const Item: React.FC<IItem> = ({
</Suspense>
</div>
);
} else if (attachment.type === 'unknown') {
} else */if (attachment.type === 'unknown') {
const filename = truncateFilename(attachment.url, MAX_FILENAME_LENGTH);
const attachmentIcon = (
<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')}
/>
);
@ -195,7 +195,7 @@ const Item: React.FC<IItem> = ({
target='_blank'
>
<StillImage
className='h-full w-full'
className='size-full'
src={mediaPreview ? attachment.preview_url : attachment.url}
alt={attachment.description}
letterboxed={letterboxed}

View File

@ -44,7 +44,7 @@ const PendingItemsRow: React.FC<IPendingItemsRow> = ({ to, count, size = 'md' })
<Icon
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>
</Link>

View File

@ -7,7 +7,6 @@ import { useAppDispatch } from 'soapbox/hooks';
import RelativeTimestamp from '../relative-timestamp';
import { Button, HStack, Stack, Text, Tooltip } from '../ui';
import type { Selected } from './poll';
import type { Poll as PollEntity } from 'soapbox/types/entities';
const messages = defineMessages({
@ -18,7 +17,7 @@ const messages = defineMessages({
interface IPollFooter {
poll: PollEntity;
showResults: boolean;
selected: Selected;
selected: Record<number, boolean>;
}
const PollFooter: React.FC<IPollFooter> = ({ poll, showResults, selected }): JSX.Element => {

View File

@ -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'>
<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,
'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}
>
{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>
</div>
@ -145,7 +145,7 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
<Icon
src={require('@tabler/icons/outline/circle-check.svg')}
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' />

View File

@ -10,8 +10,6 @@ import { Stack, Text } from '../ui';
import PollFooter from './poll-footer';
import PollOption from './poll-option';
export type Selected = Record<number, boolean>;
interface IPoll {
id: string;
status?: string;
@ -28,7 +26,7 @@ const Poll: React.FC<IPoll> = ({ id, status }): JSX.Element | null => {
const isLoggedIn = useAppSelector((state) => state.me);
const poll = useAppSelector((state) => state.polls.get(id));
const [selected, setSelected] = useState({} as Selected);
const [selected, setSelected] = useState<Record<number, boolean>>({});
const openUnauthorizedModal = () =>
dispatch(openModal('UNAUTHORIZED', {
@ -49,7 +47,7 @@ const Poll: React.FC<IPoll> = ({ id, status }): JSX.Element | null => {
}
setSelected(tmp);
} else {
const tmp: Selected = {};
const tmp: Record<number, boolean> = {};
tmp[value] = true;
setSelected(tmp);
handleVote(value);

View File

@ -148,7 +148,7 @@ const PreviewCard: React.FC<IPreviewCard> = ({
const canvas = (
<Blurhash
className='absolute inset-0 -z-10 h-full w-full'
className='absolute inset-0 -z-10 size-full'
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'>
<Icon
src={iconVariant}
className='h-6 w-6 text-inherit'
className='size-6 text-inherit'
/>
</button>
@ -199,7 +199,7 @@ const PreviewCard: React.FC<IPreviewCard> = ({
>
<Icon
src={require('@tabler/icons/outline/external-link.svg')}
className='h-6 w-6 text-inherit'
className='size-6 text-inherit'
/>
</a>
)}
@ -220,7 +220,7 @@ const PreviewCard: React.FC<IPreviewCard> = ({
embed = (
<div className={clsx(
'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-[200px]': !horizontal,

View File

@ -124,7 +124,7 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
<HStack alignItems='center' space={0.5}>
<Icon
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)}>

View File

@ -18,7 +18,7 @@ const QuotedStatusIndicator: React.FC<IQuotedStatusIndicator> = ({ statusId }) =
return (
<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>
</HStack>
);

View File

@ -31,7 +31,7 @@ const RadioItem: React.FC<IRadioItem> = ({ label, hint, checked = false, onChang
checked={checked}
onChange={onChange}
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>
);

View File

@ -91,7 +91,7 @@ const ScrollTopButton: React.FC<IScrollTopButton> = ({
onClick={handleClick}
>
<Icon
className='h-4 w-4'
className='size-4'
src={require('@tabler/icons/outline/arrow-bar-to-up.svg')}
/>

View File

@ -56,7 +56,7 @@ const SidebarLink: React.FC<ISidebarLink> = ({ href, to, icon, text, onClick, co
const body = (
<HStack space={2} alignItems='center'>
<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>
<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'
/>
<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'>
<Stack space={4}>
<Link to={`/@${account.acct}`} onClick={onClose}>
@ -348,7 +348,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
<Icon
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,
})}
/>
@ -360,7 +360,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
{otherAccounts.map(account => renderAccount(account))}
<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>
</NavLink>
</div>

View File

@ -50,7 +50,7 @@ const SidebarNavigationLink = React.forwardRef((props: ISidebarNavigationLink, r
src={(isActive && activeIcon) || icon}
count={count}
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-primary-500 dark:text-primary-400': isActive,
})}

View File

@ -84,7 +84,7 @@ const SiteErrorBoundary: React.FC<ISiteErrorBoundary> = ({ children }) => {
<div className='py-8'>
<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.' />
</h1>
<p className='text-lg text-gray-700 dark:text-gray-600'>

View File

@ -741,7 +741,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
>
<StatusActionButton
title={replyTitle}
icon={require('@tabler/icons/outline/message-circle-2.svg')}
icon={require('@tabler/icons/outline/message-circle.svg')}
onClick={handleReplyClick}
count={replyCount}
text={withLabels ? intl.formatMessage(messages.reply) : undefined}

View File

@ -44,8 +44,8 @@ const StatusActionButton = React.forwardRef<HTMLButtonElement, IStatusActionButt
const renderIcon = () => {
if (emoji) {
return (
<span className='flex h-6 w-6 items-center justify-center'>
<Emoji className='h-full w-full p-0.5' emoji={emoji.name} src={emoji.url} />
<span className='flex size-6 items-center justify-center'>
<Emoji className='size-full p-0.5' emoji={emoji.name} src={emoji.url} />
</span>
);
} else {

View File

@ -27,7 +27,7 @@ interface IReadMoreButton {
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}>
<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>
);

View File

@ -212,7 +212,7 @@ const Status: React.FC<IStatus> = (props) => {
return (
<StatusInfo
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={
<FormattedMessage
id='status.reblogged_by_with_group'
@ -252,7 +252,7 @@ const Status: React.FC<IStatus> = (props) => {
return (
<StatusInfo
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={
<FormattedMessage
id='status.reblogged_by'
@ -279,7 +279,7 @@ const Status: React.FC<IStatus> = (props) => {
return (
<StatusInfo
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={
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
}
@ -289,7 +289,7 @@ const Status: React.FC<IStatus> = (props) => {
return (
<StatusInfo
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={
<FormattedMessage
id='status.group'

View File

@ -25,7 +25,7 @@ const StatusInfo = (props: IStatusInfo) => {
<HStack
space={3}
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
className='flex justify-end'

View File

@ -40,7 +40,7 @@ const StillImage: React.FC<IStillImage> = ({ alt, className, src, style, letterb
};
/** 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-cover': !letterboxed,
});

View File

@ -67,13 +67,13 @@ const Accordion: React.FC<IAccordion> = ({ headline, children, menu, expanded =
<button onClick={handleAction} title={actionLabel}>
<Icon
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>
)}
<Icon
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>
</button>

View File

@ -36,7 +36,7 @@ const Avatar = (props: IAvatar) => {
>
<Icon
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>
);

View File

@ -66,7 +66,7 @@ const Button = React.forwardRef<HTMLButtonElement, IButton>((props, ref): JSX.El
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) => {

View File

@ -72,7 +72,7 @@ const CardHeader: React.FC<ICardHeader> = ({ className, children, backHref, onBa
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)}>
<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>
</Comp>
);

View File

@ -69,7 +69,7 @@ const Carousel: React.FC<ICarousel> = (props): JSX.Element => {
>
<Icon
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>
</div>
@ -101,7 +101,7 @@ const Carousel: React.FC<ICarousel> = (props): JSX.Element => {
>
<Icon
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>
</div>

View File

@ -9,7 +9,7 @@ const Checkbox = React.forwardRef<HTMLInputElement, ICheckbox>((props, ref) => {
{...props}
ref={ref}
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'
/>
);
});

View File

@ -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}`;
}

View File

@ -32,7 +32,7 @@ const EmojiButton: React.FC<IEmojiButton> = ({ emoji, className, onClick, tabInd
return (
<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>
);
};

View File

@ -17,7 +17,7 @@ const messages = defineMessages({
/** Possible theme names for an Input. */
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. */
autoFocus?: boolean;
/** The initial text in the input. */
@ -73,7 +73,7 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
>
{icon ? (
<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>
) : null}
@ -124,7 +124,7 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
>
<SvgIcon
src={revealed ? require('@tabler/icons/outline/eye-off.svg') : require('@tabler/icons/outline/eye.svg')}
className='h-4 w-4'
className='size-4'
/>
</button>
</div>

View File

@ -47,11 +47,11 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ language, setLanguage }
return (
<DropdownMenu items={newMenu} modal>
{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()}
</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>
);

View File

@ -118,7 +118,7 @@ const Modal = React.forwardRef<HTMLDivElement, IModal>(({
src={require('@tabler/icons/outline/arrow-left.svg')}
title={intl.formatMessage(messages.back)}
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}
title={intl.formatMessage(messages.close)}
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>

View File

@ -25,7 +25,7 @@ const RadioButton: React.FC<IRadioButton> = ({ name, value, checked, onChange, l
value={value}
checked={checked}
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'>

View File

@ -14,7 +14,7 @@ const Select = React.forwardRef<HTMLSelectElement, ISelect>((props, ref) => {
<select
ref={ref}
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,
{
'w-full': full,

View File

@ -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 -translate-y-1/2 rounded-full bg-accent-500' style={{ width: `${value * 100}%` }} />
<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}
style={{ left: `${value * 100}%` }}
/>

View File

@ -45,7 +45,7 @@ const TagInput: React.FC<ITagInput> = ({ tags, onChange, placeholder }) => {
return (
<div className='relative mt-1 grow shadow-sm'>
<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}
wrap
>

View File

@ -91,7 +91,7 @@ const Textarea = React.forwardRef(({
ref={ref}
rows={rows}
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':
theme === 'default',
'bg-transparent border-0 focus:border-0 focus:ring-0': theme === 'transparent',

View File

@ -43,7 +43,7 @@ const Toast = (props: IToast) => {
return (
<Icon
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
/>
);
@ -51,7 +51,7 @@ const Toast = (props: IToast) => {
return (
<Icon
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
/>
);
@ -59,7 +59,7 @@ const Toast = (props: IToast) => {
return (
<Icon
src={require('@tabler/icons/outline/alert-circle.svg')}
className='h-6 w-6 text-danger-600'
className='size-6 text-danger-600'
aria-hidden
/>
);
@ -143,7 +143,7 @@ const Toast = (props: IToast) => {
data-testid='toast-dismiss'
>
<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>
</div>
</HStack>

View File

@ -52,7 +52,7 @@ const Widget: React.FC<IWidget> = ({
<WidgetTitle title={title} />
{action || (onActionClick && (
<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}
onClick={onActionClick}
title={actionTitle}

View File

@ -14,7 +14,7 @@ const UploadProgress: React.FC<IUploadProgress> = ({ progress }) => {
<HStack alignItems='center' space={2}>
<Icon
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}>

View File

@ -148,7 +148,7 @@ const Upload: React.FC<IUpload> = ({
const uploadIcon = mediaType === 'unknown' && (
<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}
/>
);
@ -225,7 +225,7 @@ const Upload: React.FC<IUpload> = ({
'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' />
</span>
)}

View File

@ -68,7 +68,7 @@ const MediaItem: React.FC<IMediaItem> = ({ attachment, onOpenMedia }) => {
src={attachment.preview_url}
alt={attachment.description}
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) {

View File

@ -114,14 +114,14 @@ const Header: React.FC<IHeader> = ({ account }) => {
return (
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6'>
<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 className='px-4 sm:px-6'>
<HStack alignItems='bottom' space={5} className='-mt-12'>
<div className='relative flex'>
<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>
</HStack>
@ -660,7 +660,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
)}
<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()}
<div className='absolute left-2 top-2'>
@ -678,12 +678,12 @@ const Header: React.FC<IHeader> = ({ account }) => {
<Avatar
src={account.avatar}
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>
{account.verified && (
<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>

View File

@ -115,7 +115,7 @@ const Announcements: React.FC = () => {
emptyMessage={emptyMessage}
itemClassName='py-3 first:pt-0 last:pb-0'
isLoading={isLoading}
showLoading={isLoading && !announcements?.length}
showLoading={isLoading}
>
{announcements!.map((announcement) => (
<Announcement key={announcement.id} announcement={announcement} />

View File

@ -1,16 +1,13 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React, { useEffect, useState } from 'react';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
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 AccountContainer from 'soapbox/containers/account-container';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
const messages = defineMessages({
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 {
@ -20,18 +17,8 @@ interface ILatestAccountsPanel {
const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
const intl = useIntl();
const history = useHistory();
const dispatch = useAppDispatch();
const accountIds = useAppSelector<ImmutableOrderedSet<string>>((state) => state.admin.get('latestUsers').take(limit));
const [total, setTotal] = useState(accountIds.size);
useEffect(() => {
dispatch(fetchUsers(['local', 'active'], 1, null, limit))
.then((value) => {
setTotal((value as { count: number }).count);
})
.catch(() => {});
}, []);
const { accounts } = useAdminAccounts(['local', 'active'], limit);
const handleAction = () => {
history.push('/soapbox/admin/users');
@ -41,10 +28,9 @@ const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
<Widget
title={intl.formatMessage(messages.title)}
onActionClick={handleAction}
actionTitle={intl.formatMessage(messages.expand, { count: total })}
>
{accountIds.take(limit).map((account) => (
<AccountContainer key={account} id={account} withRelationship={false} withDate />
{accounts.slice(0, limit).map(account => (
<Account key={account.id} account={account} withRelationship={false} withDate />
))}
</Widget>
);

View File

@ -152,7 +152,7 @@ const Dashboard: React.FC = () => {
<span>{sourceCode.displayName} {sourceCode.version}</span>
<Icon
className='h-4 w-4'
className='size-4'
src={require('@tabler/icons/outline/external-link.svg')}
/>
</a>

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