Merge remote-tracking branch 'soapbox/develop' into follow-hashtags
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
commit
5aaf4d75af
|
@ -5,6 +5,7 @@ module.exports = {
|
||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
'plugin:import/typescript',
|
'plugin:import/typescript',
|
||||||
'plugin:compat/recommended',
|
'plugin:compat/recommended',
|
||||||
|
'plugin:tailwindcss/recommended',
|
||||||
],
|
],
|
||||||
|
|
||||||
env: {
|
env: {
|
||||||
|
@ -54,13 +55,17 @@ module.exports = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
polyfills: [
|
polyfills: [
|
||||||
'es:all',
|
'es:all', // core-js
|
||||||
'fetch',
|
'fetch', // not polyfilled, but ignore it
|
||||||
'IntersectionObserver',
|
'IntersectionObserver', // npm:intersection-observer
|
||||||
'Promise',
|
'Promise', // core-js
|
||||||
'URL',
|
'ResizeObserver', // npm:resize-observer-polyfill
|
||||||
'URLSearchParams',
|
'URL', // core-js
|
||||||
|
'URLSearchParams', // core-js
|
||||||
],
|
],
|
||||||
|
tailwindcss: {
|
||||||
|
config: 'tailwind.config.cjs',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
|
@ -235,18 +240,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'import/newline-after-import': 'error',
|
'import/newline-after-import': 'error',
|
||||||
'import/no-extraneous-dependencies': [
|
'import/no-extraneous-dependencies': 'error',
|
||||||
'error',
|
|
||||||
// {
|
|
||||||
// devDependencies: [
|
|
||||||
// 'webpack/**',
|
|
||||||
// 'app/soapbox/test_setup.js',
|
|
||||||
// 'app/soapbox/test_helpers.js',
|
|
||||||
// 'app/**/__tests__/**',
|
|
||||||
// 'app/**/__mocks__/**',
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
],
|
|
||||||
'import/no-unresolved': 'error',
|
'import/no-unresolved': 'error',
|
||||||
'import/no-webpack-loader-syntax': 'error',
|
'import/no-webpack-loader-syntax': 'error',
|
||||||
'import/order': [
|
'import/order': [
|
||||||
|
@ -267,10 +261,30 @@ module.exports = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'@typescript-eslint/no-duplicate-imports': 'error',
|
'@typescript-eslint/no-duplicate-imports': 'error',
|
||||||
|
'@typescript-eslint/member-delimiter-style': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
multiline: {
|
||||||
|
delimiter: 'none',
|
||||||
|
},
|
||||||
|
singleline: {
|
||||||
|
delimiter: 'comma',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
'promise/catch-or-return': 'error',
|
'promise/catch-or-return': 'error',
|
||||||
|
|
||||||
'react-hooks/rules-of-hooks': 'error',
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
|
|
||||||
|
'tailwindcss/classnames-order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
classRegex: '^(base|container|icon|item|list|outer|wrapper)?[c|C]lass(Name)?$',
|
||||||
|
config: 'tailwind.config.cjs',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'tailwindcss/migration-from-tailwind-2': 'error',
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
image: node:18
|
image: node:20
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
NODE_ENV: test
|
NODE_ENV: test
|
||||||
|
@ -149,19 +149,19 @@ pages:
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image: docker:20.10.22
|
image: docker:23.0.0
|
||||||
services:
|
services:
|
||||||
- docker:20.10.22-dind
|
- docker:23.0.0-dind
|
||||||
tags:
|
tags:
|
||||||
- dind
|
- 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
|
# 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:
|
script:
|
||||||
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin
|
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin
|
||||||
- docker build -t $CI_REGISTRY_IMAGE .
|
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG .
|
||||||
- docker push $CI_REGISTRY_IMAGE
|
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
|
||||||
only:
|
rules:
|
||||||
variables:
|
- if: $CI_COMMIT_TAG
|
||||||
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
|
interruptible: false
|
||||||
|
|
||||||
release:
|
release:
|
||||||
stage: release
|
stage: release
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import sharedConfig from '../webpack/shared';
|
||||||
|
|
||||||
|
import type { StorybookConfig } from '@storybook/core-common';
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: [
|
||||||
|
'../stories/**/*.stories.mdx',
|
||||||
|
'../stories/**/*.stories.@(js|jsx|ts|tsx)'
|
||||||
|
],
|
||||||
|
addons: [
|
||||||
|
'@storybook/addon-links',
|
||||||
|
'@storybook/addon-essentials',
|
||||||
|
'@storybook/addon-interactions',
|
||||||
|
'storybook-react-intl',
|
||||||
|
{
|
||||||
|
name: '@storybook/addon-postcss',
|
||||||
|
options: {
|
||||||
|
postcssLoaderOptions: {
|
||||||
|
implementation: require('postcss'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
framework: '@storybook/react',
|
||||||
|
core: {
|
||||||
|
builder: '@storybook/builder-webpack5',
|
||||||
|
},
|
||||||
|
webpackFinal: async (config) => {
|
||||||
|
config.resolve!.alias = {
|
||||||
|
...sharedConfig.resolve!.alias,
|
||||||
|
...config.resolve!.alias,
|
||||||
|
};
|
||||||
|
|
||||||
|
config.resolve!.modules = [
|
||||||
|
...sharedConfig.resolve!.modules!,
|
||||||
|
...config.resolve!.modules!,
|
||||||
|
];
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
|
@ -0,0 +1,22 @@
|
||||||
|
import '../app/styles/tailwind.css';
|
||||||
|
import '../stories/theme.css';
|
||||||
|
|
||||||
|
import { addDecorator, Story } from '@storybook/react';
|
||||||
|
import { IntlProvider } from 'react-intl';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const withProvider = (Story: Story) => (
|
||||||
|
<IntlProvider locale='en'><Story /></IntlProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
addDecorator(withProvider);
|
||||||
|
|
||||||
|
export const parameters = {
|
||||||
|
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -1 +1 @@
|
||||||
nodejs 18.13.0
|
nodejs 20.0.0
|
||||||
|
|
51
CHANGELOG.md
51
CHANGELOG.md
|
@ -10,12 +10,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Admin: redirect the homepage to any URL.
|
- Admin: redirect the homepage to any URL.
|
||||||
- Compatibility: added compatibility with Friendica.
|
- Compatibility: added compatibility with Friendica.
|
||||||
- Hashtags: let users follow hashtags (Mastodon).
|
- Hashtags: let users follow hashtags (Mastodon).
|
||||||
|
- Posts: Support posts filtering on recent Mastodon versions
|
||||||
|
- Reactions: Support custom emoji reactions
|
||||||
|
- Compatbility: Support Mastodon v2 timeline filters.
|
||||||
|
- Posts: Support dislikes on Friendica.
|
||||||
|
- UI: added a character counter to some textareas.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- Posts: truncate Nostr pubkeys in reply mentions.
|
||||||
|
- Posts: upgraded emoji picker component.
|
||||||
|
- Posts: improved design of threads.
|
||||||
|
- UI: unified design of "approve" and "reject" buttons in follow requests and waitlist.
|
||||||
|
- UI: added sticky column header.
|
||||||
|
- UI: add specific zones the user can drag-and-drop files.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- Posts: fixed emojis being cut off in reactions modal.
|
||||||
|
- Posts: fix audio player progress bar visibility.
|
||||||
|
- Posts: added missing gap in pending status.
|
||||||
|
- Compatibility: fixed quote posting compatibility with custom Pleroma forks.
|
||||||
|
- Profile: fix "load more" button height on account gallery page.
|
||||||
|
- 18n: fixed Chinese language being detected from the browser.
|
||||||
|
- Conversations: fixed pagination (Mastodon).
|
||||||
|
- Compatibility: fix version parsing for Friendica.
|
||||||
|
|
||||||
|
## [3.2.0] - 2023-02-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Admin: redirect the homepage to any URL.
|
||||||
|
- Compatibility: added compatibility with Friendica.
|
||||||
|
- Posts: bot badge on statuses from bot accounts.
|
||||||
|
- Compatibility: improved browser support for older browsers.
|
||||||
|
- Events: allow to repost events in event menu.
|
||||||
|
- Profile: Add RSS link to user profiles.
|
||||||
|
- Reactions: adds support for reacting to chat messages.
|
||||||
|
- Groups: initial support for groups.
|
||||||
|
- Profile: add RSS link to user profiles.
|
||||||
|
- Chats: reset chat message field height after sending a message.
|
||||||
|
- Admin: allow to manage announcements.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Chats: improved display of media attachments.
|
||||||
|
- ServiceWorker: switch to a network-first strategy. The "An update is available!" prompt goes away.
|
||||||
|
- Posts: increased font size of focused status in threads.
|
||||||
|
- Posts: let "mute conversation" be clicked from any feed, not just noficiations.
|
||||||
|
- Posts: display all emoji reactions.
|
||||||
|
- Reactions: improved UI of reactions on statuses.
|
||||||
|
- Profile: make verified badge more prominent, overlapping with avatar.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Admin: fixed hover card in reports modal shows reporter not reportee
|
||||||
|
- Chats: media attachments rendering at the wrong size and/or causing the chat to scroll on load.
|
||||||
|
- Chats: don't display "copy" button for messages without text.
|
||||||
- Posts: don't have to click the play button twice for embedded videos.
|
- Posts: don't have to click the play button twice for embedded videos.
|
||||||
- index.html: remove `referrer` meta tag so it doesn't conflict with backend's `Referrer-Policy` header.
|
- index.html: remove `referrer` meta tag so it doesn't conflict with backend's `Referrer-Policy` header.
|
||||||
|
- Modals: fix media modal automatically switching to video.
|
||||||
|
- Navigation: profile dropdown erratic behavior.
|
||||||
|
- Posts: fix posts filtering.
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
- Admin: single user mode. Now the homepage can be redirected to any URL.
|
- Admin: single user mode. Now the homepage can be redirected to any URL.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:18 as build
|
FROM node:20 as build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json .
|
COPY package.json .
|
||||||
COPY yarn.lock .
|
COPY yarn.lock .
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:18
|
FROM node:20
|
||||||
|
|
||||||
RUN apt-get update &&\
|
RUN apt-get update &&\
|
||||||
apt-get install -y inotify-tools &&\
|
apt-get install -y inotify-tools &&\
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"note": "patriots 900000001",
|
||||||
|
"discoverable": true,
|
||||||
|
"id": "109989480368015378",
|
||||||
|
"domain": null,
|
||||||
|
"avatar": "https://media.covfefe.social/groups/avatars/109/989/480/368/015/378/original/50b0d899bc5aae13.jpg",
|
||||||
|
"avatar_static": "https://media.covfefe.social/groups/avatars/109/989/480/368/015/378/original/50b0d899bc5aae13.jpg",
|
||||||
|
"header": "https://media.covfefe.social/groups/headers/109/989/480/368/015/378/original/c5063b59f919cd4a.png",
|
||||||
|
"header_static": "https://media.covfefe.social/groups/headers/109/989/480/368/015/378/original/c5063b59f919cd4a.png",
|
||||||
|
"group_visibility": "everyone",
|
||||||
|
"created_at": "2023-03-08T00:00:00.000Z",
|
||||||
|
"display_name": "PATRIOT PATRIOTS",
|
||||||
|
"membership_required": true,
|
||||||
|
"members_count": 1,
|
||||||
|
"tags": []
|
||||||
|
}
|
|
@ -228,7 +228,7 @@ const fetchAccountFail = (id: string | null, error: AxiosError) => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
type FollowAccountOpts = {
|
type FollowAccountOpts = {
|
||||||
reblogs?: boolean,
|
reblogs?: boolean
|
||||||
notify?: boolean
|
notify?: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
|
import { defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import { fetchRelationships } from 'soapbox/actions/accounts';
|
import { fetchRelationships } from 'soapbox/actions/accounts';
|
||||||
import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer';
|
import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer';
|
||||||
|
import toast from 'soapbox/toast';
|
||||||
import { filterBadges, getTagDiff } from 'soapbox/utils/badges';
|
import { filterBadges, getTagDiff } from 'soapbox/utils/badges';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
|
import { openModal } from './modals';
|
||||||
|
|
||||||
import type { AxiosResponse } from 'axios';
|
import type { AxiosResponse } from 'axios';
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
import type { APIEntity } from 'soapbox/types/entities';
|
import type { APIEntity, Announcement } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST';
|
const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST';
|
||||||
const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS';
|
const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS';
|
||||||
|
@ -87,6 +92,35 @@ const ADMIN_USER_INDEX_FETCH_SUCCESS = 'ADMIN_USER_INDEX_FETCH_SUCCESS';
|
||||||
|
|
||||||
const ADMIN_USER_INDEX_QUERY_SET = 'ADMIN_USER_INDEX_QUERY_SET';
|
const ADMIN_USER_INDEX_QUERY_SET = 'ADMIN_USER_INDEX_QUERY_SET';
|
||||||
|
|
||||||
|
const ADMIN_ANNOUNCEMENTS_FETCH_FAIL = 'ADMIN_ANNOUNCEMENTS_FETCH_FAILS';
|
||||||
|
const ADMIN_ANNOUNCEMENTS_FETCH_REQUEST = 'ADMIN_ANNOUNCEMENTS_FETCH_REQUEST';
|
||||||
|
const ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS = 'ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS';
|
||||||
|
|
||||||
|
const ADMIN_ANNOUNCEMENTS_EXPAND_FAIL = 'ADMIN_ANNOUNCEMENTS_EXPAND_FAILS';
|
||||||
|
const ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST = 'ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST';
|
||||||
|
const ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS = 'ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS';
|
||||||
|
|
||||||
|
const ADMIN_ANNOUNCEMENT_CHANGE_CONTENT = 'ADMIN_ANNOUNCEMENT_CHANGE_CONTENT';
|
||||||
|
const ADMIN_ANNOUNCEMENT_CHANGE_START_TIME = 'ADMIN_ANNOUNCEMENT_CHANGE_START_TIME';
|
||||||
|
const ADMIN_ANNOUNCEMENT_CHANGE_END_TIME = 'ADMIN_ANNOUNCEMENT_CHANGE_END_TIME';
|
||||||
|
const ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY = 'ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY';
|
||||||
|
|
||||||
|
const ADMIN_ANNOUNCEMENT_CREATE_REQUEST = 'ADMIN_ANNOUNCEMENT_CREATE_REQUEST';
|
||||||
|
const ADMIN_ANNOUNCEMENT_CREATE_SUCCESS = 'ADMIN_ANNOUNCEMENT_CREATE_REQUEST';
|
||||||
|
const ADMIN_ANNOUNCEMENT_CREATE_FAIL = 'ADMIN_ANNOUNCEMENT_CREATE_FAIL';
|
||||||
|
|
||||||
|
const ADMIN_ANNOUNCEMENT_DELETE_REQUEST = 'ADMIN_ANNOUNCEMENT_DELETE_REQUEST';
|
||||||
|
const ADMIN_ANNOUNCEMENT_DELETE_SUCCESS = 'ADMIN_ANNOUNCEMENT_DELETE_REQUEST';
|
||||||
|
const ADMIN_ANNOUNCEMENT_DELETE_FAIL = 'ADMIN_ANNOUNCEMENT_DELETE_FAIL';
|
||||||
|
|
||||||
|
const ADMIN_ANNOUNCEMENT_MODAL_INIT = 'ADMIN_ANNOUNCEMENT_MODAL_INIT';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
announcementCreateSuccess: { id: 'admin.edit_announcement.created', defaultMessage: 'Announcement created' },
|
||||||
|
announcementDeleteSuccess: { id: 'admin.edit_announcement.deleted', defaultMessage: 'Announcement deleted' },
|
||||||
|
announcementUpdateSuccess: { id: 'admin.edit_announcement.updated', defaultMessage: 'Announcement edited' },
|
||||||
|
});
|
||||||
|
|
||||||
const nicknamesFromIds = (getState: () => RootState, ids: string[]) => ids.map(id => getState().accounts.get(id)!.acct);
|
const nicknamesFromIds = (getState: () => RootState, ids: string[]) => ids.map(id => getState().accounts.get(id)!.acct);
|
||||||
|
|
||||||
const fetchConfig = () =>
|
const fetchConfig = () =>
|
||||||
|
@ -598,6 +632,93 @@ const expandUserIndex = () =>
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchAdminAnnouncements = () =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_REQUEST });
|
||||||
|
return api(getState)
|
||||||
|
.get('/api/pleroma/admin/announcements', { params: { limit: 50 } })
|
||||||
|
.then(({ data }) => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS, announcements: data });
|
||||||
|
return data;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_FAIL, error });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandAdminAnnouncements = () =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
const page = getState().admin_announcements.page;
|
||||||
|
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST });
|
||||||
|
return api(getState)
|
||||||
|
.get('/api/pleroma/admin/announcements', { params: { limit: 50, offset: page * 50 } })
|
||||||
|
.then(({ data }) => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS, announcements: data });
|
||||||
|
return data;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_FAIL, error });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeAnnouncementContent = (content: string) => ({
|
||||||
|
type: ADMIN_ANNOUNCEMENT_CHANGE_CONTENT,
|
||||||
|
value: content,
|
||||||
|
});
|
||||||
|
|
||||||
|
const changeAnnouncementStartTime = (time: Date | null) => ({
|
||||||
|
type: ADMIN_ANNOUNCEMENT_CHANGE_START_TIME,
|
||||||
|
value: time,
|
||||||
|
});
|
||||||
|
|
||||||
|
const changeAnnouncementEndTime = (time: Date | null) => ({
|
||||||
|
type: ADMIN_ANNOUNCEMENT_CHANGE_END_TIME,
|
||||||
|
value: time,
|
||||||
|
});
|
||||||
|
|
||||||
|
const changeAnnouncementAllDay = (allDay: boolean) => ({
|
||||||
|
type: ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY,
|
||||||
|
value: allDay,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCreateAnnouncement = () =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_REQUEST });
|
||||||
|
|
||||||
|
const { id, content, starts_at, ends_at, all_day } = getState().admin_announcements.form;
|
||||||
|
|
||||||
|
return api(getState)[id ? 'patch' : 'post'](
|
||||||
|
id ? `/api/pleroma/admin/announcements/${id}` : '/api/pleroma/admin/announcements',
|
||||||
|
{ content, starts_at, ends_at, all_day },
|
||||||
|
).then(({ data }) => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_SUCCESS, announcement: data });
|
||||||
|
toast.success(id ? messages.announcementUpdateSuccess : messages.announcementCreateSuccess);
|
||||||
|
dispatch(fetchAdminAnnouncements());
|
||||||
|
return data;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_FAIL, error });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAnnouncement = (id: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_REQUEST, id });
|
||||||
|
|
||||||
|
return api(getState).delete(`/api/pleroma/admin/announcements/${id}`).then(({ data }) => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_SUCCESS, id });
|
||||||
|
toast.success(messages.announcementDeleteSuccess);
|
||||||
|
dispatch(fetchAdminAnnouncements());
|
||||||
|
return data;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_FAIL, id, error });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const initAnnouncementModal = (announcement?: Announcement) =>
|
||||||
|
(dispatch: AppDispatch) => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENT_MODAL_INIT, announcement });
|
||||||
|
dispatch(openModal('EDIT_ANNOUNCEMENT'));
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ADMIN_CONFIG_FETCH_REQUEST,
|
ADMIN_CONFIG_FETCH_REQUEST,
|
||||||
ADMIN_CONFIG_FETCH_SUCCESS,
|
ADMIN_CONFIG_FETCH_SUCCESS,
|
||||||
|
@ -657,6 +778,23 @@ export {
|
||||||
ADMIN_USER_INDEX_FETCH_REQUEST,
|
ADMIN_USER_INDEX_FETCH_REQUEST,
|
||||||
ADMIN_USER_INDEX_FETCH_SUCCESS,
|
ADMIN_USER_INDEX_FETCH_SUCCESS,
|
||||||
ADMIN_USER_INDEX_QUERY_SET,
|
ADMIN_USER_INDEX_QUERY_SET,
|
||||||
|
ADMIN_ANNOUNCEMENTS_FETCH_FAIL,
|
||||||
|
ADMIN_ANNOUNCEMENTS_FETCH_REQUEST,
|
||||||
|
ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS,
|
||||||
|
ADMIN_ANNOUNCEMENTS_EXPAND_FAIL,
|
||||||
|
ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST,
|
||||||
|
ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS,
|
||||||
|
ADMIN_ANNOUNCEMENT_CHANGE_CONTENT,
|
||||||
|
ADMIN_ANNOUNCEMENT_CHANGE_START_TIME,
|
||||||
|
ADMIN_ANNOUNCEMENT_CHANGE_END_TIME,
|
||||||
|
ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY,
|
||||||
|
ADMIN_ANNOUNCEMENT_CREATE_FAIL,
|
||||||
|
ADMIN_ANNOUNCEMENT_CREATE_REQUEST,
|
||||||
|
ADMIN_ANNOUNCEMENT_CREATE_SUCCESS,
|
||||||
|
ADMIN_ANNOUNCEMENT_DELETE_FAIL,
|
||||||
|
ADMIN_ANNOUNCEMENT_DELETE_REQUEST,
|
||||||
|
ADMIN_ANNOUNCEMENT_DELETE_SUCCESS,
|
||||||
|
ADMIN_ANNOUNCEMENT_MODAL_INIT,
|
||||||
fetchConfig,
|
fetchConfig,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
updateSoapboxConfig,
|
updateSoapboxConfig,
|
||||||
|
@ -686,4 +824,13 @@ export {
|
||||||
setUserIndexQuery,
|
setUserIndexQuery,
|
||||||
fetchUserIndex,
|
fetchUserIndex,
|
||||||
expandUserIndex,
|
expandUserIndex,
|
||||||
|
fetchAdminAnnouncements,
|
||||||
|
expandAdminAnnouncements,
|
||||||
|
changeAnnouncementContent,
|
||||||
|
changeAnnouncementStartTime,
|
||||||
|
changeAnnouncementEndTime,
|
||||||
|
changeAnnouncementAllDay,
|
||||||
|
handleCreateAnnouncement,
|
||||||
|
deleteAnnouncement,
|
||||||
|
initAnnouncementModal,
|
||||||
};
|
};
|
||||||
|
|
|
@ -50,6 +50,7 @@ const customApp = custom('app');
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
loggedOut: { id: 'auth.logged_out', defaultMessage: 'Logged out.' },
|
loggedOut: { id: 'auth.logged_out', defaultMessage: 'Logged out.' },
|
||||||
|
awaitingApproval: { id: 'auth.awaiting_approval', defaultMessage: 'Your account is awaiting approval' },
|
||||||
invalidCredentials: { id: 'auth.invalid_credentials', defaultMessage: 'Wrong username or password' },
|
invalidCredentials: { id: 'auth.invalid_credentials', defaultMessage: 'Wrong username or password' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -187,6 +188,8 @@ export const logIn = (username: string, password: string) =>
|
||||||
if ((error.response?.data as any)?.error === 'mfa_required') {
|
if ((error.response?.data as any)?.error === 'mfa_required') {
|
||||||
// If MFA is required, throw the error and handle it in the component.
|
// If MFA is required, throw the error and handle it in the component.
|
||||||
throw error;
|
throw error;
|
||||||
|
} else if ((error.response?.data as any)?.identifier === 'awaiting_approval') {
|
||||||
|
toast.error(messages.awaitingApproval);
|
||||||
} else {
|
} else {
|
||||||
// Return "wrong password" message.
|
// Return "wrong password" message.
|
||||||
toast.error(messages.invalidCredentials);
|
toast.error(messages.invalidCredentials);
|
||||||
|
|
|
@ -4,7 +4,8 @@ import throttle from 'lodash/throttle';
|
||||||
import { defineMessages, IntlShape } from 'react-intl';
|
import { defineMessages, IntlShape } from 'react-intl';
|
||||||
|
|
||||||
import api from 'soapbox/api';
|
import api from 'soapbox/api';
|
||||||
import { search as emojiSearch } from 'soapbox/features/emoji/emoji-mart-search-light';
|
import { isNativeEmoji } from 'soapbox/features/emoji';
|
||||||
|
import emojiSearch from 'soapbox/features/emoji/search';
|
||||||
import { tagHistory } from 'soapbox/settings';
|
import { tagHistory } from 'soapbox/settings';
|
||||||
import toast from 'soapbox/toast';
|
import toast from 'soapbox/toast';
|
||||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||||
|
@ -19,8 +20,9 @@ import { openModal, closeModal } from './modals';
|
||||||
import { getSettings } from './settings';
|
import { getSettings } from './settings';
|
||||||
import { createStatus } from './statuses';
|
import { createStatus } from './statuses';
|
||||||
|
|
||||||
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
|
|
||||||
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
||||||
|
import type { Emoji } from 'soapbox/features/emoji';
|
||||||
|
import type { Group } from 'soapbox/schemas';
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
import type { Account, APIEntity, Status, Tag } from 'soapbox/types/entities';
|
import type { Account, APIEntity, Status, Tag } from 'soapbox/types/entities';
|
||||||
import type { History } from 'soapbox/types/history';
|
import type { History } from 'soapbox/types/history';
|
||||||
|
@ -46,6 +48,8 @@ const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
|
||||||
const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
|
const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
|
||||||
const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
|
const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
|
||||||
const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
|
const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
|
||||||
|
const COMPOSE_GROUP_POST = 'COMPOSE_GROUP_POST';
|
||||||
|
const COMPOSE_SET_GROUP_TIMELINE_VISIBLE = 'COMPOSE_SET_GROUP_TIMELINE_VISIBLE';
|
||||||
|
|
||||||
const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
|
const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
|
||||||
const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
|
const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
|
||||||
|
@ -86,7 +90,7 @@ const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' },
|
exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' },
|
||||||
exceededVideoSizeLimit: { id: 'upload_error.video_size_limit', defaultMessage: 'Video exceeds the current file size limit ({limit})' },
|
exceededVideoSizeLimit: { id: 'upload_error.video_size_limit', defaultMessage: 'Video exceeds the current file size limit ({limit})' },
|
||||||
exceededVideoDurationLimit: { id: 'upload_error.video_duration_limit', defaultMessage: 'Video exceeds the current duration limit ({limit} seconds)' },
|
exceededVideoDurationLimit: { id: 'upload_error.video_duration_limit', defaultMessage: 'Video exceeds the current duration limit ({limit, plural, one {# second} other {# seconds}})' },
|
||||||
scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' },
|
scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' },
|
||||||
success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent' },
|
success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent' },
|
||||||
editSuccess: { id: 'compose.edit_success', defaultMessage: 'Your post was edited' },
|
editSuccess: { id: 'compose.edit_success', defaultMessage: 'Your post was edited' },
|
||||||
|
@ -165,6 +169,14 @@ const cancelQuoteCompose = () => ({
|
||||||
id: 'compose-modal',
|
id: 'compose-modal',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const groupComposeModal = (group: Group) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
const composeId = `group:${group.id}`;
|
||||||
|
|
||||||
|
dispatch(groupCompose(composeId, group.id));
|
||||||
|
dispatch(openModal('COMPOSE', { composeId }));
|
||||||
|
};
|
||||||
|
|
||||||
const resetCompose = (composeId = 'compose-modal') => ({
|
const resetCompose = (composeId = 'compose-modal') => ({
|
||||||
type: COMPOSE_RESET,
|
type: COMPOSE_RESET,
|
||||||
id: composeId,
|
id: composeId,
|
||||||
|
@ -276,7 +288,7 @@ const submitCompose = (composeId: string, routerHistory?: History, force = false
|
||||||
|
|
||||||
const idempotencyKey = compose.idempotencyKey;
|
const idempotencyKey = compose.idempotencyKey;
|
||||||
|
|
||||||
const params = {
|
const params: Record<string, any> = {
|
||||||
status,
|
status,
|
||||||
in_reply_to_id: compose.in_reply_to,
|
in_reply_to_id: compose.in_reply_to,
|
||||||
quote_id: compose.quote,
|
quote_id: compose.quote,
|
||||||
|
@ -290,6 +302,11 @@ const submitCompose = (composeId: string, routerHistory?: History, force = false
|
||||||
to,
|
to,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (compose.privacy === 'group') {
|
||||||
|
params.group_id = compose.group_id;
|
||||||
|
params.group_timeline_visible = compose.group_timeline_visible; // Truth Social
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(createStatus(params, idempotencyKey, statusId)).then(function(data) {
|
dispatch(createStatus(params, idempotencyKey, statusId)).then(function(data) {
|
||||||
if (!statusId && data.visibility === 'direct' && getState().conversations.mounted <= 0 && routerHistory) {
|
if (!statusId && data.visibility === 'direct' && getState().conversations.mounted <= 0 && routerHistory) {
|
||||||
routerHistory.push('/messages');
|
routerHistory.push('/messages');
|
||||||
|
@ -470,6 +487,21 @@ const undoUploadCompose = (composeId: string, media_id: string) => ({
|
||||||
media_id: media_id,
|
media_id: media_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const groupCompose = (composeId: string, groupId: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch({
|
||||||
|
type: COMPOSE_GROUP_POST,
|
||||||
|
id: composeId,
|
||||||
|
group_id: groupId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setGroupTimelineVisible = (composeId: string, groupTimelineVisible: boolean) => ({
|
||||||
|
type: COMPOSE_SET_GROUP_TIMELINE_VISIBLE,
|
||||||
|
id: composeId,
|
||||||
|
groupTimelineVisible,
|
||||||
|
});
|
||||||
|
|
||||||
const clearComposeSuggestions = (composeId: string) => {
|
const clearComposeSuggestions = (composeId: string) => {
|
||||||
if (cancelFetchComposeSuggestionsAccounts) {
|
if (cancelFetchComposeSuggestionsAccounts) {
|
||||||
cancelFetchComposeSuggestionsAccounts();
|
cancelFetchComposeSuggestionsAccounts();
|
||||||
|
@ -504,7 +536,9 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId,
|
||||||
}, 200, { leading: true, trailing: true });
|
}, 200, { leading: true, trailing: true });
|
||||||
|
|
||||||
const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => {
|
const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => {
|
||||||
const results = emojiSearch(token.replace(':', ''), { maxResults: 5 } as any);
|
const state = getState();
|
||||||
|
const results = emojiSearch(token.replace(':', ''), { maxResults: 5 }, state.custom_emojis);
|
||||||
|
|
||||||
dispatch(readyComposeSuggestionsEmojis(composeId, token, results));
|
dispatch(readyComposeSuggestionsEmojis(composeId, token, results));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -549,7 +583,7 @@ const selectComposeSuggestion = (composeId: string, position: number, token: str
|
||||||
let completion, startPosition;
|
let completion, startPosition;
|
||||||
|
|
||||||
if (typeof suggestion === 'object' && suggestion.id) {
|
if (typeof suggestion === 'object' && suggestion.id) {
|
||||||
completion = suggestion.native || suggestion.colons;
|
completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons;
|
||||||
startPosition = position - 1;
|
startPosition = position - 1;
|
||||||
|
|
||||||
dispatch(useEmoji(suggestion));
|
dispatch(useEmoji(suggestion));
|
||||||
|
@ -722,7 +756,7 @@ const eventDiscussionCompose = (composeId: string, status: Status) =>
|
||||||
const instance = state.instance;
|
const instance = state.instance;
|
||||||
const { explicitAddressing } = getFeatures(instance);
|
const { explicitAddressing } = getFeatures(instance);
|
||||||
|
|
||||||
dispatch({
|
return dispatch({
|
||||||
type: COMPOSE_EVENT_REPLY,
|
type: COMPOSE_EVENT_REPLY,
|
||||||
id: composeId,
|
id: composeId,
|
||||||
status: status,
|
status: status,
|
||||||
|
@ -749,6 +783,7 @@ export {
|
||||||
COMPOSE_UPLOAD_FAIL,
|
COMPOSE_UPLOAD_FAIL,
|
||||||
COMPOSE_UPLOAD_PROGRESS,
|
COMPOSE_UPLOAD_PROGRESS,
|
||||||
COMPOSE_UPLOAD_UNDO,
|
COMPOSE_UPLOAD_UNDO,
|
||||||
|
COMPOSE_GROUP_POST,
|
||||||
COMPOSE_SUGGESTIONS_CLEAR,
|
COMPOSE_SUGGESTIONS_CLEAR,
|
||||||
COMPOSE_SUGGESTIONS_READY,
|
COMPOSE_SUGGESTIONS_READY,
|
||||||
COMPOSE_SUGGESTION_SELECT,
|
COMPOSE_SUGGESTION_SELECT,
|
||||||
|
@ -776,6 +811,7 @@ export {
|
||||||
COMPOSE_ADD_TO_MENTIONS,
|
COMPOSE_ADD_TO_MENTIONS,
|
||||||
COMPOSE_REMOVE_FROM_MENTIONS,
|
COMPOSE_REMOVE_FROM_MENTIONS,
|
||||||
COMPOSE_SET_STATUS,
|
COMPOSE_SET_STATUS,
|
||||||
|
COMPOSE_SET_GROUP_TIMELINE_VISIBLE,
|
||||||
setComposeToStatus,
|
setComposeToStatus,
|
||||||
changeCompose,
|
changeCompose,
|
||||||
replyCompose,
|
replyCompose,
|
||||||
|
@ -801,6 +837,9 @@ export {
|
||||||
uploadComposeSuccess,
|
uploadComposeSuccess,
|
||||||
uploadComposeFail,
|
uploadComposeFail,
|
||||||
undoUploadCompose,
|
undoUploadCompose,
|
||||||
|
groupCompose,
|
||||||
|
groupComposeModal,
|
||||||
|
setGroupTimelineVisible,
|
||||||
clearComposeSuggestions,
|
clearComposeSuggestions,
|
||||||
fetchComposeSuggestions,
|
fetchComposeSuggestions,
|
||||||
readyComposeSuggestionsEmojis,
|
readyComposeSuggestionsEmojis,
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
import type { DropdownPlacement } from 'soapbox/components/dropdown-menu';
|
|
||||||
|
|
||||||
const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
|
const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
|
||||||
const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
|
const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
|
||||||
|
|
||||||
const openDropdownMenu = (id: number, placement: DropdownPlacement, keyboard: boolean) =>
|
const openDropdownMenu = () => ({ type: DROPDOWN_MENU_OPEN });
|
||||||
({ type: DROPDOWN_MENU_OPEN, id, placement, keyboard });
|
const closeDropdownMenu = () => ({ type: DROPDOWN_MENU_CLOSE });
|
||||||
|
|
||||||
const closeDropdownMenu = (id: number) =>
|
|
||||||
({ type: DROPDOWN_MENU_CLOSE, id });
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DROPDOWN_MENU_OPEN,
|
DROPDOWN_MENU_OPEN,
|
||||||
|
|
|
@ -25,7 +25,7 @@ const EMOJI_REACTS_FETCH_FAIL = 'EMOJI_REACTS_FETCH_FAIL';
|
||||||
|
|
||||||
const noOp = () => () => new Promise(f => f(undefined));
|
const noOp = () => () => new Promise(f => f(undefined));
|
||||||
|
|
||||||
const simpleEmojiReact = (status: Status, emoji: string) =>
|
const simpleEmojiReact = (status: Status, emoji: string, custom?: string) =>
|
||||||
(dispatch: AppDispatch) => {
|
(dispatch: AppDispatch) => {
|
||||||
const emojiReacts: ImmutableList<ImmutableMap<string, any>> = status.pleroma.get('emoji_reactions') || ImmutableList();
|
const emojiReacts: ImmutableList<ImmutableMap<string, any>> = status.pleroma.get('emoji_reactions') || ImmutableList();
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ const simpleEmojiReact = (status: Status, emoji: string) =>
|
||||||
if (emoji === '👍') {
|
if (emoji === '👍') {
|
||||||
dispatch(favourite(status));
|
dispatch(favourite(status));
|
||||||
} else {
|
} else {
|
||||||
dispatch(emojiReact(status, emoji));
|
dispatch(emojiReact(status, emoji, custom));
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
@ -70,11 +70,11 @@ const fetchEmojiReacts = (id: string, emoji: string) =>
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const emojiReact = (status: Status, emoji: string) =>
|
const emojiReact = (status: Status, emoji: string, custom?: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
if (!isLoggedIn(getState)) return dispatch(noOp());
|
if (!isLoggedIn(getState)) return dispatch(noOp());
|
||||||
|
|
||||||
dispatch(emojiReactRequest(status, emoji));
|
dispatch(emojiReactRequest(status, emoji, custom));
|
||||||
|
|
||||||
return api(getState)
|
return api(getState)
|
||||||
.put(`/api/v1/pleroma/statuses/${status.get('id')}/reactions/${emoji}`)
|
.put(`/api/v1/pleroma/statuses/${status.get('id')}/reactions/${emoji}`)
|
||||||
|
@ -120,10 +120,11 @@ const fetchEmojiReactsFail = (id: string, error: AxiosError) => ({
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emojiReactRequest = (status: Status, emoji: string) => ({
|
const emojiReactRequest = (status: Status, emoji: string, custom?: string) => ({
|
||||||
type: EMOJI_REACT_REQUEST,
|
type: EMOJI_REACT_REQUEST,
|
||||||
status,
|
status,
|
||||||
emoji,
|
emoji,
|
||||||
|
custom,
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { saveSettings } from './settings';
|
import { saveSettings } from './settings';
|
||||||
|
|
||||||
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
|
import type { Emoji } from 'soapbox/features/emoji';
|
||||||
import type { AppDispatch } from 'soapbox/store';
|
import type { AppDispatch } from 'soapbox/store';
|
||||||
|
|
||||||
const EMOJI_USE = 'EMOJI_USE';
|
const EMOJI_USE = 'EMOJI_USE';
|
||||||
|
|
|
@ -569,7 +569,7 @@ const rejectEventParticipationRequestFail = (id: string, accountId: string, erro
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchEventIcs = (id: string) =>
|
const fetchEventIcs = (id: string) =>
|
||||||
(dispatch: any, getState: () => RootState) =>
|
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||||
api(getState).get(`/api/v1/pleroma/events/${id}/ics`);
|
api(getState).get(`/api/v1/pleroma/events/${id}/ics`);
|
||||||
|
|
||||||
const cancelEventCompose = () => ({
|
const cancelEventCompose = () => ({
|
||||||
|
|
|
@ -34,8 +34,8 @@ type ExportDataActions = {
|
||||||
| typeof EXPORT_BLOCKS_FAIL
|
| typeof EXPORT_BLOCKS_FAIL
|
||||||
| typeof EXPORT_MUTES_REQUEST
|
| typeof EXPORT_MUTES_REQUEST
|
||||||
| typeof EXPORT_MUTES_SUCCESS
|
| typeof EXPORT_MUTES_SUCCESS
|
||||||
| typeof EXPORT_MUTES_FAIL,
|
| typeof EXPORT_MUTES_FAIL
|
||||||
error?: any,
|
error?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileExport(content: string, fileName: string) {
|
function fileExport(content: string, fileName: string) {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import sourceCode from 'soapbox/utils/code';
|
||||||
import { getWalletAndSign } from 'soapbox/utils/ethereum';
|
import { getWalletAndSign } from 'soapbox/utils/ethereum';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
import { getQuirks } from 'soapbox/utils/quirks';
|
import { getQuirks } from 'soapbox/utils/quirks';
|
||||||
import { getScopes } from 'soapbox/utils/scopes';
|
import { getInstanceScopes } from 'soapbox/utils/scopes';
|
||||||
|
|
||||||
import { baseClient } from '../api';
|
import { baseClient } from '../api';
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ const fetchExternalInstance = (baseURL?: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createExternalApp = (instance: Instance, baseURL?: string) =>
|
const createExternalApp = (instance: Instance, baseURL?: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, _getState: () => RootState) => {
|
||||||
// Mitra: skip creating the auth app
|
// Mitra: skip creating the auth app
|
||||||
if (getQuirks(instance).noApps) return new Promise(f => f({}));
|
if (getQuirks(instance).noApps) return new Promise(f => f({}));
|
||||||
|
|
||||||
|
@ -46,15 +46,15 @@ const createExternalApp = (instance: Instance, baseURL?: string) =>
|
||||||
client_name: sourceCode.displayName,
|
client_name: sourceCode.displayName,
|
||||||
redirect_uris: `${window.location.origin}/login/external`,
|
redirect_uris: `${window.location.origin}/login/external`,
|
||||||
website: sourceCode.homepage,
|
website: sourceCode.homepage,
|
||||||
scopes: getScopes(getState()),
|
scopes: getInstanceScopes(instance),
|
||||||
};
|
};
|
||||||
|
|
||||||
return dispatch(createApp(params, baseURL));
|
return dispatch(createApp(params, baseURL));
|
||||||
};
|
};
|
||||||
|
|
||||||
const externalAuthorize = (instance: Instance, baseURL: string) =>
|
const externalAuthorize = (instance: Instance, baseURL: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, _getState: () => RootState) => {
|
||||||
const scopes = getScopes(getState());
|
const scopes = getInstanceScopes(instance);
|
||||||
|
|
||||||
return dispatch(createExternalApp(instance, baseURL)).then((app) => {
|
return dispatch(createExternalApp(instance, baseURL)).then((app) => {
|
||||||
const { client_id, redirect_uri } = app as Record<string, string>;
|
const { client_id, redirect_uri } = app as Record<string, string>;
|
||||||
|
@ -88,7 +88,7 @@ const externalEthereumLogin = (instance: Instance, baseURL?: string) =>
|
||||||
client_secret: client_secret,
|
client_secret: client_secret,
|
||||||
password: signature as string,
|
password: signature as string,
|
||||||
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||||
scope: getScopes(getState()),
|
scope: getInstanceScopes(instance),
|
||||||
};
|
};
|
||||||
|
|
||||||
return dispatch(obtainOAuthToken(params, baseURL))
|
return dispatch(obtainOAuthToken(params, baseURL))
|
||||||
|
|
|
@ -11,25 +11,25 @@ export const FAMILIAR_FOLLOWERS_FETCH_SUCCESS = 'FAMILIAR_FOLLOWERS_FETCH_SUCCES
|
||||||
export const FAMILIAR_FOLLOWERS_FETCH_FAIL = 'FAMILIAR_FOLLOWERS_FETCH_FAIL';
|
export const FAMILIAR_FOLLOWERS_FETCH_FAIL = 'FAMILIAR_FOLLOWERS_FETCH_FAIL';
|
||||||
|
|
||||||
type FamiliarFollowersFetchRequestAction = {
|
type FamiliarFollowersFetchRequestAction = {
|
||||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_REQUEST,
|
type: typeof FAMILIAR_FOLLOWERS_FETCH_REQUEST
|
||||||
id: string,
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type FamiliarFollowersFetchRequestSuccessAction = {
|
type FamiliarFollowersFetchRequestSuccessAction = {
|
||||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_SUCCESS,
|
type: typeof FAMILIAR_FOLLOWERS_FETCH_SUCCESS
|
||||||
id: string,
|
id: string
|
||||||
accounts: Array<APIEntity>,
|
accounts: Array<APIEntity>
|
||||||
}
|
}
|
||||||
|
|
||||||
type FamiliarFollowersFetchRequestFailAction = {
|
type FamiliarFollowersFetchRequestFailAction = {
|
||||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_FAIL,
|
type: typeof FAMILIAR_FOLLOWERS_FETCH_FAIL
|
||||||
id: string,
|
id: string
|
||||||
error: any,
|
error: any
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccountsImportAction = {
|
type AccountsImportAction = {
|
||||||
type: typeof ACCOUNTS_IMPORT,
|
type: typeof ACCOUNTS_IMPORT
|
||||||
accounts: Array<APIEntity>,
|
accounts: Array<APIEntity>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FamiliarFollowersActions = FamiliarFollowersFetchRequestAction | FamiliarFollowersFetchRequestSuccessAction | FamiliarFollowersFetchRequestFailAction | AccountsImportAction
|
export type FamiliarFollowersActions = FamiliarFollowersFetchRequestAction | FamiliarFollowersFetchRequestSuccessAction | FamiliarFollowersFetchRequestFailAction | AccountsImportAction
|
||||||
|
|
|
@ -12,10 +12,18 @@ const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST';
|
||||||
const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
|
const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
|
||||||
const FILTERS_FETCH_FAIL = 'FILTERS_FETCH_FAIL';
|
const FILTERS_FETCH_FAIL = 'FILTERS_FETCH_FAIL';
|
||||||
|
|
||||||
|
const FILTER_FETCH_REQUEST = 'FILTER_FETCH_REQUEST';
|
||||||
|
const FILTER_FETCH_SUCCESS = 'FILTER_FETCH_SUCCESS';
|
||||||
|
const FILTER_FETCH_FAIL = 'FILTER_FETCH_FAIL';
|
||||||
|
|
||||||
const FILTERS_CREATE_REQUEST = 'FILTERS_CREATE_REQUEST';
|
const FILTERS_CREATE_REQUEST = 'FILTERS_CREATE_REQUEST';
|
||||||
const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS';
|
const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS';
|
||||||
const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL';
|
const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL';
|
||||||
|
|
||||||
|
const FILTERS_UPDATE_REQUEST = 'FILTERS_UPDATE_REQUEST';
|
||||||
|
const FILTERS_UPDATE_SUCCESS = 'FILTERS_UPDATE_SUCCESS';
|
||||||
|
const FILTERS_UPDATE_FAIL = 'FILTERS_UPDATE_FAIL';
|
||||||
|
|
||||||
const FILTERS_DELETE_REQUEST = 'FILTERS_DELETE_REQUEST';
|
const FILTERS_DELETE_REQUEST = 'FILTERS_DELETE_REQUEST';
|
||||||
const FILTERS_DELETE_SUCCESS = 'FILTERS_DELETE_SUCCESS';
|
const FILTERS_DELETE_SUCCESS = 'FILTERS_DELETE_SUCCESS';
|
||||||
const FILTERS_DELETE_FAIL = 'FILTERS_DELETE_FAIL';
|
const FILTERS_DELETE_FAIL = 'FILTERS_DELETE_FAIL';
|
||||||
|
@ -25,22 +33,16 @@ const messages = defineMessages({
|
||||||
removed: { id: 'filters.removed', defaultMessage: 'Filter deleted.' },
|
removed: { id: 'filters.removed', defaultMessage: 'Filter deleted.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchFilters = () =>
|
type FilterKeywords = { keyword: string, whole_word: boolean }[];
|
||||||
|
|
||||||
|
const fetchFiltersV1 = () =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
|
|
||||||
const state = getState();
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
if (!features.filters) return;
|
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: FILTERS_FETCH_REQUEST,
|
type: FILTERS_FETCH_REQUEST,
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
api(getState)
|
return api(getState)
|
||||||
.get('/api/v1/filters')
|
.get('/api/v1/filters')
|
||||||
.then(({ data }) => dispatch({
|
.then(({ data }) => dispatch({
|
||||||
type: FILTERS_FETCH_SUCCESS,
|
type: FILTERS_FETCH_SUCCESS,
|
||||||
|
@ -55,15 +57,105 @@ const fetchFilters = () =>
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const createFilter = (phrase: string, expires_at: string, context: Array<string>, whole_word: boolean, irreversible: boolean) =>
|
const fetchFiltersV2 = () =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch({
|
||||||
|
type: FILTERS_FETCH_REQUEST,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return api(getState)
|
||||||
|
.get('/api/v2/filters')
|
||||||
|
.then(({ data }) => dispatch({
|
||||||
|
type: FILTERS_FETCH_SUCCESS,
|
||||||
|
filters: data,
|
||||||
|
skipLoading: true,
|
||||||
|
}))
|
||||||
|
.catch(err => dispatch({
|
||||||
|
type: FILTERS_FETCH_FAIL,
|
||||||
|
err,
|
||||||
|
skipLoading: true,
|
||||||
|
skipAlert: true,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchFilters = (fromFiltersPage = false) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
if (!isLoggedIn(getState)) return;
|
||||||
|
|
||||||
|
const state = getState();
|
||||||
|
const instance = state.instance;
|
||||||
|
const features = getFeatures(instance);
|
||||||
|
|
||||||
|
if (features.filtersV2 && fromFiltersPage) return dispatch(fetchFiltersV2());
|
||||||
|
|
||||||
|
if (features.filters) return dispatch(fetchFiltersV1());
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchFilterV1 = (id: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch({
|
||||||
|
type: FILTER_FETCH_REQUEST,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return api(getState)
|
||||||
|
.get(`/api/v1/filters/${id}`)
|
||||||
|
.then(({ data }) => dispatch({
|
||||||
|
type: FILTER_FETCH_SUCCESS,
|
||||||
|
filter: data,
|
||||||
|
skipLoading: true,
|
||||||
|
}))
|
||||||
|
.catch(err => dispatch({
|
||||||
|
type: FILTER_FETCH_FAIL,
|
||||||
|
err,
|
||||||
|
skipLoading: true,
|
||||||
|
skipAlert: true,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchFilterV2 = (id: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch({
|
||||||
|
type: FILTER_FETCH_REQUEST,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return api(getState)
|
||||||
|
.get(`/api/v2/filters/${id}`)
|
||||||
|
.then(({ data }) => dispatch({
|
||||||
|
type: FILTER_FETCH_SUCCESS,
|
||||||
|
filter: data,
|
||||||
|
skipLoading: true,
|
||||||
|
}))
|
||||||
|
.catch(err => dispatch({
|
||||||
|
type: FILTER_FETCH_FAIL,
|
||||||
|
err,
|
||||||
|
skipLoading: true,
|
||||||
|
skipAlert: true,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchFilter = (id: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
const state = getState();
|
||||||
|
const instance = state.instance;
|
||||||
|
const features = getFeatures(instance);
|
||||||
|
|
||||||
|
if (features.filtersV2) return dispatch(fetchFilterV2(id));
|
||||||
|
|
||||||
|
if (features.filters) return dispatch(fetchFilterV1(id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFilterV1 = (title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
dispatch({ type: FILTERS_CREATE_REQUEST });
|
dispatch({ type: FILTERS_CREATE_REQUEST });
|
||||||
return api(getState).post('/api/v1/filters', {
|
return api(getState).post('/api/v1/filters', {
|
||||||
phrase,
|
phrase: keywords[0].keyword,
|
||||||
context,
|
context,
|
||||||
irreversible,
|
irreversible: hide,
|
||||||
whole_word,
|
whole_word: keywords[0].whole_word,
|
||||||
expires_at,
|
expires_in,
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
|
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
|
||||||
toast.success(messages.added);
|
toast.success(messages.added);
|
||||||
|
@ -72,7 +164,80 @@ const createFilter = (phrase: string, expires_at: string, context: Array<string>
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteFilter = (id: string) =>
|
const createFilterV2 = (title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords_attributes: FilterKeywords) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch({ type: FILTERS_CREATE_REQUEST });
|
||||||
|
return api(getState).post('/api/v2/filters', {
|
||||||
|
title,
|
||||||
|
context,
|
||||||
|
filter_action: hide ? 'hide' : 'warn',
|
||||||
|
expires_in,
|
||||||
|
keywords_attributes,
|
||||||
|
}).then(response => {
|
||||||
|
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
|
||||||
|
toast.success(messages.added);
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: FILTERS_CREATE_FAIL, error });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFilter = (title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
const state = getState();
|
||||||
|
const instance = state.instance;
|
||||||
|
const features = getFeatures(instance);
|
||||||
|
|
||||||
|
if (features.filtersV2) return dispatch(createFilterV2(title, expires_in, context, hide, keywords));
|
||||||
|
|
||||||
|
return dispatch(createFilterV1(title, expires_in, context, hide, keywords));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFilterV1 = (id: string, title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch({ type: FILTERS_UPDATE_REQUEST });
|
||||||
|
return api(getState).patch(`/api/v1/filters/${id}`, {
|
||||||
|
phrase: keywords[0].keyword,
|
||||||
|
context,
|
||||||
|
irreversible: hide,
|
||||||
|
whole_word: keywords[0].whole_word,
|
||||||
|
expires_in,
|
||||||
|
}).then(response => {
|
||||||
|
dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response.data });
|
||||||
|
toast.success(messages.added);
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: FILTERS_UPDATE_FAIL, error });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFilterV2 = (id: string, title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords_attributes: FilterKeywords) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch({ type: FILTERS_UPDATE_REQUEST });
|
||||||
|
return api(getState).patch(`/api/v2/filters/${id}`, {
|
||||||
|
title,
|
||||||
|
context,
|
||||||
|
filter_action: hide ? 'hide' : 'warn',
|
||||||
|
expires_in,
|
||||||
|
keywords_attributes,
|
||||||
|
}).then(response => {
|
||||||
|
dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response.data });
|
||||||
|
toast.success(messages.added);
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: FILTERS_UPDATE_FAIL, error });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFilter = (id: string, title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
const state = getState();
|
||||||
|
const instance = state.instance;
|
||||||
|
const features = getFeatures(instance);
|
||||||
|
|
||||||
|
if (features.filtersV2) return dispatch(updateFilterV2(id, title, expires_in, context, hide, keywords));
|
||||||
|
|
||||||
|
return dispatch(updateFilterV1(id, title, expires_in, context, hide, keywords));
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteFilterV1 = (id: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
dispatch({ type: FILTERS_DELETE_REQUEST });
|
dispatch({ type: FILTERS_DELETE_REQUEST });
|
||||||
return api(getState).delete(`/api/v1/filters/${id}`).then(response => {
|
return api(getState).delete(`/api/v1/filters/${id}`).then(response => {
|
||||||
|
@ -83,17 +248,47 @@ const deleteFilter = (id: string) =>
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteFilterV2 = (id: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch({ type: FILTERS_DELETE_REQUEST });
|
||||||
|
return api(getState).delete(`/api/v2/filters/${id}`).then(response => {
|
||||||
|
dispatch({ type: FILTERS_DELETE_SUCCESS, filter: response.data });
|
||||||
|
toast.success(messages.removed);
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: FILTERS_DELETE_FAIL, error });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteFilter = (id: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
const state = getState();
|
||||||
|
const instance = state.instance;
|
||||||
|
const features = getFeatures(instance);
|
||||||
|
|
||||||
|
if (features.filtersV2) return dispatch(deleteFilterV2(id));
|
||||||
|
|
||||||
|
return dispatch(deleteFilterV1(id));
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
FILTERS_FETCH_REQUEST,
|
FILTERS_FETCH_REQUEST,
|
||||||
FILTERS_FETCH_SUCCESS,
|
FILTERS_FETCH_SUCCESS,
|
||||||
FILTERS_FETCH_FAIL,
|
FILTERS_FETCH_FAIL,
|
||||||
|
FILTER_FETCH_REQUEST,
|
||||||
|
FILTER_FETCH_SUCCESS,
|
||||||
|
FILTER_FETCH_FAIL,
|
||||||
FILTERS_CREATE_REQUEST,
|
FILTERS_CREATE_REQUEST,
|
||||||
FILTERS_CREATE_SUCCESS,
|
FILTERS_CREATE_SUCCESS,
|
||||||
FILTERS_CREATE_FAIL,
|
FILTERS_CREATE_FAIL,
|
||||||
|
FILTERS_UPDATE_REQUEST,
|
||||||
|
FILTERS_UPDATE_SUCCESS,
|
||||||
|
FILTERS_UPDATE_FAIL,
|
||||||
FILTERS_DELETE_REQUEST,
|
FILTERS_DELETE_REQUEST,
|
||||||
FILTERS_DELETE_SUCCESS,
|
FILTERS_DELETE_SUCCESS,
|
||||||
FILTERS_DELETE_FAIL,
|
FILTERS_DELETE_FAIL,
|
||||||
fetchFilters,
|
fetchFilters,
|
||||||
|
fetchFilter,
|
||||||
createFilter,
|
createFilter,
|
||||||
|
updateFilter,
|
||||||
deleteFilter,
|
deleteFilter,
|
||||||
};
|
};
|
|
@ -0,0 +1,794 @@
|
||||||
|
import { deleteEntities } from 'soapbox/entity-store/actions';
|
||||||
|
|
||||||
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
|
import { fetchRelationships } from './accounts';
|
||||||
|
import { importFetchedGroups, importFetchedAccounts } from './importer';
|
||||||
|
import { deleteFromTimelines } from './timelines';
|
||||||
|
|
||||||
|
import type { AxiosError } from 'axios';
|
||||||
|
import type { GroupRole } from 'soapbox/reducers/group-memberships';
|
||||||
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
|
import type { APIEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
const GROUP_CREATE_REQUEST = 'GROUP_CREATE_REQUEST';
|
||||||
|
const GROUP_CREATE_SUCCESS = 'GROUP_CREATE_SUCCESS';
|
||||||
|
const GROUP_CREATE_FAIL = 'GROUP_CREATE_FAIL';
|
||||||
|
|
||||||
|
const GROUP_UPDATE_REQUEST = 'GROUP_UPDATE_REQUEST';
|
||||||
|
const GROUP_UPDATE_SUCCESS = 'GROUP_UPDATE_SUCCESS';
|
||||||
|
const GROUP_UPDATE_FAIL = 'GROUP_UPDATE_FAIL';
|
||||||
|
|
||||||
|
const GROUP_DELETE_REQUEST = 'GROUP_DELETE_REQUEST';
|
||||||
|
const GROUP_DELETE_SUCCESS = 'GROUP_DELETE_SUCCESS';
|
||||||
|
const GROUP_DELETE_FAIL = 'GROUP_DELETE_FAIL';
|
||||||
|
|
||||||
|
const GROUP_FETCH_REQUEST = 'GROUP_FETCH_REQUEST';
|
||||||
|
const GROUP_FETCH_SUCCESS = 'GROUP_FETCH_SUCCESS';
|
||||||
|
const GROUP_FETCH_FAIL = 'GROUP_FETCH_FAIL';
|
||||||
|
|
||||||
|
const GROUPS_FETCH_REQUEST = 'GROUPS_FETCH_REQUEST';
|
||||||
|
const GROUPS_FETCH_SUCCESS = 'GROUPS_FETCH_SUCCESS';
|
||||||
|
const GROUPS_FETCH_FAIL = 'GROUPS_FETCH_FAIL';
|
||||||
|
|
||||||
|
const GROUP_RELATIONSHIPS_FETCH_REQUEST = 'GROUP_RELATIONSHIPS_FETCH_REQUEST';
|
||||||
|
const GROUP_RELATIONSHIPS_FETCH_SUCCESS = 'GROUP_RELATIONSHIPS_FETCH_SUCCESS';
|
||||||
|
const GROUP_RELATIONSHIPS_FETCH_FAIL = 'GROUP_RELATIONSHIPS_FETCH_FAIL';
|
||||||
|
|
||||||
|
const GROUP_DELETE_STATUS_REQUEST = 'GROUP_DELETE_STATUS_REQUEST';
|
||||||
|
const GROUP_DELETE_STATUS_SUCCESS = 'GROUP_DELETE_STATUS_SUCCESS';
|
||||||
|
const GROUP_DELETE_STATUS_FAIL = 'GROUP_DELETE_STATUS_FAIL';
|
||||||
|
|
||||||
|
const GROUP_KICK_REQUEST = 'GROUP_KICK_REQUEST';
|
||||||
|
const GROUP_KICK_SUCCESS = 'GROUP_KICK_SUCCESS';
|
||||||
|
const GROUP_KICK_FAIL = 'GROUP_KICK_FAIL';
|
||||||
|
|
||||||
|
const GROUP_BLOCKS_FETCH_REQUEST = 'GROUP_BLOCKS_FETCH_REQUEST';
|
||||||
|
const GROUP_BLOCKS_FETCH_SUCCESS = 'GROUP_BLOCKS_FETCH_SUCCESS';
|
||||||
|
const GROUP_BLOCKS_FETCH_FAIL = 'GROUP_BLOCKS_FETCH_FAIL';
|
||||||
|
|
||||||
|
const GROUP_BLOCKS_EXPAND_REQUEST = 'GROUP_BLOCKS_EXPAND_REQUEST';
|
||||||
|
const GROUP_BLOCKS_EXPAND_SUCCESS = 'GROUP_BLOCKS_EXPAND_SUCCESS';
|
||||||
|
const GROUP_BLOCKS_EXPAND_FAIL = 'GROUP_BLOCKS_EXPAND_FAIL';
|
||||||
|
|
||||||
|
const GROUP_BLOCK_REQUEST = 'GROUP_BLOCK_REQUEST';
|
||||||
|
const GROUP_BLOCK_SUCCESS = 'GROUP_BLOCK_SUCCESS';
|
||||||
|
const GROUP_BLOCK_FAIL = 'GROUP_BLOCK_FAIL';
|
||||||
|
|
||||||
|
const GROUP_UNBLOCK_REQUEST = 'GROUP_UNBLOCK_REQUEST';
|
||||||
|
const GROUP_UNBLOCK_SUCCESS = 'GROUP_UNBLOCK_SUCCESS';
|
||||||
|
const GROUP_UNBLOCK_FAIL = 'GROUP_UNBLOCK_FAIL';
|
||||||
|
|
||||||
|
const GROUP_PROMOTE_REQUEST = 'GROUP_PROMOTE_REQUEST';
|
||||||
|
const GROUP_PROMOTE_SUCCESS = 'GROUP_PROMOTE_SUCCESS';
|
||||||
|
const GROUP_PROMOTE_FAIL = 'GROUP_PROMOTE_FAIL';
|
||||||
|
|
||||||
|
const GROUP_DEMOTE_REQUEST = 'GROUP_DEMOTE_REQUEST';
|
||||||
|
const GROUP_DEMOTE_SUCCESS = 'GROUP_DEMOTE_SUCCESS';
|
||||||
|
const GROUP_DEMOTE_FAIL = 'GROUP_DEMOTE_FAIL';
|
||||||
|
|
||||||
|
const GROUP_MEMBERSHIPS_FETCH_REQUEST = 'GROUP_MEMBERSHIPS_FETCH_REQUEST';
|
||||||
|
const GROUP_MEMBERSHIPS_FETCH_SUCCESS = 'GROUP_MEMBERSHIPS_FETCH_SUCCESS';
|
||||||
|
const GROUP_MEMBERSHIPS_FETCH_FAIL = 'GROUP_MEMBERSHIPS_FETCH_FAIL';
|
||||||
|
|
||||||
|
const GROUP_MEMBERSHIPS_EXPAND_REQUEST = 'GROUP_MEMBERSHIPS_EXPAND_REQUEST';
|
||||||
|
const GROUP_MEMBERSHIPS_EXPAND_SUCCESS = 'GROUP_MEMBERSHIPS_EXPAND_SUCCESS';
|
||||||
|
const GROUP_MEMBERSHIPS_EXPAND_FAIL = 'GROUP_MEMBERSHIPS_EXPAND_FAIL';
|
||||||
|
|
||||||
|
const GROUP_MEMBERSHIP_REQUESTS_FETCH_REQUEST = 'GROUP_MEMBERSHIP_REQUESTS_FETCH_REQUEST';
|
||||||
|
const GROUP_MEMBERSHIP_REQUESTS_FETCH_SUCCESS = 'GROUP_MEMBERSHIP_REQUESTS_FETCH_SUCCESS';
|
||||||
|
const GROUP_MEMBERSHIP_REQUESTS_FETCH_FAIL = 'GROUP_MEMBERSHIP_REQUESTS_FETCH_FAIL';
|
||||||
|
|
||||||
|
const GROUP_MEMBERSHIP_REQUESTS_EXPAND_REQUEST = 'GROUP_MEMBERSHIP_REQUESTS_EXPAND_REQUEST';
|
||||||
|
const GROUP_MEMBERSHIP_REQUESTS_EXPAND_SUCCESS = 'GROUP_MEMBERSHIP_REQUESTS_EXPAND_SUCCESS';
|
||||||
|
const GROUP_MEMBERSHIP_REQUESTS_EXPAND_FAIL = 'GROUP_MEMBERSHIP_REQUESTS_EXPAND_FAIL';
|
||||||
|
|
||||||
|
const GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_REQUEST = 'GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_REQUEST';
|
||||||
|
const GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_SUCCESS = 'GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_SUCCESS';
|
||||||
|
const GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_FAIL = 'GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_FAIL';
|
||||||
|
|
||||||
|
const GROUP_MEMBERSHIP_REQUEST_REJECT_REQUEST = 'GROUP_MEMBERSHIP_REQUEST_REJECT_REQUEST';
|
||||||
|
const GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS = 'GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS';
|
||||||
|
const GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL = 'GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL';
|
||||||
|
|
||||||
|
const deleteGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch(deleteEntities([id], 'Group'));
|
||||||
|
|
||||||
|
return api(getState).delete(`/api/v1/groups/${id}`)
|
||||||
|
.then(() => dispatch(deleteGroupSuccess(id)))
|
||||||
|
.catch(err => dispatch(deleteGroupFail(id, err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteGroupRequest = (id: string) => ({
|
||||||
|
type: GROUP_DELETE_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteGroupSuccess = (id: string) => ({
|
||||||
|
type: GROUP_DELETE_SUCCESS,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteGroupFail = (id: string, error: AxiosError) => ({
|
||||||
|
type: GROUP_DELETE_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch(fetchGroupRelationships([id]));
|
||||||
|
dispatch(fetchGroupRequest(id));
|
||||||
|
|
||||||
|
return api(getState).get(`/api/v1/groups/${id}`)
|
||||||
|
.then(({ data }) => {
|
||||||
|
dispatch(importFetchedGroups([data]));
|
||||||
|
dispatch(fetchGroupSuccess(data));
|
||||||
|
})
|
||||||
|
.catch(err => dispatch(fetchGroupFail(id, err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchGroupRequest = (id: string) => ({
|
||||||
|
type: GROUP_FETCH_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchGroupSuccess = (group: APIEntity) => ({
|
||||||
|
type: GROUP_FETCH_SUCCESS,
|
||||||
|
group,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchGroupFail = (id: string, error: AxiosError) => ({
|
||||||
|
type: GROUP_FETCH_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchGroups = () => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch(fetchGroupsRequest());
|
||||||
|
|
||||||
|
return api(getState).get('/api/v1/groups')
|
||||||
|
.then(({ data }) => {
|
||||||
|
dispatch(importFetchedGroups(data));
|
||||||
|
dispatch(fetchGroupsSuccess(data));
|
||||||
|
dispatch(fetchGroupRelationships(data.map((item: APIEntity) => item.id)));
|
||||||
|
}).catch(err => dispatch(fetchGroupsFail(err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchGroupsRequest = () => ({
|
||||||
|
type: GROUPS_FETCH_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchGroupsSuccess = (groups: APIEntity[]) => ({
|
||||||
|
type: GROUPS_FETCH_SUCCESS,
|
||||||
|
groups,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchGroupsFail = (error: AxiosError) => ({
|
||||||
|
type: GROUPS_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchGroupRelationships = (groupIds: string[]) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
const state = getState();
|
||||||
|
const loadedRelationships = state.group_relationships;
|
||||||
|
const newGroupIds = groupIds.filter(id => loadedRelationships.get(id, null) === null);
|
||||||
|
|
||||||
|
if (!state.me || newGroupIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(fetchGroupRelationshipsRequest(newGroupIds));
|
||||||
|
|
||||||
|
return api(getState).get(`/api/v1/groups/relationships?${newGroupIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
|
||||||
|
dispatch(fetchGroupRelationshipsSuccess(response.data));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(fetchGroupRelationshipsFail(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchGroupRelationshipsRequest = (ids: string[]) => ({
|
||||||
|
type: GROUP_RELATIONSHIPS_FETCH_REQUEST,
|
||||||
|
ids,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchGroupRelationshipsSuccess = (relationships: APIEntity[]) => ({
|
||||||
|
type: GROUP_RELATIONSHIPS_FETCH_SUCCESS,
|
||||||
|
relationships,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchGroupRelationshipsFail = (error: AxiosError) => ({
|
||||||
|
type: GROUP_RELATIONSHIPS_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
skipLoading: true,
|
||||||
|
skipNotFound: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupDeleteStatus = (groupId: string, statusId: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch(groupDeleteStatusRequest(groupId, statusId));
|
||||||
|
|
||||||
|
return api(getState).delete(`/api/v1/groups/${groupId}/statuses/${statusId}`)
|
||||||
|
.then(() => {
|
||||||
|
dispatch(deleteFromTimelines(statusId));
|
||||||
|
dispatch(groupDeleteStatusSuccess(groupId, statusId));
|
||||||
|
}).catch(err => dispatch(groupDeleteStatusFail(groupId, statusId, err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupDeleteStatusRequest = (groupId: string, statusId: string) => ({
|
||||||
|
type: GROUP_DELETE_STATUS_REQUEST,
|
||||||
|
groupId,
|
||||||
|
statusId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupDeleteStatusSuccess = (groupId: string, statusId: string) => ({
|
||||||
|
type: GROUP_DELETE_STATUS_SUCCESS,
|
||||||
|
groupId,
|
||||||
|
statusId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupDeleteStatusFail = (groupId: string, statusId: string, error: AxiosError) => ({
|
||||||
|
type: GROUP_DELETE_STATUS_SUCCESS,
|
||||||
|
groupId,
|
||||||
|
statusId,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupKick = (groupId: string, accountId: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch(groupKickRequest(groupId, accountId));
|
||||||
|
|
||||||
|
return api(getState).post(`/api/v1/groups/${groupId}/kick`, { account_ids: [accountId] })
|
||||||
|
.then(() => dispatch(groupKickSuccess(groupId, accountId)))
|
||||||
|
.catch(err => dispatch(groupKickFail(groupId, accountId, err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupKickRequest = (groupId: string, accountId: string) => ({
|
||||||
|
type: GROUP_KICK_REQUEST,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupKickSuccess = (groupId: string, accountId: string) => ({
|
||||||
|
type: GROUP_KICK_SUCCESS,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupKickFail = (groupId: string, accountId: string, error: AxiosError) => ({
|
||||||
|
type: GROUP_KICK_SUCCESS,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchGroupBlocks = (id: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch(fetchGroupBlocksRequest(id));
|
||||||
|
|
||||||
|
return api(getState).get(`/api/v1/groups/${id}/blocks`).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
|
dispatch(fetchGroupBlocksSuccess(id, response.data, next ? next.uri : null));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(fetchGroupBlocksFail(id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchGroupBlocksRequest = (id: string) => ({
|
||||||
|
type: GROUP_BLOCKS_FETCH_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchGroupBlocksSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
|
||||||
|
type: GROUP_BLOCKS_FETCH_SUCCESS,
|
||||||
|
id,
|
||||||
|
accounts,
|
||||||
|
next,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchGroupBlocksFail = (id: string, error: AxiosError) => ({
|
||||||
|
type: GROUP_BLOCKS_FETCH_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
skipNotFound: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandGroupBlocks = (id: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
const url = getState().user_lists.group_blocks.get(id)?.next || null;
|
||||||
|
|
||||||
|
if (url === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(expandGroupBlocksRequest(id));
|
||||||
|
|
||||||
|
return api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
|
dispatch(expandGroupBlocksSuccess(id, response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(expandGroupBlocksFail(id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandGroupBlocksRequest = (id: string) => ({
|
||||||
|
type: GROUP_BLOCKS_EXPAND_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandGroupBlocksSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
|
||||||
|
type: GROUP_BLOCKS_EXPAND_SUCCESS,
|
||||||
|
id,
|
||||||
|
accounts,
|
||||||
|
next,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandGroupBlocksFail = (id: string, error: AxiosError) => ({
|
||||||
|
type: GROUP_BLOCKS_EXPAND_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupBlock = (groupId: string, accountId: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch(groupBlockRequest(groupId, accountId));
|
||||||
|
|
||||||
|
return api(getState).post(`/api/v1/groups/${groupId}/blocks`, { account_ids: [accountId] })
|
||||||
|
.then(() => dispatch(groupBlockSuccess(groupId, accountId)))
|
||||||
|
.catch(err => dispatch(groupBlockFail(groupId, accountId, err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupBlockRequest = (groupId: string, accountId: string) => ({
|
||||||
|
type: GROUP_BLOCK_REQUEST,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupBlockSuccess = (groupId: string, accountId: string) => ({
|
||||||
|
type: GROUP_BLOCK_SUCCESS,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupBlockFail = (groupId: string, accountId: string, error: AxiosError) => ({
|
||||||
|
type: GROUP_BLOCK_FAIL,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupUnblock = (groupId: string, accountId: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch(groupUnblockRequest(groupId, accountId));
|
||||||
|
|
||||||
|
return api(getState).delete(`/api/v1/groups/${groupId}/blocks?account_ids[]=${accountId}`)
|
||||||
|
.then(() => dispatch(groupUnblockSuccess(groupId, accountId)))
|
||||||
|
.catch(err => dispatch(groupUnblockFail(groupId, accountId, err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupUnblockRequest = (groupId: string, accountId: string) => ({
|
||||||
|
type: GROUP_UNBLOCK_REQUEST,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupUnblockSuccess = (groupId: string, accountId: string) => ({
|
||||||
|
type: GROUP_UNBLOCK_SUCCESS,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupUnblockFail = (groupId: string, accountId: string, error: AxiosError) => ({
|
||||||
|
type: GROUP_UNBLOCK_FAIL,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupPromoteAccount = (groupId: string, accountId: string, role: GroupRole) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch(groupPromoteAccountRequest(groupId, accountId));
|
||||||
|
|
||||||
|
return api(getState).post(`/api/v1/groups/${groupId}/promote`, { account_ids: [accountId], role: role })
|
||||||
|
.then((response) => dispatch(groupPromoteAccountSuccess(groupId, accountId, response.data)))
|
||||||
|
.catch(err => dispatch(groupPromoteAccountFail(groupId, accountId, err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupPromoteAccountRequest = (groupId: string, accountId: string) => ({
|
||||||
|
type: GROUP_PROMOTE_REQUEST,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupPromoteAccountSuccess = (groupId: string, accountId: string, memberships: APIEntity[]) => ({
|
||||||
|
type: GROUP_PROMOTE_SUCCESS,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
memberships,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupPromoteAccountFail = (groupId: string, accountId: string, error: AxiosError) => ({
|
||||||
|
type: GROUP_PROMOTE_FAIL,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupDemoteAccount = (groupId: string, accountId: string, role: GroupRole) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch(groupDemoteAccountRequest(groupId, accountId));
|
||||||
|
|
||||||
|
return api(getState).post(`/api/v1/groups/${groupId}/demote`, { account_ids: [accountId], role: role })
|
||||||
|
.then((response) => dispatch(groupDemoteAccountSuccess(groupId, accountId, response.data)))
|
||||||
|
.catch(err => dispatch(groupDemoteAccountFail(groupId, accountId, err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupDemoteAccountRequest = (groupId: string, accountId: string) => ({
|
||||||
|
type: GROUP_DEMOTE_REQUEST,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupDemoteAccountSuccess = (groupId: string, accountId: string, memberships: APIEntity[]) => ({
|
||||||
|
type: GROUP_DEMOTE_SUCCESS,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
memberships,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupDemoteAccountFail = (groupId: string, accountId: string, error: AxiosError) => ({
|
||||||
|
type: GROUP_DEMOTE_FAIL,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchGroupMemberships = (id: string, role: GroupRole) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch(fetchGroupMembershipsRequest(id, role));
|
||||||
|
|
||||||
|
return api(getState).get(`/api/v1/groups/${id}/memberships`, { params: { role } }).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data.map((membership: APIEntity) => membership.account)));
|
||||||
|
dispatch(fetchGroupMembershipsSuccess(id, role, response.data, next ? next.uri : null));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(fetchGroupMembershipsFail(id, role, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchGroupMembershipsRequest = (id: string, role: GroupRole) => ({
|
||||||
|
type: GROUP_MEMBERSHIPS_FETCH_REQUEST,
|
||||||
|
id,
|
||||||
|
role,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchGroupMembershipsSuccess = (id: string, role: GroupRole, memberships: APIEntity[], next: string | null) => ({
|
||||||
|
type: GROUP_MEMBERSHIPS_FETCH_SUCCESS,
|
||||||
|
id,
|
||||||
|
role,
|
||||||
|
memberships,
|
||||||
|
next,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchGroupMembershipsFail = (id: string, role: GroupRole, error: AxiosError) => ({
|
||||||
|
type: GROUP_MEMBERSHIPS_FETCH_FAIL,
|
||||||
|
id,
|
||||||
|
role,
|
||||||
|
error,
|
||||||
|
skipNotFound: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandGroupMemberships = (id: string, role: GroupRole) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
const url = getState().group_memberships.get(role).get(id)?.next || null;
|
||||||
|
|
||||||
|
if (url === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(expandGroupMembershipsRequest(id, role));
|
||||||
|
|
||||||
|
return api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data.map((membership: APIEntity) => membership.account)));
|
||||||
|
dispatch(expandGroupMembershipsSuccess(id, role, response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(expandGroupMembershipsFail(id, role, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandGroupMembershipsRequest = (id: string, role: GroupRole) => ({
|
||||||
|
type: GROUP_MEMBERSHIPS_EXPAND_REQUEST,
|
||||||
|
id,
|
||||||
|
role,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandGroupMembershipsSuccess = (id: string, role: GroupRole, memberships: APIEntity[], next: string | null) => ({
|
||||||
|
type: GROUP_MEMBERSHIPS_EXPAND_SUCCESS,
|
||||||
|
id,
|
||||||
|
role,
|
||||||
|
memberships,
|
||||||
|
next,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandGroupMembershipsFail = (id: string, role: GroupRole, error: AxiosError) => ({
|
||||||
|
type: GROUP_MEMBERSHIPS_EXPAND_FAIL,
|
||||||
|
id,
|
||||||
|
role,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchGroupMembershipRequests = (id: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch(fetchGroupMembershipRequestsRequest(id));
|
||||||
|
|
||||||
|
return api(getState).get(`/api/v1/groups/${id}/membership_requests`).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
|
dispatch(fetchGroupMembershipRequestsSuccess(id, response.data, next ? next.uri : null));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(fetchGroupMembershipRequestsFail(id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchGroupMembershipRequestsRequest = (id: string) => ({
|
||||||
|
type: GROUP_MEMBERSHIP_REQUESTS_FETCH_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchGroupMembershipRequestsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
|
||||||
|
type: GROUP_MEMBERSHIP_REQUESTS_FETCH_SUCCESS,
|
||||||
|
id,
|
||||||
|
accounts,
|
||||||
|
next,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchGroupMembershipRequestsFail = (id: string, error: AxiosError) => ({
|
||||||
|
type: GROUP_MEMBERSHIP_REQUESTS_FETCH_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
skipNotFound: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandGroupMembershipRequests = (id: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
const url = getState().user_lists.membership_requests.get(id)?.next || null;
|
||||||
|
|
||||||
|
if (url === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(expandGroupMembershipRequestsRequest(id));
|
||||||
|
|
||||||
|
return api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
|
dispatch(expandGroupMembershipRequestsSuccess(id, response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(expandGroupMembershipRequestsFail(id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandGroupMembershipRequestsRequest = (id: string) => ({
|
||||||
|
type: GROUP_MEMBERSHIP_REQUESTS_EXPAND_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandGroupMembershipRequestsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
|
||||||
|
type: GROUP_MEMBERSHIP_REQUESTS_EXPAND_SUCCESS,
|
||||||
|
id,
|
||||||
|
accounts,
|
||||||
|
next,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandGroupMembershipRequestsFail = (id: string, error: AxiosError) => ({
|
||||||
|
type: GROUP_MEMBERSHIP_REQUESTS_EXPAND_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const authorizeGroupMembershipRequest = (groupId: string, accountId: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch(authorizeGroupMembershipRequestRequest(groupId, accountId));
|
||||||
|
|
||||||
|
return api(getState)
|
||||||
|
.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/authorize`)
|
||||||
|
.then(() => dispatch(authorizeGroupMembershipRequestSuccess(groupId, accountId)))
|
||||||
|
.catch(error => dispatch(authorizeGroupMembershipRequestFail(groupId, accountId, error)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const authorizeGroupMembershipRequestRequest = (groupId: string, accountId: string) => ({
|
||||||
|
type: GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_REQUEST,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const authorizeGroupMembershipRequestSuccess = (groupId: string, accountId: string) => ({
|
||||||
|
type: GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_SUCCESS,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const authorizeGroupMembershipRequestFail = (groupId: string, accountId: string, error: AxiosError) => ({
|
||||||
|
type: GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_FAIL,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rejectGroupMembershipRequest = (groupId: string, accountId: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch(rejectGroupMembershipRequestRequest(groupId, accountId));
|
||||||
|
|
||||||
|
return api(getState)
|
||||||
|
.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/reject`)
|
||||||
|
.then(() => dispatch(rejectGroupMembershipRequestSuccess(groupId, accountId)))
|
||||||
|
.catch(error => dispatch(rejectGroupMembershipRequestFail(groupId, accountId, error)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const rejectGroupMembershipRequestRequest = (groupId: string, accountId: string) => ({
|
||||||
|
type: GROUP_MEMBERSHIP_REQUEST_REJECT_REQUEST,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rejectGroupMembershipRequestSuccess = (groupId: string, accountId: string) => ({
|
||||||
|
type: GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rejectGroupMembershipRequestFail = (groupId: string, accountId: string, error?: AxiosError) => ({
|
||||||
|
type: GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL,
|
||||||
|
groupId,
|
||||||
|
accountId,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export {
|
||||||
|
GROUP_CREATE_REQUEST,
|
||||||
|
GROUP_CREATE_SUCCESS,
|
||||||
|
GROUP_CREATE_FAIL,
|
||||||
|
GROUP_UPDATE_REQUEST,
|
||||||
|
GROUP_UPDATE_SUCCESS,
|
||||||
|
GROUP_UPDATE_FAIL,
|
||||||
|
GROUP_DELETE_REQUEST,
|
||||||
|
GROUP_DELETE_SUCCESS,
|
||||||
|
GROUP_DELETE_FAIL,
|
||||||
|
GROUP_FETCH_REQUEST,
|
||||||
|
GROUP_FETCH_SUCCESS,
|
||||||
|
GROUP_FETCH_FAIL,
|
||||||
|
GROUPS_FETCH_REQUEST,
|
||||||
|
GROUPS_FETCH_SUCCESS,
|
||||||
|
GROUPS_FETCH_FAIL,
|
||||||
|
GROUP_RELATIONSHIPS_FETCH_REQUEST,
|
||||||
|
GROUP_RELATIONSHIPS_FETCH_SUCCESS,
|
||||||
|
GROUP_RELATIONSHIPS_FETCH_FAIL,
|
||||||
|
GROUP_DELETE_STATUS_REQUEST,
|
||||||
|
GROUP_DELETE_STATUS_SUCCESS,
|
||||||
|
GROUP_DELETE_STATUS_FAIL,
|
||||||
|
GROUP_KICK_REQUEST,
|
||||||
|
GROUP_KICK_SUCCESS,
|
||||||
|
GROUP_KICK_FAIL,
|
||||||
|
GROUP_BLOCKS_FETCH_REQUEST,
|
||||||
|
GROUP_BLOCKS_FETCH_SUCCESS,
|
||||||
|
GROUP_BLOCKS_FETCH_FAIL,
|
||||||
|
GROUP_BLOCKS_EXPAND_REQUEST,
|
||||||
|
GROUP_BLOCKS_EXPAND_SUCCESS,
|
||||||
|
GROUP_BLOCKS_EXPAND_FAIL,
|
||||||
|
GROUP_BLOCK_REQUEST,
|
||||||
|
GROUP_BLOCK_SUCCESS,
|
||||||
|
GROUP_BLOCK_FAIL,
|
||||||
|
GROUP_UNBLOCK_REQUEST,
|
||||||
|
GROUP_UNBLOCK_SUCCESS,
|
||||||
|
GROUP_UNBLOCK_FAIL,
|
||||||
|
GROUP_PROMOTE_REQUEST,
|
||||||
|
GROUP_PROMOTE_SUCCESS,
|
||||||
|
GROUP_PROMOTE_FAIL,
|
||||||
|
GROUP_DEMOTE_REQUEST,
|
||||||
|
GROUP_DEMOTE_SUCCESS,
|
||||||
|
GROUP_DEMOTE_FAIL,
|
||||||
|
GROUP_MEMBERSHIPS_FETCH_REQUEST,
|
||||||
|
GROUP_MEMBERSHIPS_FETCH_SUCCESS,
|
||||||
|
GROUP_MEMBERSHIPS_FETCH_FAIL,
|
||||||
|
GROUP_MEMBERSHIPS_EXPAND_REQUEST,
|
||||||
|
GROUP_MEMBERSHIPS_EXPAND_SUCCESS,
|
||||||
|
GROUP_MEMBERSHIPS_EXPAND_FAIL,
|
||||||
|
GROUP_MEMBERSHIP_REQUESTS_FETCH_REQUEST,
|
||||||
|
GROUP_MEMBERSHIP_REQUESTS_FETCH_SUCCESS,
|
||||||
|
GROUP_MEMBERSHIP_REQUESTS_FETCH_FAIL,
|
||||||
|
GROUP_MEMBERSHIP_REQUESTS_EXPAND_REQUEST,
|
||||||
|
GROUP_MEMBERSHIP_REQUESTS_EXPAND_SUCCESS,
|
||||||
|
GROUP_MEMBERSHIP_REQUESTS_EXPAND_FAIL,
|
||||||
|
GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_REQUEST,
|
||||||
|
GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_SUCCESS,
|
||||||
|
GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_FAIL,
|
||||||
|
GROUP_MEMBERSHIP_REQUEST_REJECT_REQUEST,
|
||||||
|
GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS,
|
||||||
|
GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL,
|
||||||
|
deleteGroup,
|
||||||
|
deleteGroupRequest,
|
||||||
|
deleteGroupSuccess,
|
||||||
|
deleteGroupFail,
|
||||||
|
fetchGroup,
|
||||||
|
fetchGroupRequest,
|
||||||
|
fetchGroupSuccess,
|
||||||
|
fetchGroupFail,
|
||||||
|
fetchGroups,
|
||||||
|
fetchGroupsRequest,
|
||||||
|
fetchGroupsSuccess,
|
||||||
|
fetchGroupsFail,
|
||||||
|
fetchGroupRelationships,
|
||||||
|
fetchGroupRelationshipsRequest,
|
||||||
|
fetchGroupRelationshipsSuccess,
|
||||||
|
fetchGroupRelationshipsFail,
|
||||||
|
groupDeleteStatus,
|
||||||
|
groupDeleteStatusRequest,
|
||||||
|
groupDeleteStatusSuccess,
|
||||||
|
groupDeleteStatusFail,
|
||||||
|
groupKick,
|
||||||
|
groupKickRequest,
|
||||||
|
groupKickSuccess,
|
||||||
|
groupKickFail,
|
||||||
|
fetchGroupBlocks,
|
||||||
|
fetchGroupBlocksRequest,
|
||||||
|
fetchGroupBlocksSuccess,
|
||||||
|
fetchGroupBlocksFail,
|
||||||
|
expandGroupBlocks,
|
||||||
|
expandGroupBlocksRequest,
|
||||||
|
expandGroupBlocksSuccess,
|
||||||
|
expandGroupBlocksFail,
|
||||||
|
groupBlock,
|
||||||
|
groupBlockRequest,
|
||||||
|
groupBlockSuccess,
|
||||||
|
groupBlockFail,
|
||||||
|
groupUnblock,
|
||||||
|
groupUnblockRequest,
|
||||||
|
groupUnblockSuccess,
|
||||||
|
groupUnblockFail,
|
||||||
|
groupPromoteAccount,
|
||||||
|
groupPromoteAccountRequest,
|
||||||
|
groupPromoteAccountSuccess,
|
||||||
|
groupPromoteAccountFail,
|
||||||
|
groupDemoteAccount,
|
||||||
|
groupDemoteAccountRequest,
|
||||||
|
groupDemoteAccountSuccess,
|
||||||
|
groupDemoteAccountFail,
|
||||||
|
fetchGroupMemberships,
|
||||||
|
fetchGroupMembershipsRequest,
|
||||||
|
fetchGroupMembershipsSuccess,
|
||||||
|
fetchGroupMembershipsFail,
|
||||||
|
expandGroupMemberships,
|
||||||
|
expandGroupMembershipsRequest,
|
||||||
|
expandGroupMembershipsSuccess,
|
||||||
|
expandGroupMembershipsFail,
|
||||||
|
fetchGroupMembershipRequests,
|
||||||
|
fetchGroupMembershipRequestsRequest,
|
||||||
|
fetchGroupMembershipRequestsSuccess,
|
||||||
|
fetchGroupMembershipRequestsFail,
|
||||||
|
expandGroupMembershipRequests,
|
||||||
|
expandGroupMembershipRequestsRequest,
|
||||||
|
expandGroupMembershipRequestsSuccess,
|
||||||
|
expandGroupMembershipRequestsFail,
|
||||||
|
authorizeGroupMembershipRequest,
|
||||||
|
authorizeGroupMembershipRequestRequest,
|
||||||
|
authorizeGroupMembershipRequestSuccess,
|
||||||
|
authorizeGroupMembershipRequestFail,
|
||||||
|
rejectGroupMembershipRequest,
|
||||||
|
rejectGroupMembershipRequestRequest,
|
||||||
|
rejectGroupMembershipRequestSuccess,
|
||||||
|
rejectGroupMembershipRequestFail,
|
||||||
|
};
|
|
@ -27,8 +27,8 @@ type ImportDataActions = {
|
||||||
| typeof IMPORT_BLOCKS_FAIL
|
| typeof IMPORT_BLOCKS_FAIL
|
||||||
| typeof IMPORT_MUTES_REQUEST
|
| typeof IMPORT_MUTES_REQUEST
|
||||||
| typeof IMPORT_MUTES_SUCCESS
|
| typeof IMPORT_MUTES_SUCCESS
|
||||||
| typeof IMPORT_MUTES_FAIL,
|
| typeof IMPORT_MUTES_FAIL
|
||||||
error?: any,
|
error?: any
|
||||||
config?: string
|
config?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
import { importEntities } from 'soapbox/entity-store/actions';
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { Group, groupSchema } from 'soapbox/schemas';
|
||||||
|
import { filteredArray } from 'soapbox/schemas/utils';
|
||||||
|
|
||||||
import { getSettings } from '../settings';
|
import { getSettings } from '../settings';
|
||||||
|
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
|
@ -5,42 +10,44 @@ import type { APIEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT';
|
const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT';
|
||||||
const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
|
const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
|
||||||
|
const GROUP_IMPORT = 'GROUP_IMPORT';
|
||||||
|
const GROUPS_IMPORT = 'GROUPS_IMPORT';
|
||||||
const STATUS_IMPORT = 'STATUS_IMPORT';
|
const STATUS_IMPORT = 'STATUS_IMPORT';
|
||||||
const STATUSES_IMPORT = 'STATUSES_IMPORT';
|
const STATUSES_IMPORT = 'STATUSES_IMPORT';
|
||||||
const POLLS_IMPORT = 'POLLS_IMPORT';
|
const POLLS_IMPORT = 'POLLS_IMPORT';
|
||||||
const ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP = 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP';
|
const ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP = 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP';
|
||||||
|
|
||||||
export function importAccount(account: APIEntity) {
|
const importAccount = (account: APIEntity) =>
|
||||||
return { type: ACCOUNT_IMPORT, account };
|
({ type: ACCOUNT_IMPORT, account });
|
||||||
}
|
|
||||||
|
|
||||||
export function importAccounts(accounts: APIEntity[]) {
|
const importAccounts = (accounts: APIEntity[]) =>
|
||||||
return { type: ACCOUNTS_IMPORT, accounts };
|
({ type: ACCOUNTS_IMPORT, accounts });
|
||||||
}
|
|
||||||
|
|
||||||
export function importStatus(status: APIEntity, idempotencyKey?: string) {
|
const importGroup = (group: Group) =>
|
||||||
return (dispatch: AppDispatch, getState: () => RootState) => {
|
importEntities([group], Entities.GROUPS);
|
||||||
|
|
||||||
|
const importGroups = (groups: Group[]) =>
|
||||||
|
importEntities(groups, Entities.GROUPS);
|
||||||
|
|
||||||
|
const importStatus = (status: APIEntity, idempotencyKey?: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const expandSpoilers = getSettings(getState()).get('expandSpoilers');
|
const expandSpoilers = getSettings(getState()).get('expandSpoilers');
|
||||||
return dispatch({ type: STATUS_IMPORT, status, idempotencyKey, expandSpoilers });
|
return dispatch({ type: STATUS_IMPORT, status, idempotencyKey, expandSpoilers });
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export function importStatuses(statuses: APIEntity[]) {
|
const importStatuses = (statuses: APIEntity[]) =>
|
||||||
return (dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const expandSpoilers = getSettings(getState()).get('expandSpoilers');
|
const expandSpoilers = getSettings(getState()).get('expandSpoilers');
|
||||||
return dispatch({ type: STATUSES_IMPORT, statuses, expandSpoilers });
|
return dispatch({ type: STATUSES_IMPORT, statuses, expandSpoilers });
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export function importPolls(polls: APIEntity[]) {
|
const importPolls = (polls: APIEntity[]) =>
|
||||||
return { type: POLLS_IMPORT, polls };
|
({ type: POLLS_IMPORT, polls });
|
||||||
}
|
|
||||||
|
|
||||||
export function importFetchedAccount(account: APIEntity) {
|
const importFetchedAccount = (account: APIEntity) =>
|
||||||
return importFetchedAccounts([account]);
|
importFetchedAccounts([account]);
|
||||||
}
|
|
||||||
|
|
||||||
export function importFetchedAccounts(accounts: APIEntity[], args = { should_refetch: false }) {
|
const importFetchedAccounts = (accounts: APIEntity[], args = { should_refetch: false }) => {
|
||||||
const { should_refetch } = args;
|
const { should_refetch } = args;
|
||||||
const normalAccounts: APIEntity[] = [];
|
const normalAccounts: APIEntity[] = [];
|
||||||
|
|
||||||
|
@ -61,10 +68,18 @@ export function importFetchedAccounts(accounts: APIEntity[], args = { should_ref
|
||||||
accounts.forEach(processAccount);
|
accounts.forEach(processAccount);
|
||||||
|
|
||||||
return importAccounts(normalAccounts);
|
return importAccounts(normalAccounts);
|
||||||
}
|
};
|
||||||
|
|
||||||
export function importFetchedStatus(status: APIEntity, idempotencyKey?: string) {
|
const importFetchedGroup = (group: APIEntity) =>
|
||||||
return (dispatch: AppDispatch) => {
|
importFetchedGroups([group]);
|
||||||
|
|
||||||
|
const importFetchedGroups = (groups: APIEntity[]) => {
|
||||||
|
const entities = filteredArray(groupSchema).catch([]).parse(groups);
|
||||||
|
return importGroups(entities);
|
||||||
|
};
|
||||||
|
|
||||||
|
const importFetchedStatus = (status: APIEntity, idempotencyKey?: string) =>
|
||||||
|
(dispatch: AppDispatch) => {
|
||||||
// Skip broken statuses
|
// Skip broken statuses
|
||||||
if (isBroken(status)) return;
|
if (isBroken(status)) return;
|
||||||
|
|
||||||
|
@ -96,10 +111,13 @@ export function importFetchedStatus(status: APIEntity, idempotencyKey?: string)
|
||||||
dispatch(importFetchedPoll(status.poll));
|
dispatch(importFetchedPoll(status.poll));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status.group?.id) {
|
||||||
|
dispatch(importFetchedGroup(status.group));
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(importFetchedAccount(status.account));
|
dispatch(importFetchedAccount(status.account));
|
||||||
dispatch(importStatus(status, idempotencyKey));
|
dispatch(importStatus(status, idempotencyKey));
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// Sometimes Pleroma can return an empty account,
|
// Sometimes Pleroma can return an empty account,
|
||||||
// or a repost can appear of a deleted account. Skip these statuses.
|
// or a repost can appear of a deleted account. Skip these statuses.
|
||||||
|
@ -117,8 +135,8 @@ const isBroken = (status: APIEntity) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function importFetchedStatuses(statuses: APIEntity[]) {
|
const importFetchedStatuses = (statuses: APIEntity[]) =>
|
||||||
return (dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const accounts: APIEntity[] = [];
|
const accounts: APIEntity[] = [];
|
||||||
const normalStatuses: APIEntity[] = [];
|
const normalStatuses: APIEntity[] = [];
|
||||||
const polls: APIEntity[] = [];
|
const polls: APIEntity[] = [];
|
||||||
|
@ -146,6 +164,10 @@ export function importFetchedStatuses(statuses: APIEntity[]) {
|
||||||
if (status.poll?.id) {
|
if (status.poll?.id) {
|
||||||
polls.push(status.poll);
|
polls.push(status.poll);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status.group?.id) {
|
||||||
|
dispatch(importFetchedGroup(status.group));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statuses.forEach(processStatus);
|
statuses.forEach(processStatus);
|
||||||
|
@ -154,23 +176,37 @@ export function importFetchedStatuses(statuses: APIEntity[]) {
|
||||||
dispatch(importFetchedAccounts(accounts));
|
dispatch(importFetchedAccounts(accounts));
|
||||||
dispatch(importStatuses(normalStatuses));
|
dispatch(importStatuses(normalStatuses));
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export function importFetchedPoll(poll: APIEntity) {
|
const importFetchedPoll = (poll: APIEntity) =>
|
||||||
return (dispatch: AppDispatch) => {
|
(dispatch: AppDispatch) => {
|
||||||
dispatch(importPolls([poll]));
|
dispatch(importPolls([poll]));
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export function importErrorWhileFetchingAccountByUsername(username: string) {
|
const importErrorWhileFetchingAccountByUsername = (username: string) =>
|
||||||
return { type: ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP, username };
|
({ type: ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP, username });
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ACCOUNT_IMPORT,
|
ACCOUNT_IMPORT,
|
||||||
ACCOUNTS_IMPORT,
|
ACCOUNTS_IMPORT,
|
||||||
|
GROUP_IMPORT,
|
||||||
|
GROUPS_IMPORT,
|
||||||
STATUS_IMPORT,
|
STATUS_IMPORT,
|
||||||
STATUSES_IMPORT,
|
STATUSES_IMPORT,
|
||||||
POLLS_IMPORT,
|
POLLS_IMPORT,
|
||||||
ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP,
|
ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP,
|
||||||
|
importAccount,
|
||||||
|
importAccounts,
|
||||||
|
importGroup,
|
||||||
|
importGroups,
|
||||||
|
importStatus,
|
||||||
|
importStatuses,
|
||||||
|
importPolls,
|
||||||
|
importFetchedAccount,
|
||||||
|
importFetchedAccounts,
|
||||||
|
importFetchedGroup,
|
||||||
|
importFetchedGroups,
|
||||||
|
importFetchedStatus,
|
||||||
|
importFetchedStatuses,
|
||||||
|
importFetchedPoll,
|
||||||
|
importErrorWhileFetchingAccountByUsername,
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,6 +20,10 @@ const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
|
||||||
const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
|
const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
|
||||||
const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
|
const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
|
||||||
|
|
||||||
|
const DISLIKE_REQUEST = 'DISLIKE_REQUEST';
|
||||||
|
const DISLIKE_SUCCESS = 'DISLIKE_SUCCESS';
|
||||||
|
const DISLIKE_FAIL = 'DISLIKE_FAIL';
|
||||||
|
|
||||||
const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST';
|
const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST';
|
||||||
const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS';
|
const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS';
|
||||||
const UNREBLOG_FAIL = 'UNREBLOG_FAIL';
|
const UNREBLOG_FAIL = 'UNREBLOG_FAIL';
|
||||||
|
@ -28,6 +32,10 @@ const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST';
|
||||||
const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS';
|
const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS';
|
||||||
const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL';
|
const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL';
|
||||||
|
|
||||||
|
const UNDISLIKE_REQUEST = 'UNDISLIKE_REQUEST';
|
||||||
|
const UNDISLIKE_SUCCESS = 'UNDISLIKE_SUCCESS';
|
||||||
|
const UNDISLIKE_FAIL = 'UNDISLIKE_FAIL';
|
||||||
|
|
||||||
const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST';
|
const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST';
|
||||||
const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS';
|
const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS';
|
||||||
const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL';
|
const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL';
|
||||||
|
@ -36,6 +44,10 @@ const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
|
||||||
const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
|
const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
|
||||||
const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
|
const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
|
||||||
|
|
||||||
|
const DISLIKES_FETCH_REQUEST = 'DISLIKES_FETCH_REQUEST';
|
||||||
|
const DISLIKES_FETCH_SUCCESS = 'DISLIKES_FETCH_SUCCESS';
|
||||||
|
const DISLIKES_FETCH_FAIL = 'DISLIKES_FETCH_FAIL';
|
||||||
|
|
||||||
const REACTIONS_FETCH_REQUEST = 'REACTIONS_FETCH_REQUEST';
|
const REACTIONS_FETCH_REQUEST = 'REACTIONS_FETCH_REQUEST';
|
||||||
const REACTIONS_FETCH_SUCCESS = 'REACTIONS_FETCH_SUCCESS';
|
const REACTIONS_FETCH_SUCCESS = 'REACTIONS_FETCH_SUCCESS';
|
||||||
const REACTIONS_FETCH_FAIL = 'REACTIONS_FETCH_FAIL';
|
const REACTIONS_FETCH_FAIL = 'REACTIONS_FETCH_FAIL';
|
||||||
|
@ -96,7 +108,7 @@ const unreblog = (status: StatusEntity) =>
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleReblog = (status: StatusEntity) =>
|
const toggleReblog = (status: StatusEntity) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch) => {
|
||||||
if (status.reblogged) {
|
if (status.reblogged) {
|
||||||
dispatch(unreblog(status));
|
dispatch(unreblog(status));
|
||||||
} else {
|
} else {
|
||||||
|
@ -169,7 +181,7 @@ const unfavourite = (status: StatusEntity) =>
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleFavourite = (status: StatusEntity) =>
|
const toggleFavourite = (status: StatusEntity) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch) => {
|
||||||
if (status.favourited) {
|
if (status.favourited) {
|
||||||
dispatch(unfavourite(status));
|
dispatch(unfavourite(status));
|
||||||
} else {
|
} else {
|
||||||
|
@ -215,6 +227,79 @@ const unfavouriteFail = (status: StatusEntity, error: AxiosError) => ({
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dislike = (status: StatusEntity) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
if (!isLoggedIn(getState)) return;
|
||||||
|
|
||||||
|
dispatch(dislikeRequest(status));
|
||||||
|
|
||||||
|
api(getState).post(`/api/friendica/statuses/${status.get('id')}/dislike`).then(function() {
|
||||||
|
dispatch(dislikeSuccess(status));
|
||||||
|
}).catch(function(error) {
|
||||||
|
dispatch(dislikeFail(status, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const undislike = (status: StatusEntity) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
if (!isLoggedIn(getState)) return;
|
||||||
|
|
||||||
|
dispatch(undislikeRequest(status));
|
||||||
|
|
||||||
|
api(getState).post(`/api/friendica/statuses/${status.get('id')}/undislike`).then(() => {
|
||||||
|
dispatch(undislikeSuccess(status));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(undislikeFail(status, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDislike = (status: StatusEntity) =>
|
||||||
|
(dispatch: AppDispatch) => {
|
||||||
|
if (status.disliked) {
|
||||||
|
dispatch(undislike(status));
|
||||||
|
} else {
|
||||||
|
dispatch(dislike(status));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const dislikeRequest = (status: StatusEntity) => ({
|
||||||
|
type: DISLIKE_REQUEST,
|
||||||
|
status: status,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dislikeSuccess = (status: StatusEntity) => ({
|
||||||
|
type: DISLIKE_SUCCESS,
|
||||||
|
status: status,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dislikeFail = (status: StatusEntity, error: AxiosError) => ({
|
||||||
|
type: DISLIKE_FAIL,
|
||||||
|
status: status,
|
||||||
|
error: error,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const undislikeRequest = (status: StatusEntity) => ({
|
||||||
|
type: UNDISLIKE_REQUEST,
|
||||||
|
status: status,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const undislikeSuccess = (status: StatusEntity) => ({
|
||||||
|
type: UNDISLIKE_SUCCESS,
|
||||||
|
status: status,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const undislikeFail = (status: StatusEntity, error: AxiosError) => ({
|
||||||
|
type: UNDISLIKE_FAIL,
|
||||||
|
status: status,
|
||||||
|
error: error,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
const bookmark = (status: StatusEntity) =>
|
const bookmark = (status: StatusEntity) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
dispatch(bookmarkRequest(status));
|
dispatch(bookmarkRequest(status));
|
||||||
|
@ -351,6 +436,38 @@ const fetchFavouritesFail = (id: string, error: AxiosError) => ({
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fetchDislikes = (id: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
if (!isLoggedIn(getState)) return;
|
||||||
|
|
||||||
|
dispatch(fetchDislikesRequest(id));
|
||||||
|
|
||||||
|
api(getState).get(`/api/friendica/statuses/${id}/disliked_by`).then(response => {
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
|
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||||
|
dispatch(fetchDislikesSuccess(id, response.data));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(fetchDislikesFail(id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDislikesRequest = (id: string) => ({
|
||||||
|
type: DISLIKES_FETCH_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchDislikesSuccess = (id: string, accounts: APIEntity[]) => ({
|
||||||
|
type: DISLIKES_FETCH_SUCCESS,
|
||||||
|
id,
|
||||||
|
accounts,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchDislikesFail = (id: string, error: AxiosError) => ({
|
||||||
|
type: DISLIKES_FETCH_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
const fetchReactions = (id: string) =>
|
const fetchReactions = (id: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
dispatch(fetchReactionsRequest(id));
|
dispatch(fetchReactionsRequest(id));
|
||||||
|
@ -498,18 +615,27 @@ export {
|
||||||
FAVOURITE_REQUEST,
|
FAVOURITE_REQUEST,
|
||||||
FAVOURITE_SUCCESS,
|
FAVOURITE_SUCCESS,
|
||||||
FAVOURITE_FAIL,
|
FAVOURITE_FAIL,
|
||||||
|
DISLIKE_REQUEST,
|
||||||
|
DISLIKE_SUCCESS,
|
||||||
|
DISLIKE_FAIL,
|
||||||
UNREBLOG_REQUEST,
|
UNREBLOG_REQUEST,
|
||||||
UNREBLOG_SUCCESS,
|
UNREBLOG_SUCCESS,
|
||||||
UNREBLOG_FAIL,
|
UNREBLOG_FAIL,
|
||||||
UNFAVOURITE_REQUEST,
|
UNFAVOURITE_REQUEST,
|
||||||
UNFAVOURITE_SUCCESS,
|
UNFAVOURITE_SUCCESS,
|
||||||
UNFAVOURITE_FAIL,
|
UNFAVOURITE_FAIL,
|
||||||
|
UNDISLIKE_REQUEST,
|
||||||
|
UNDISLIKE_SUCCESS,
|
||||||
|
UNDISLIKE_FAIL,
|
||||||
REBLOGS_FETCH_REQUEST,
|
REBLOGS_FETCH_REQUEST,
|
||||||
REBLOGS_FETCH_SUCCESS,
|
REBLOGS_FETCH_SUCCESS,
|
||||||
REBLOGS_FETCH_FAIL,
|
REBLOGS_FETCH_FAIL,
|
||||||
FAVOURITES_FETCH_REQUEST,
|
FAVOURITES_FETCH_REQUEST,
|
||||||
FAVOURITES_FETCH_SUCCESS,
|
FAVOURITES_FETCH_SUCCESS,
|
||||||
FAVOURITES_FETCH_FAIL,
|
FAVOURITES_FETCH_FAIL,
|
||||||
|
DISLIKES_FETCH_REQUEST,
|
||||||
|
DISLIKES_FETCH_SUCCESS,
|
||||||
|
DISLIKES_FETCH_FAIL,
|
||||||
REACTIONS_FETCH_REQUEST,
|
REACTIONS_FETCH_REQUEST,
|
||||||
REACTIONS_FETCH_SUCCESS,
|
REACTIONS_FETCH_SUCCESS,
|
||||||
REACTIONS_FETCH_FAIL,
|
REACTIONS_FETCH_FAIL,
|
||||||
|
@ -546,6 +672,15 @@ export {
|
||||||
unfavouriteRequest,
|
unfavouriteRequest,
|
||||||
unfavouriteSuccess,
|
unfavouriteSuccess,
|
||||||
unfavouriteFail,
|
unfavouriteFail,
|
||||||
|
dislike,
|
||||||
|
undislike,
|
||||||
|
toggleDislike,
|
||||||
|
dislikeRequest,
|
||||||
|
dislikeSuccess,
|
||||||
|
dislikeFail,
|
||||||
|
undislikeRequest,
|
||||||
|
undislikeSuccess,
|
||||||
|
undislikeFail,
|
||||||
bookmark,
|
bookmark,
|
||||||
unbookmark,
|
unbookmark,
|
||||||
toggleBookmark,
|
toggleBookmark,
|
||||||
|
@ -563,6 +698,10 @@ export {
|
||||||
fetchFavouritesRequest,
|
fetchFavouritesRequest,
|
||||||
fetchFavouritesSuccess,
|
fetchFavouritesSuccess,
|
||||||
fetchFavouritesFail,
|
fetchFavouritesFail,
|
||||||
|
fetchDislikes,
|
||||||
|
fetchDislikesRequest,
|
||||||
|
fetchDislikesSuccess,
|
||||||
|
fetchDislikesFail,
|
||||||
fetchReactions,
|
fetchReactions,
|
||||||
fetchReactionsRequest,
|
fetchReactionsRequest,
|
||||||
fetchReactionsSuccess,
|
fetchReactionsSuccess,
|
||||||
|
|
|
@ -112,27 +112,6 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () =
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rejectUserModal = (intl: IntlShape, accountId: string, afterConfirm = () => {}) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
const acct = state.accounts.get(accountId)!.acct;
|
|
||||||
const name = state.accounts.get(accountId)!.username;
|
|
||||||
|
|
||||||
dispatch(openModal('CONFIRM', {
|
|
||||||
icon: require('@tabler/icons/user-off.svg'),
|
|
||||||
heading: intl.formatMessage(messages.rejectUserHeading, { acct }),
|
|
||||||
message: intl.formatMessage(messages.rejectUserPrompt, { acct }),
|
|
||||||
confirm: intl.formatMessage(messages.rejectUserConfirm, { name }),
|
|
||||||
onConfirm: () => {
|
|
||||||
dispatch(deleteUsers([accountId]))
|
|
||||||
.then(() => {
|
|
||||||
afterConfirm();
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensitive: boolean, afterConfirm = () => {}) =>
|
const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensitive: boolean, afterConfirm = () => {}) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
@ -178,7 +157,6 @@ const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = ()
|
||||||
export {
|
export {
|
||||||
deactivateUserModal,
|
deactivateUserModal,
|
||||||
deleteUserModal,
|
deleteUserModal,
|
||||||
rejectUserModal,
|
|
||||||
toggleStatusSensitivityModal,
|
toggleStatusSensitivityModal,
|
||||||
deleteStatusModal,
|
deleteStatusModal,
|
||||||
};
|
};
|
||||||
|
|
|
@ -47,7 +47,7 @@ const MAX_QUEUED_NOTIFICATIONS = 40;
|
||||||
|
|
||||||
defineMessages({
|
defineMessages({
|
||||||
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
||||||
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
|
group: { id: 'notifications.group', defaultMessage: '{count, plural, one {# notification} other {# notifications}}' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchRelatedRelationships = (dispatch: AppDispatch, notifications: APIEntity[]) => {
|
const fetchRelatedRelationships = (dispatch: AppDispatch, notifications: APIEntity[]) => {
|
||||||
|
|
|
@ -37,8 +37,8 @@ const subscribe = (registration: ServiceWorkerRegistration, getState: () => Root
|
||||||
});
|
});
|
||||||
|
|
||||||
const unsubscribe = ({ registration, subscription }: {
|
const unsubscribe = ({ registration, subscription }: {
|
||||||
registration: ServiceWorkerRegistration,
|
registration: ServiceWorkerRegistration
|
||||||
subscription: PushSubscription | null,
|
subscription: PushSubscription | null
|
||||||
}) =>
|
}) =>
|
||||||
subscription ? subscription.unsubscribe().then(() => registration) : new Promise<ServiceWorkerRegistration>(r => r(registration));
|
subscription ? subscription.unsubscribe().then(() => registration) : new Promise<ServiceWorkerRegistration>(r => r(registration));
|
||||||
|
|
||||||
|
@ -82,8 +82,8 @@ const register = () =>
|
||||||
.then(getPushSubscription)
|
.then(getPushSubscription)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
.then(({ registration, subscription }: {
|
.then(({ registration, subscription }: {
|
||||||
registration: ServiceWorkerRegistration,
|
registration: ServiceWorkerRegistration
|
||||||
subscription: PushSubscription | null,
|
subscription: PushSubscription | null
|
||||||
}) => {
|
}) => {
|
||||||
if (subscription !== null) {
|
if (subscription !== null) {
|
||||||
// We have a subscription, check if it is still valid
|
// We have a subscription, check if it is still valid
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { openModal } from './modals';
|
||||||
|
|
||||||
import type { AxiosError } from 'axios';
|
import type { AxiosError } from 'axios';
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
import type { Account, ChatMessage, Status } from 'soapbox/types/entities';
|
import type { Account, ChatMessage, Group, Status } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const REPORT_INIT = 'REPORT_INIT';
|
const REPORT_INIT = 'REPORT_INIT';
|
||||||
const REPORT_CANCEL = 'REPORT_CANCEL';
|
const REPORT_CANCEL = 'REPORT_CANCEL';
|
||||||
|
@ -20,19 +20,29 @@ const REPORT_BLOCK_CHANGE = 'REPORT_BLOCK_CHANGE';
|
||||||
|
|
||||||
const REPORT_RULE_CHANGE = 'REPORT_RULE_CHANGE';
|
const REPORT_RULE_CHANGE = 'REPORT_RULE_CHANGE';
|
||||||
|
|
||||||
type ReportedEntity = {
|
enum ReportableEntities {
|
||||||
status?: Status,
|
ACCOUNT = 'ACCOUNT',
|
||||||
chatMessage?: ChatMessage
|
CHAT_MESSAGE = 'CHAT_MESSAGE',
|
||||||
|
GROUP = 'GROUP',
|
||||||
|
STATUS = 'STATUS'
|
||||||
}
|
}
|
||||||
|
|
||||||
const initReport = (account: Account, entities?: ReportedEntity) => (dispatch: AppDispatch) => {
|
type ReportedEntity = {
|
||||||
const { status, chatMessage } = entities || {};
|
status?: Status
|
||||||
|
chatMessage?: ChatMessage
|
||||||
|
group?: Group
|
||||||
|
}
|
||||||
|
|
||||||
|
const initReport = (entityType: ReportableEntities, account: Account, entities?: ReportedEntity) => (dispatch: AppDispatch) => {
|
||||||
|
const { status, chatMessage, group } = entities || {};
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: REPORT_INIT,
|
type: REPORT_INIT,
|
||||||
|
entityType,
|
||||||
account,
|
account,
|
||||||
status,
|
status,
|
||||||
chatMessage,
|
chatMessage,
|
||||||
|
group,
|
||||||
});
|
});
|
||||||
|
|
||||||
return dispatch(openModal('REPORT'));
|
return dispatch(openModal('REPORT'));
|
||||||
|
@ -56,7 +66,8 @@ const submitReport = () =>
|
||||||
return api(getState).post('/api/v1/reports', {
|
return api(getState).post('/api/v1/reports', {
|
||||||
account_id: reports.getIn(['new', 'account_id']),
|
account_id: reports.getIn(['new', 'account_id']),
|
||||||
status_ids: reports.getIn(['new', 'status_ids']),
|
status_ids: reports.getIn(['new', 'status_ids']),
|
||||||
message_ids: [reports.getIn(['new', 'chat_message', 'id'])],
|
message_ids: [reports.getIn(['new', 'chat_message', 'id'])].filter(Boolean),
|
||||||
|
group_id: reports.getIn(['new', 'group', 'id']),
|
||||||
rule_ids: reports.getIn(['new', 'rule_ids']),
|
rule_ids: reports.getIn(['new', 'rule_ids']),
|
||||||
comment: reports.getIn(['new', 'comment']),
|
comment: reports.getIn(['new', 'comment']),
|
||||||
forward: reports.getIn(['new', 'forward']),
|
forward: reports.getIn(['new', 'forward']),
|
||||||
|
@ -97,6 +108,7 @@ const changeReportRule = (ruleId: string) => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
ReportableEntities,
|
||||||
REPORT_INIT,
|
REPORT_INIT,
|
||||||
REPORT_CANCEL,
|
REPORT_CANCEL,
|
||||||
REPORT_SUBMIT_REQUEST,
|
REPORT_SUBMIT_REQUEST,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
import { fetchRelationships } from './accounts';
|
import { fetchRelationships } from './accounts';
|
||||||
import { importFetchedAccounts, importFetchedStatuses } from './importer';
|
import { importFetchedAccounts, importFetchedGroups, importFetchedStatuses } from './importer';
|
||||||
|
|
||||||
import type { AxiosError } from 'axios';
|
import type { AxiosError } from 'axios';
|
||||||
import type { SearchFilter } from 'soapbox/reducers/search';
|
import type { SearchFilter } from 'soapbox/reducers/search';
|
||||||
|
@ -83,6 +83,10 @@ const submitSearch = (filter?: SearchFilter) =>
|
||||||
dispatch(importFetchedStatuses(response.data.statuses));
|
dispatch(importFetchedStatuses(response.data.statuses));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.data.groups) {
|
||||||
|
dispatch(importFetchedGroups(response.data.groups));
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(fetchSearchSuccess(response.data, value, type));
|
dispatch(fetchSearchSuccess(response.data, value, type));
|
||||||
dispatch(fetchRelationships(response.data.accounts.map((item: APIEntity) => item.id)));
|
dispatch(fetchRelationships(response.data.accounts.map((item: APIEntity) => item.id)));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
@ -139,6 +143,10 @@ const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: (
|
||||||
dispatch(importFetchedStatuses(data.statuses));
|
dispatch(importFetchedStatuses(data.statuses));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.groups) {
|
||||||
|
dispatch(importFetchedGroups(data.groups));
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(expandSearchSuccess(data, value, type));
|
dispatch(expandSearchSuccess(data, value, type));
|
||||||
dispatch(fetchRelationships(data.accounts.map((item: APIEntity) => item.id)));
|
dispatch(fetchRelationships(data.accounts.map((item: APIEntity) => item.id)));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
|
|
@ -50,7 +50,7 @@ const MOVE_ACCOUNT_FAIL = 'MOVE_ACCOUNT_FAIL';
|
||||||
const fetchOAuthTokens = () =>
|
const fetchOAuthTokens = () =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
dispatch({ type: FETCH_TOKENS_REQUEST });
|
dispatch({ type: FETCH_TOKENS_REQUEST });
|
||||||
return api(getState).get('/api/oauth_tokens.json').then(({ data: tokens }) => {
|
return api(getState).get('/api/oauth_tokens').then(({ data: tokens }) => {
|
||||||
dispatch({ type: FETCH_TOKENS_SUCCESS, tokens });
|
dispatch({ type: FETCH_TOKENS_SUCCESS, tokens });
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
dispatch({ type: FETCH_TOKENS_FAIL });
|
dispatch({ type: FETCH_TOKENS_FAIL });
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||||
import { defineMessages } from 'react-intl';
|
import { defineMessage } from 'react-intl';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { patchMe } from 'soapbox/actions/me';
|
import { patchMe } from 'soapbox/actions/me';
|
||||||
|
import messages from 'soapbox/locales/messages';
|
||||||
import toast from 'soapbox/toast';
|
import toast from 'soapbox/toast';
|
||||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||||
|
|
||||||
|
@ -18,12 +19,10 @@ const FE_NAME = 'soapbox_fe';
|
||||||
/** Options when changing/saving settings. */
|
/** Options when changing/saving settings. */
|
||||||
type SettingOpts = {
|
type SettingOpts = {
|
||||||
/** Whether to display an alert when settings are saved. */
|
/** Whether to display an alert when settings are saved. */
|
||||||
showAlert?: boolean,
|
showAlert?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = defineMessages({
|
const saveSuccessMessage = defineMessage({ id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' });
|
||||||
saveSuccess: { id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultSettings = ImmutableMap({
|
const defaultSettings = ImmutableMap({
|
||||||
onboarded: false,
|
onboarded: false,
|
||||||
|
@ -40,7 +39,7 @@ const defaultSettings = ImmutableMap({
|
||||||
defaultPrivacy: 'public',
|
defaultPrivacy: 'public',
|
||||||
defaultContentType: 'text/plain',
|
defaultContentType: 'text/plain',
|
||||||
themeMode: 'system',
|
themeMode: 'system',
|
||||||
locale: navigator.language.split(/[-_]/)[0] || 'en',
|
locale: navigator.language || 'en',
|
||||||
showExplanationBox: true,
|
showExplanationBox: true,
|
||||||
explanationBox: true,
|
explanationBox: true,
|
||||||
autoloadTimelines: true,
|
autoloadTimelines: true,
|
||||||
|
@ -156,6 +155,8 @@ const defaultSettings = ImmutableMap({
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
groups: ImmutableMap({}),
|
||||||
|
|
||||||
trends: ImmutableMap({
|
trends: ImmutableMap({
|
||||||
show: true,
|
show: true,
|
||||||
}),
|
}),
|
||||||
|
@ -219,7 +220,7 @@ const saveSettingsImmediate = (opts?: SettingOpts) =>
|
||||||
dispatch({ type: SETTING_SAVE });
|
dispatch({ type: SETTING_SAVE });
|
||||||
|
|
||||||
if (opts?.showAlert) {
|
if (opts?.showAlert) {
|
||||||
toast.success(messages.saveSuccess);
|
toast.success(saveSuccessMessage);
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
toast.showAlertForError(error);
|
toast.showAlertForError(error);
|
||||||
|
@ -229,6 +230,12 @@ const saveSettingsImmediate = (opts?: SettingOpts) =>
|
||||||
const saveSettings = (opts?: SettingOpts) =>
|
const saveSettings = (opts?: SettingOpts) =>
|
||||||
(dispatch: AppDispatch) => dispatch(saveSettingsImmediate(opts));
|
(dispatch: AppDispatch) => dispatch(saveSettingsImmediate(opts));
|
||||||
|
|
||||||
|
const getLocale = (state: RootState, fallback = 'en') => {
|
||||||
|
const localeWithVariant = (getSettings(state).get('locale') as string).replace('_', '-');
|
||||||
|
const locale = localeWithVariant.split('-')[0];
|
||||||
|
return Object.keys(messages).includes(localeWithVariant) ? localeWithVariant : Object.keys(messages).includes(locale) ? locale : fallback;
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
SETTING_CHANGE,
|
SETTING_CHANGE,
|
||||||
SETTING_SAVE,
|
SETTING_SAVE,
|
||||||
|
@ -240,4 +247,5 @@ export {
|
||||||
changeSetting,
|
changeSetting,
|
||||||
saveSettingsImmediate,
|
saveSettingsImmediate,
|
||||||
saveSettings,
|
saveSettings,
|
||||||
|
getLocale,
|
||||||
};
|
};
|
||||||
|
|
|
@ -32,8 +32,8 @@ const getSoapboxConfig = createSelector([
|
||||||
}
|
}
|
||||||
|
|
||||||
// If RGI reacts aren't supported, strip VS16s
|
// If RGI reacts aren't supported, strip VS16s
|
||||||
// // https://git.pleroma.social/pleroma/pleroma/-/issues/2355
|
// https://git.pleroma.social/pleroma/pleroma/-/issues/2355
|
||||||
if (!features.emojiReactsRGI) {
|
if (features.emojiReactsNonRGI) {
|
||||||
soapboxConfig.set('allowedEmoji', soapboxConfig.allowedEmoji.map(removeVS16s));
|
soapboxConfig.set('allowedEmoji', soapboxConfig.allowedEmoji.map(removeVS16s));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { shouldHaveCard } from 'soapbox/utils/status';
|
||||||
import api, { getNextLink } from '../api';
|
import api, { getNextLink } from '../api';
|
||||||
|
|
||||||
import { setComposeToStatus } from './compose';
|
import { setComposeToStatus } from './compose';
|
||||||
|
import { fetchGroupRelationships } from './groups';
|
||||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||||
import { openModal } from './modals';
|
import { openModal } from './modals';
|
||||||
import { deleteFromTimelines } from './timelines';
|
import { deleteFromTimelines } from './timelines';
|
||||||
|
@ -48,6 +49,8 @@ const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS';
|
||||||
const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL';
|
const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL';
|
||||||
const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO';
|
const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO';
|
||||||
|
|
||||||
|
const STATUS_UNFILTER = 'STATUS_UNFILTER';
|
||||||
|
|
||||||
const statusExists = (getState: () => RootState, statusId: string) => {
|
const statusExists = (getState: () => RootState, statusId: string) => {
|
||||||
return (getState().statuses.get(statusId) || null) !== null;
|
return (getState().statuses.get(statusId) || null) !== null;
|
||||||
};
|
};
|
||||||
|
@ -122,6 +125,9 @@ const fetchStatus = (id: string) => {
|
||||||
|
|
||||||
return api(getState).get(`/api/v1/statuses/${id}`).then(({ data: status }) => {
|
return api(getState).get(`/api/v1/statuses/${id}`).then(({ data: status }) => {
|
||||||
dispatch(importFetchedStatus(status));
|
dispatch(importFetchedStatus(status));
|
||||||
|
if (status.group) {
|
||||||
|
dispatch(fetchGroupRelationships([status.group.id]));
|
||||||
|
}
|
||||||
dispatch({ type: STATUS_FETCH_SUCCESS, status, skipLoading });
|
dispatch({ type: STATUS_FETCH_SUCCESS, status, skipLoading });
|
||||||
return status;
|
return status;
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
@ -335,6 +341,11 @@ const undoStatusTranslation = (id: string) => ({
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const unfilterStatus = (id: string) => ({
|
||||||
|
type: STATUS_UNFILTER,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
STATUS_CREATE_REQUEST,
|
STATUS_CREATE_REQUEST,
|
||||||
STATUS_CREATE_SUCCESS,
|
STATUS_CREATE_SUCCESS,
|
||||||
|
@ -363,6 +374,7 @@ export {
|
||||||
STATUS_TRANSLATE_SUCCESS,
|
STATUS_TRANSLATE_SUCCESS,
|
||||||
STATUS_TRANSLATE_FAIL,
|
STATUS_TRANSLATE_FAIL,
|
||||||
STATUS_TRANSLATE_UNDO,
|
STATUS_TRANSLATE_UNDO,
|
||||||
|
STATUS_UNFILTER,
|
||||||
createStatus,
|
createStatus,
|
||||||
editStatus,
|
editStatus,
|
||||||
fetchStatus,
|
fetchStatus,
|
||||||
|
@ -381,4 +393,5 @@ export {
|
||||||
toggleStatusHidden,
|
toggleStatusHidden,
|
||||||
translateStatus,
|
translateStatus,
|
||||||
undoStatusTranslation,
|
undoStatusTranslation,
|
||||||
|
unfilterStatus,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { getSettings } from 'soapbox/actions/settings';
|
import { getLocale, getSettings } from 'soapbox/actions/settings';
|
||||||
import messages from 'soapbox/locales/messages';
|
import messages from 'soapbox/locales/messages';
|
||||||
import { ChatKeys, IChat, isLastMessage } from 'soapbox/queries/chats';
|
import { ChatKeys, IChat, isLastMessage } from 'soapbox/queries/chats';
|
||||||
import { queryClient } from 'soapbox/queries/client';
|
import { queryClient } from 'soapbox/queries/client';
|
||||||
import { getUnreadChatsCount, updateChatListItem } from 'soapbox/utils/chats';
|
import { getUnreadChatsCount, updateChatListItem, updateChatMessage } from 'soapbox/utils/chats';
|
||||||
import { removePageItem } from 'soapbox/utils/queries';
|
import { removePageItem } from 'soapbox/utils/queries';
|
||||||
import { play, soundCache } from 'soapbox/utils/sounds';
|
import { play, soundCache } from 'soapbox/utils/sounds';
|
||||||
|
|
||||||
|
@ -34,13 +34,6 @@ import type { APIEntity, Chat } from 'soapbox/types/entities';
|
||||||
const STREAMING_CHAT_UPDATE = 'STREAMING_CHAT_UPDATE';
|
const STREAMING_CHAT_UPDATE = 'STREAMING_CHAT_UPDATE';
|
||||||
const STREAMING_FOLLOW_RELATIONSHIPS_UPDATE = 'STREAMING_FOLLOW_RELATIONSHIPS_UPDATE';
|
const STREAMING_FOLLOW_RELATIONSHIPS_UPDATE = 'STREAMING_FOLLOW_RELATIONSHIPS_UPDATE';
|
||||||
|
|
||||||
const validLocale = (locale: string) => Object.keys(messages).includes(locale);
|
|
||||||
|
|
||||||
const getLocale = (state: RootState) => {
|
|
||||||
const locale = getSettings(state).get('locale') as string;
|
|
||||||
return validLocale(locale) ? locale : 'en';
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateFollowRelationships = (relationships: APIEntity) =>
|
const updateFollowRelationships = (relationships: APIEntity) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const me = getState().me;
|
const me = getState().me;
|
||||||
|
@ -81,7 +74,7 @@ const updateChatQuery = (chat: IChat) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface StreamOpts {
|
interface StreamOpts {
|
||||||
statContext?: IStatContext,
|
statContext?: IStatContext
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectTimelineStream = (
|
const connectTimelineStream = (
|
||||||
|
@ -170,6 +163,9 @@ const connectTimelineStream = (
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case 'chat_message.reaction': // TruthSocial
|
||||||
|
updateChatMessage(JSON.parse(data.payload));
|
||||||
|
break;
|
||||||
case 'pleroma:follow_relationships_update':
|
case 'pleroma:follow_relationships_update':
|
||||||
dispatch(updateFollowRelationships(JSON.parse(data.payload)));
|
dispatch(updateFollowRelationships(JSON.parse(data.payload)));
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { getSettings } from 'soapbox/actions/settings';
|
||||||
import { normalizeStatus } from 'soapbox/normalizers';
|
import { normalizeStatus } from 'soapbox/normalizers';
|
||||||
import { shouldFilter } from 'soapbox/utils/timelines';
|
import { shouldFilter } from 'soapbox/utils/timelines';
|
||||||
|
|
||||||
import api, { getLinks } from '../api';
|
import api, { getNextLink, getPrevLink } from '../api';
|
||||||
|
|
||||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ const parseTags = (tags: Record<string, any[]> = {}, mode: 'any' | 'all' | 'none
|
||||||
};
|
};
|
||||||
|
|
||||||
const replaceHomeTimeline = (
|
const replaceHomeTimeline = (
|
||||||
accountId: string | null,
|
accountId: string | undefined,
|
||||||
{ maxId }: Record<string, any> = {},
|
{ maxId }: Record<string, any> = {},
|
||||||
done?: () => void,
|
done?: () => void,
|
||||||
) => (dispatch: AppDispatch, _getState: () => RootState) => {
|
) => (dispatch: AppDispatch, _getState: () => RootState) => {
|
||||||
|
@ -162,7 +162,12 @@ const expandTimeline = (timelineId: string, path: string, params: Record<string,
|
||||||
return dispatch(noOpAsync());
|
return dispatch(noOpAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!params.max_id && !params.pinned && (timeline.items || ImmutableOrderedSet()).size > 0) {
|
if (
|
||||||
|
!params.max_id &&
|
||||||
|
!params.pinned &&
|
||||||
|
(timeline.items || ImmutableOrderedSet()).size > 0 &&
|
||||||
|
!path.includes('max_id=')
|
||||||
|
) {
|
||||||
params.since_id = timeline.getIn(['items', 0]);
|
params.since_id = timeline.getIn(['items', 0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,9 +176,16 @@ const expandTimeline = (timelineId: string, path: string, params: Record<string,
|
||||||
dispatch(expandTimelineRequest(timelineId, isLoadingMore));
|
dispatch(expandTimelineRequest(timelineId, isLoadingMore));
|
||||||
|
|
||||||
return api(getState).get(path, { params }).then(response => {
|
return api(getState).get(path, { params }).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
|
||||||
dispatch(importFetchedStatuses(response.data));
|
dispatch(importFetchedStatuses(response.data));
|
||||||
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore));
|
dispatch(expandTimelineSuccess(
|
||||||
|
timelineId,
|
||||||
|
response.data,
|
||||||
|
getNextLink(response),
|
||||||
|
getPrevLink(response),
|
||||||
|
response.status === 206,
|
||||||
|
isLoadingRecent,
|
||||||
|
isLoadingMore,
|
||||||
|
));
|
||||||
done();
|
done();
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
|
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
|
||||||
|
@ -181,9 +193,26 @@ const expandTimeline = (timelineId: string, path: string, params: Record<string,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const expandHomeTimeline = ({ accountId, maxId }: Record<string, any> = {}, done = noOp) => {
|
interface ExpandHomeTimelineOpts {
|
||||||
const endpoint = accountId ? `/api/v1/accounts/${accountId}/statuses` : '/api/v1/timelines/home';
|
accountId?: string
|
||||||
const params: any = { max_id: maxId };
|
maxId?: string
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HomeTimelineParams {
|
||||||
|
max_id?: string
|
||||||
|
exclude_replies?: boolean
|
||||||
|
with_muted?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandHomeTimeline = ({ url, accountId, maxId }: ExpandHomeTimelineOpts = {}, done = noOp) => {
|
||||||
|
const endpoint = url || (accountId ? `/api/v1/accounts/${accountId}/statuses` : '/api/v1/timelines/home');
|
||||||
|
const params: HomeTimelineParams = {};
|
||||||
|
|
||||||
|
if (!url && maxId) {
|
||||||
|
params.max_id = maxId;
|
||||||
|
}
|
||||||
|
|
||||||
if (accountId) {
|
if (accountId) {
|
||||||
params.exclude_replies = true;
|
params.exclude_replies = true;
|
||||||
params.with_muted = true;
|
params.with_muted = true;
|
||||||
|
@ -219,6 +248,12 @@ const expandListTimeline = (id: string, { maxId }: Record<string, any> = {}, don
|
||||||
const expandGroupTimeline = (id: string, { maxId }: Record<string, any> = {}, done = noOp) =>
|
const expandGroupTimeline = (id: string, { maxId }: Record<string, any> = {}, done = noOp) =>
|
||||||
expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { max_id: maxId }, done);
|
expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { max_id: maxId }, done);
|
||||||
|
|
||||||
|
const expandGroupTimelineFromTag = (id: string, tagName: string, { maxId }: Record<string, any> = {}, done = noOp) =>
|
||||||
|
expandTimeline(`group:tags:${id}:${tagName}`, `/api/v1/timelines/group/${id}/tags/${tagName}`, { max_id: maxId }, done);
|
||||||
|
|
||||||
|
const expandGroupMediaTimeline = (id: string | number, { maxId }: Record<string, any> = {}) =>
|
||||||
|
expandTimeline(`group:${id}:media`, `/api/v1/timelines/group/${id}`, { max_id: maxId, only_media: true, limit: 40, with_muted: true });
|
||||||
|
|
||||||
const expandHashtagTimeline = (hashtag: string, { maxId, tags }: Record<string, any> = {}, done = noOp) => {
|
const expandHashtagTimeline = (hashtag: string, { maxId, tags }: Record<string, any> = {}, done = noOp) => {
|
||||||
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
|
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||||
max_id: maxId,
|
max_id: maxId,
|
||||||
|
@ -234,11 +269,20 @@ const expandTimelineRequest = (timeline: string, isLoadingMore: boolean) => ({
|
||||||
skipLoading: !isLoadingMore,
|
skipLoading: !isLoadingMore,
|
||||||
});
|
});
|
||||||
|
|
||||||
const expandTimelineSuccess = (timeline: string, statuses: APIEntity[], next: string | null, partial: boolean, isLoadingRecent: boolean, isLoadingMore: boolean) => ({
|
const expandTimelineSuccess = (
|
||||||
|
timeline: string,
|
||||||
|
statuses: APIEntity[],
|
||||||
|
next: string | undefined,
|
||||||
|
prev: string | undefined,
|
||||||
|
partial: boolean,
|
||||||
|
isLoadingRecent: boolean,
|
||||||
|
isLoadingMore: boolean,
|
||||||
|
) => ({
|
||||||
type: TIMELINE_EXPAND_SUCCESS,
|
type: TIMELINE_EXPAND_SUCCESS,
|
||||||
timeline,
|
timeline,
|
||||||
statuses,
|
statuses,
|
||||||
next,
|
next,
|
||||||
|
prev,
|
||||||
partial,
|
partial,
|
||||||
isLoadingRecent,
|
isLoadingRecent,
|
||||||
skipLoading: !isLoadingMore,
|
skipLoading: !isLoadingMore,
|
||||||
|
@ -309,6 +353,8 @@ export {
|
||||||
expandAccountMediaTimeline,
|
expandAccountMediaTimeline,
|
||||||
expandListTimeline,
|
expandListTimeline,
|
||||||
expandGroupTimeline,
|
expandGroupTimeline,
|
||||||
|
expandGroupTimelineFromTag,
|
||||||
|
expandGroupMediaTimeline,
|
||||||
expandHashtagTimeline,
|
expandHashtagTimeline,
|
||||||
expandTimelineRequest,
|
expandTimelineRequest,
|
||||||
expandTimelineSuccess,
|
expandTimelineSuccess,
|
||||||
|
|
|
@ -31,14 +31,14 @@ const AGE: Challenge = 'age';
|
||||||
export type Challenge = 'age' | 'sms' | 'email'
|
export type Challenge = 'age' | 'sms' | 'email'
|
||||||
|
|
||||||
type Challenges = {
|
type Challenges = {
|
||||||
email?: 0 | 1,
|
email?: 0 | 1
|
||||||
sms?: 0 | 1,
|
sms?: 0 | 1
|
||||||
age?: 0 | 1,
|
age?: 0 | 1
|
||||||
}
|
}
|
||||||
|
|
||||||
type Verification = {
|
type Verification = {
|
||||||
token?: string,
|
token?: string
|
||||||
challenges?: Challenges,
|
challenges?: Challenges
|
||||||
challengeTypes?: Array<'age' | 'sms' | 'email'>
|
challengeTypes?: Array<'age' | 'sms' | 'email'>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,12 @@ export const getLinks = (response: AxiosResponse): LinkHeader => {
|
||||||
|
|
||||||
export const getNextLink = (response: AxiosResponse) => {
|
export const getNextLink = (response: AxiosResponse) => {
|
||||||
const nextLink = new LinkHeader(response.headers?.link);
|
const nextLink = new LinkHeader(response.headers?.link);
|
||||||
return nextLink.refs.find((ref) => ref.uri)?.uri;
|
return nextLink.refs.find(link => link.rel === 'next')?.uri;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPrevLink = (response: AxiosResponse) => {
|
||||||
|
const prevLink = new LinkHeader(response.headers?.link);
|
||||||
|
return prevLink.refs.find(link => link.rel === 'prev')?.uri;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const baseClient = (...params: any[]) => {
|
export const baseClient = (...params: any[]) => {
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi } from 'soapbox/hooks/useApi';
|
||||||
|
import { type Account, accountSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
|
||||||
|
import { useRelationships } from './useRelationships';
|
||||||
|
|
||||||
|
function useAccount(id: string) {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const { entity: account, ...result } = useEntity<Account>(
|
||||||
|
[Entities.ACCOUNTS, id],
|
||||||
|
() => api.get(`/api/v1/accounts/${id}`),
|
||||||
|
{ schema: accountSchema },
|
||||||
|
);
|
||||||
|
const { relationships, isLoading } = useRelationships([account?.id as string]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
isLoading: result.isLoading || isLoading,
|
||||||
|
account: account ? { ...account, relationship: relationships[0] || null } : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useAccount };
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi } from 'soapbox/hooks/useApi';
|
||||||
|
import { type Relationship, relationshipSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function useRelationships(ids: string[]) {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const { entities: relationships, ...result } = useEntities<Relationship>(
|
||||||
|
[Entities.RELATIONSHIPS],
|
||||||
|
() => api.get(`/api/v1/accounts/relationships?${ids.map(id => `id[]=${id}`).join('&')}`),
|
||||||
|
{ schema: relationshipSchema, enabled: ids.filter(Boolean).length > 0 },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
relationships,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useRelationships };
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntityActions } from 'soapbox/entity-store/hooks';
|
||||||
|
|
||||||
|
import type { Group, GroupMember } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function useBlockGroupMember(group: Group, groupMember: GroupMember) {
|
||||||
|
const { createEntity } = useEntityActions<GroupMember>(
|
||||||
|
[Entities.GROUP_MEMBERSHIPS, groupMember.id],
|
||||||
|
{ post: `/api/v1/groups/${group.id}/blocks` },
|
||||||
|
);
|
||||||
|
|
||||||
|
return createEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useBlockGroupMember };
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useCreateEntity } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi, useOwnAccount } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import type { Group } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function useCancelMembershipRequest(group: Group) {
|
||||||
|
const api = useApi();
|
||||||
|
const me = useOwnAccount();
|
||||||
|
|
||||||
|
const { createEntity, isSubmitting } = useCreateEntity(
|
||||||
|
[Entities.GROUP_RELATIONSHIPS],
|
||||||
|
() => api.post(`/api/v1/groups/${group.id}/membership_requests/${me?.id}/reject`),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mutate: createEntity,
|
||||||
|
isSubmitting,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useCancelMembershipRequest };
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useCreateEntity } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi } from 'soapbox/hooks/useApi';
|
||||||
|
import { groupSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
interface CreateGroupParams {
|
||||||
|
display_name?: string
|
||||||
|
note?: string
|
||||||
|
avatar?: File
|
||||||
|
header?: File
|
||||||
|
group_visibility?: 'members_only' | 'everyone'
|
||||||
|
discoverable?: boolean
|
||||||
|
tags?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function useCreateGroup() {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const { createEntity, ...rest } = useCreateEntity([Entities.GROUPS, 'search', ''], (params: CreateGroupParams) => {
|
||||||
|
return api.post('/api/v1/groups', params, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, { schema: groupSchema });
|
||||||
|
|
||||||
|
return {
|
||||||
|
createGroup: createEntity,
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useCreateGroup, type CreateGroupParams };
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntityActions } from 'soapbox/entity-store/hooks';
|
||||||
|
|
||||||
|
import type { Group } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function useDeleteGroup() {
|
||||||
|
const { deleteEntity, isSubmitting } = useEntityActions<Group>(
|
||||||
|
[Entities.GROUPS],
|
||||||
|
{ delete: '/api/v1/groups/:id' },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mutate: deleteEntity,
|
||||||
|
isSubmitting,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useDeleteGroup };
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntityActions } from 'soapbox/entity-store/hooks';
|
||||||
|
import { groupMemberSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import type { Group, GroupMember } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function useDemoteGroupMember(group: Group, groupMember: GroupMember) {
|
||||||
|
const { createEntity } = useEntityActions<GroupMember>(
|
||||||
|
[Entities.GROUP_MEMBERSHIPS, groupMember.id],
|
||||||
|
{ post: `/api/v1/groups/${group.id}/demote` },
|
||||||
|
{ schema: z.array(groupMemberSchema).transform((arr) => arr[0]) },
|
||||||
|
);
|
||||||
|
|
||||||
|
return createEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useDemoteGroupMember };
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi } from 'soapbox/hooks';
|
||||||
|
import { type Group, groupSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import { useGroupRelationship } from './useGroupRelationship';
|
||||||
|
|
||||||
|
function useGroup(groupId: string, refetch = true) {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const { entity: group, ...result } = useEntity<Group>(
|
||||||
|
[Entities.GROUPS, groupId],
|
||||||
|
() => api.get(`/api/v1/groups/${groupId}`),
|
||||||
|
{ schema: groupSchema, refetch },
|
||||||
|
);
|
||||||
|
const { entity: relationship } = useGroupRelationship(groupId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
group: group ? { ...group, relationship: relationship || null } : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useGroup };
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntityLookup } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi } from 'soapbox/hooks/useApi';
|
||||||
|
import { groupSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function useGroupLookup(slug: string) {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
return useEntityLookup(
|
||||||
|
Entities.GROUPS,
|
||||||
|
(group) => group.slug === slug,
|
||||||
|
() => api.get(`/api/v1/groups/lookup?name=${slug}`),
|
||||||
|
{ schema: groupSchema },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useGroupLookup };
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi } from 'soapbox/hooks/useApi';
|
||||||
|
import { statusSchema } from 'soapbox/schemas/status';
|
||||||
|
|
||||||
|
function useGroupMedia(groupId: string) {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
return useEntities([Entities.STATUSES, 'groupMedia', groupId], () => {
|
||||||
|
return api.get(`/api/v1/timelines/group/${groupId}?only_media=true`);
|
||||||
|
}, { schema: statusSchema });
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useGroupMedia };
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||||
|
import { GroupMember, groupMemberSchema } from 'soapbox/schemas';
|
||||||
|
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||||
|
|
||||||
|
import { useApi } from '../../../hooks/useApi';
|
||||||
|
|
||||||
|
function useGroupMembers(groupId: string, role: GroupRoles) {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const { entities, ...result } = useEntities<GroupMember>(
|
||||||
|
[Entities.GROUP_MEMBERSHIPS, groupId, role],
|
||||||
|
() => api.get(`/api/v1/groups/${groupId}/memberships?role=${role}`),
|
||||||
|
{ schema: groupMemberSchema },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
groupMembers: entities,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useGroupMembers };
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useDismissEntity, useEntities } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi } from 'soapbox/hooks/useApi';
|
||||||
|
import { accountSchema } from 'soapbox/schemas';
|
||||||
|
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||||
|
|
||||||
|
import { useGroupRelationship } from './useGroupRelationship';
|
||||||
|
|
||||||
|
import type { ExpandedEntitiesPath } from 'soapbox/entity-store/hooks/types';
|
||||||
|
|
||||||
|
function useGroupMembershipRequests(groupId: string) {
|
||||||
|
const api = useApi();
|
||||||
|
const path: ExpandedEntitiesPath = [Entities.ACCOUNTS, 'membership_requests', groupId];
|
||||||
|
|
||||||
|
const { entity: relationship } = useGroupRelationship(groupId);
|
||||||
|
|
||||||
|
const { entities, invalidate, ...rest } = useEntities(
|
||||||
|
path,
|
||||||
|
() => api.get(`/api/v1/groups/${groupId}/membership_requests`),
|
||||||
|
{
|
||||||
|
schema: accountSchema,
|
||||||
|
enabled: relationship?.role === GroupRoles.OWNER || relationship?.role === GroupRoles.ADMIN,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { dismissEntity: authorize } = useDismissEntity(path, async (accountId: string) => {
|
||||||
|
const response = await api.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/authorize`);
|
||||||
|
invalidate();
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { dismissEntity: reject } = useDismissEntity(path, async (accountId: string) => {
|
||||||
|
const response = await api.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/reject`);
|
||||||
|
invalidate();
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
accounts: entities,
|
||||||
|
authorize,
|
||||||
|
reject,
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useGroupMembershipRequests };
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { fetchGroupRelationshipsSuccess } from 'soapbox/actions/groups';
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi, useAppDispatch } from 'soapbox/hooks';
|
||||||
|
import { type GroupRelationship, groupRelationshipSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function useGroupRelationship(groupId: string) {
|
||||||
|
const api = useApi();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const { entity: groupRelationship, ...result } = useEntity<GroupRelationship>(
|
||||||
|
[Entities.GROUP_RELATIONSHIPS, groupId],
|
||||||
|
() => api.get(`/api/v1/groups/relationships?id[]=${groupId}`),
|
||||||
|
{ schema: z.array(groupRelationshipSchema).transform(arr => arr[0]) },
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (groupRelationship?.id) {
|
||||||
|
dispatch(fetchGroupRelationshipsSuccess([groupRelationship]));
|
||||||
|
}
|
||||||
|
}, [groupRelationship?.id]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
entity: groupRelationship,
|
||||||
|
...result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useGroupRelationship };
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi } from 'soapbox/hooks';
|
||||||
|
import { type GroupRelationship, groupRelationshipSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function useGroupRelationships(groupIds: string[]) {
|
||||||
|
const api = useApi();
|
||||||
|
const q = groupIds.map(id => `id[]=${id}`).join('&');
|
||||||
|
|
||||||
|
const { entities, ...result } = useEntities<GroupRelationship>(
|
||||||
|
[Entities.GROUP_RELATIONSHIPS, ...groupIds],
|
||||||
|
() => api.get(`/api/v1/groups/relationships?${q}`),
|
||||||
|
{ schema: groupRelationshipSchema, enabled: groupIds.length > 0 },
|
||||||
|
);
|
||||||
|
|
||||||
|
const relationships = entities.reduce<Record<string, GroupRelationship>>((map, relationship) => {
|
||||||
|
map[relationship.id] = relationship;
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
relationships,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useGroupRelationships };
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi, useFeatures } from 'soapbox/hooks';
|
||||||
|
import { groupSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import { useGroupRelationships } from './useGroupRelationships';
|
||||||
|
|
||||||
|
import type { Group } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function useGroupSearch(search: string) {
|
||||||
|
const api = useApi();
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
|
const { entities, ...result } = useEntities<Group>(
|
||||||
|
[Entities.GROUPS, 'discover', 'search', search],
|
||||||
|
() => api.get('/api/v1/groups/search', {
|
||||||
|
params: {
|
||||||
|
q: search,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ enabled: features.groupsDiscovery && !!search, schema: groupSchema },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
|
||||||
|
|
||||||
|
const groups = entities.map((group) => ({
|
||||||
|
...group,
|
||||||
|
relationship: relationships[group.id] || null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
groups,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useGroupSearch };
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi } from 'soapbox/hooks';
|
||||||
|
import { type GroupTag, groupTagSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function useGroupTag(tagId: string) {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const { entity: tag, ...result } = useEntity<GroupTag>(
|
||||||
|
[Entities.GROUP_TAGS, tagId],
|
||||||
|
() => api.get(`/api/v1/tags/${tagId }`),
|
||||||
|
{ schema: groupTagSchema },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
tag,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useGroupTag };
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi } from 'soapbox/hooks/useApi';
|
||||||
|
import { groupTagSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import type { GroupTag } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function useGroupTags(groupId: string) {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const { entities, ...result } = useEntities<GroupTag>(
|
||||||
|
[Entities.GROUP_TAGS, groupId],
|
||||||
|
() => api.get(`/api/v1/truth/trends/groups/${groupId}/tags`),
|
||||||
|
{ schema: groupTagSchema },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
tags: entities,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useGroupTags };
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useApi } from 'soapbox/hooks/useApi';
|
||||||
|
import { useFeatures } from 'soapbox/hooks/useFeatures';
|
||||||
|
|
||||||
|
type Validation = {
|
||||||
|
error: string
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ValidationKeys = {
|
||||||
|
validation: (name: string) => ['group', 'validation', name] as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
function useGroupValidation(name: string = '') {
|
||||||
|
const api = useApi();
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
|
const getValidation = async() => {
|
||||||
|
const { data } = await api.get<Validation>('/api/v1/groups/validate', {
|
||||||
|
params: { name },
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (error.response.status === 422) {
|
||||||
|
return { data: error.response.data };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryInfo = useQuery<Validation>(ValidationKeys.validation(name), getValidation, {
|
||||||
|
enabled: features.groupsValidation && !!name,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...queryInfo,
|
||||||
|
data: {
|
||||||
|
...queryInfo.data,
|
||||||
|
isValid: !queryInfo.data?.error ?? true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useGroupValidation };
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi } from 'soapbox/hooks';
|
||||||
|
import { useFeatures } from 'soapbox/hooks/useFeatures';
|
||||||
|
import { groupSchema, type Group } from 'soapbox/schemas/group';
|
||||||
|
|
||||||
|
import { useGroupRelationships } from './useGroupRelationships';
|
||||||
|
|
||||||
|
function useGroups(q: string = '') {
|
||||||
|
const api = useApi();
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
|
const { entities, ...result } = useEntities<Group>(
|
||||||
|
[Entities.GROUPS, 'search', q],
|
||||||
|
() => api.get('/api/v1/groups', { params: { q } }),
|
||||||
|
{ enabled: features.groups, schema: groupSchema },
|
||||||
|
);
|
||||||
|
const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
|
||||||
|
|
||||||
|
const groups = entities.map((group) => ({
|
||||||
|
...group,
|
||||||
|
relationship: relationships[group.id] || null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
groups,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useGroups };
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi, useFeatures } from 'soapbox/hooks';
|
||||||
|
import { groupSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import { useGroupRelationships } from './useGroupRelationships';
|
||||||
|
|
||||||
|
import type { Group } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function useGroupsFromTag(tagId: string) {
|
||||||
|
const api = useApi();
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
|
const { entities, ...result } = useEntities<Group>(
|
||||||
|
[Entities.GROUPS, 'tags', tagId],
|
||||||
|
() => api.get(`/api/v1/tags/${tagId}/groups`),
|
||||||
|
{
|
||||||
|
schema: groupSchema,
|
||||||
|
enabled: features.groupsDiscovery,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
|
||||||
|
|
||||||
|
const groups = entities.map((group) => ({
|
||||||
|
...group,
|
||||||
|
relationship: relationships[group.id] || null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
groups,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useGroupsFromTag };
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntityActions } from 'soapbox/entity-store/hooks';
|
||||||
|
import { groupRelationshipSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import { useGroups } from './useGroups';
|
||||||
|
|
||||||
|
import type { Group, GroupRelationship } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function useJoinGroup(group: Group) {
|
||||||
|
const { invalidate } = useGroups();
|
||||||
|
|
||||||
|
const { createEntity, isSubmitting } = useEntityActions<GroupRelationship>(
|
||||||
|
[Entities.GROUP_RELATIONSHIPS, group.id],
|
||||||
|
{ post: `/api/v1/groups/${group.id}/join` },
|
||||||
|
{ schema: groupRelationshipSchema },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mutate: createEntity,
|
||||||
|
isSubmitting,
|
||||||
|
invalidate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useJoinGroup };
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntityActions } from 'soapbox/entity-store/hooks';
|
||||||
|
import { groupRelationshipSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import { useGroups } from './useGroups';
|
||||||
|
|
||||||
|
import type { Group, GroupRelationship } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function useLeaveGroup(group: Group) {
|
||||||
|
const { invalidate } = useGroups();
|
||||||
|
|
||||||
|
const { createEntity, isSubmitting } = useEntityActions<GroupRelationship>(
|
||||||
|
[Entities.GROUP_RELATIONSHIPS, group.id],
|
||||||
|
{ post: `/api/v1/groups/${group.id}/leave` },
|
||||||
|
{ schema: groupRelationshipSchema },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mutate: createEntity,
|
||||||
|
isSubmitting,
|
||||||
|
invalidate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useLeaveGroup };
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||||
|
import { Group, groupSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import { useApi } from '../../../hooks/useApi';
|
||||||
|
import { useFeatures } from '../../../hooks/useFeatures';
|
||||||
|
|
||||||
|
import { useGroupRelationships } from './useGroupRelationships';
|
||||||
|
|
||||||
|
function usePopularGroups() {
|
||||||
|
const api = useApi();
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
|
const { entities, ...result } = useEntities<Group>(
|
||||||
|
[Entities.GROUPS, 'popular'],
|
||||||
|
() => api.get('/api/v1/truth/trends/groups'),
|
||||||
|
{
|
||||||
|
schema: groupSchema,
|
||||||
|
enabled: features.groupsDiscovery,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
|
||||||
|
|
||||||
|
const groups = entities.map((group) => ({
|
||||||
|
...group,
|
||||||
|
relationship: relationships[group.id] || null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
groups,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { usePopularGroups };
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi, useFeatures } from 'soapbox/hooks';
|
||||||
|
import { type GroupTag, groupTagSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function usePopularTags() {
|
||||||
|
const api = useApi();
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
|
const { entities, ...result } = useEntities<GroupTag>(
|
||||||
|
[Entities.GROUP_TAGS],
|
||||||
|
() => api.get('/api/v1/groups/tags'),
|
||||||
|
{
|
||||||
|
schema: groupTagSchema,
|
||||||
|
enabled: features.groupsDiscovery,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
tags: entities,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { usePopularTags };
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntityActions } from 'soapbox/entity-store/hooks';
|
||||||
|
import { groupMemberSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import type { Group, GroupMember } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function usePromoteGroupMember(group: Group, groupMember: GroupMember) {
|
||||||
|
const { createEntity } = useEntityActions<GroupMember>(
|
||||||
|
[Entities.GROUP_MEMBERSHIPS, groupMember.id],
|
||||||
|
{ post: `/api/v1/groups/${group.id}/promote` },
|
||||||
|
{ schema: z.array(groupMemberSchema).transform((arr) => arr[0]) },
|
||||||
|
);
|
||||||
|
|
||||||
|
return createEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { usePromoteGroupMember };
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi, useFeatures } from 'soapbox/hooks';
|
||||||
|
import { type Group, groupSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import { useGroupRelationships } from './useGroupRelationships';
|
||||||
|
|
||||||
|
function useSuggestedGroups() {
|
||||||
|
const api = useApi();
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
|
const { entities, ...result } = useEntities<Group>(
|
||||||
|
[Entities.GROUPS, 'suggested'],
|
||||||
|
() => api.get('/api/v1/truth/suggestions/groups'),
|
||||||
|
{
|
||||||
|
schema: groupSchema,
|
||||||
|
enabled: features.groupsDiscovery,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
|
||||||
|
|
||||||
|
const groups = entities.map((group) => ({
|
||||||
|
...group,
|
||||||
|
relationship: relationships[group.id] || null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
groups,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useSuggestedGroups };
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useCreateEntity } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi } from 'soapbox/hooks/useApi';
|
||||||
|
import { groupSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
interface UpdateGroupParams {
|
||||||
|
display_name?: string
|
||||||
|
note?: string
|
||||||
|
avatar?: File
|
||||||
|
header?: File
|
||||||
|
group_visibility?: string
|
||||||
|
discoverable?: boolean
|
||||||
|
tags?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function useUpdateGroup(groupId: string) {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const { createEntity, ...rest } = useCreateEntity([Entities.GROUPS], (params: UpdateGroupParams) => {
|
||||||
|
return api.put(`/api/v1/groups/${groupId}`, params, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, { schema: groupSchema });
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateGroup: createEntity,
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useUpdateGroup };
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntityActions } from 'soapbox/entity-store/hooks';
|
||||||
|
|
||||||
|
import type { GroupTag } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function useUpdateGroupTag(groupId: string, tagId: string) {
|
||||||
|
const { updateEntity, ...rest } = useEntityActions<GroupTag>(
|
||||||
|
[Entities.GROUP_TAGS, groupId, tagId],
|
||||||
|
{ patch: `/api/v1/groups/${groupId}/tags/${tagId}` },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateGroupTag: updateEntity,
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useUpdateGroupTag };
|
|
@ -0,0 +1,40 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accounts
|
||||||
|
*/
|
||||||
|
export { useAccount } from './accounts/useAccount';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups
|
||||||
|
*/
|
||||||
|
export { useBlockGroupMember } from './groups/useBlockGroupMember';
|
||||||
|
export { useCancelMembershipRequest } from './groups/useCancelMembershipRequest';
|
||||||
|
export { useCreateGroup, type CreateGroupParams } from './groups/useCreateGroup';
|
||||||
|
export { useDeleteGroup } from './groups/useDeleteGroup';
|
||||||
|
export { useDemoteGroupMember } from './groups/useDemoteGroupMember';
|
||||||
|
export { useGroup } from './groups/useGroup';
|
||||||
|
export { useGroupLookup } from './groups/useGroupLookup';
|
||||||
|
export { useGroupMedia } from './groups/useGroupMedia';
|
||||||
|
export { useGroupMembers } from './groups/useGroupMembers';
|
||||||
|
export { useGroupMembershipRequests } from './groups/useGroupMembershipRequests';
|
||||||
|
export { useGroupRelationship } from './groups/useGroupRelationship';
|
||||||
|
export { useGroupRelationships } from './groups/useGroupRelationships';
|
||||||
|
export { useGroupSearch } from './groups/useGroupSearch';
|
||||||
|
export { useGroupTag } from './groups/useGroupTag';
|
||||||
|
export { useGroupTags } from './groups/useGroupTags';
|
||||||
|
export { useGroupValidation } from './groups/useGroupValidation';
|
||||||
|
export { useGroups } from './groups/useGroups';
|
||||||
|
export { useGroupsFromTag } from './groups/useGroupsFromTag';
|
||||||
|
export { useJoinGroup } from './groups/useJoinGroup';
|
||||||
|
export { useLeaveGroup } from './groups/useLeaveGroup';
|
||||||
|
export { usePopularGroups } from './groups/usePopularGroups';
|
||||||
|
export { usePopularTags } from './groups/usePopularTags';
|
||||||
|
export { usePromoteGroupMember } from './groups/usePromoteGroupMember';
|
||||||
|
export { useSuggestedGroups } from './groups/useSuggestedGroups';
|
||||||
|
export { useUpdateGroup } from './groups/useUpdateGroup';
|
||||||
|
export { useUpdateGroupTag } from './groups/useUpdateGroupTag';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relationships
|
||||||
|
*/
|
||||||
|
export { useRelationships } from './accounts/useRelationships';
|
|
@ -29,6 +29,10 @@ export const getNextLink = (response: AxiosResponse): string | undefined => {
|
||||||
return getLinks(response).refs.find(link => link.rel === 'next')?.uri;
|
return getLinks(response).refs.find(link => link.rel === 'next')?.uri;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getPrevLink = (response: AxiosResponse): string | undefined => {
|
||||||
|
return getLinks(response).refs.find(link => link.rel === 'prev')?.uri;
|
||||||
|
};
|
||||||
|
|
||||||
const getToken = (state: RootState, authType: string) => {
|
const getToken = (state: RootState, authType: string) => {
|
||||||
return authType === 'app' ? getAppToken(state) : getAccessToken(state);
|
return authType === 'app' ? getAppToken(state) : getAccessToken(state);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface IInlineSVG {
|
interface IInlineSVG {
|
||||||
loader?: JSX.Element,
|
loader?: JSX.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
const InlineSVG: React.FC<IInlineSVG> = ({ loader }): JSX.Element => {
|
const InlineSVG: React.FC<IInlineSVG> = ({ loader }): JSX.Element => {
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { render, screen } from '../../jest/test-helpers';
|
|
||||||
import EmojiSelector from '../emoji-selector';
|
|
||||||
|
|
||||||
describe('<EmojiSelector />', () => {
|
|
||||||
it('renders correctly', () => {
|
|
||||||
const children = <EmojiSelector />;
|
|
||||||
// @ts-ignore
|
|
||||||
children.__proto__.addEventListener = () => {};
|
|
||||||
|
|
||||||
render(children);
|
|
||||||
|
|
||||||
expect(screen.queryAllByRole('button')).toHaveLength(6);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,4 +1,4 @@
|
||||||
import classNames from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
@ -12,9 +12,9 @@ const messages = defineMessages({
|
||||||
|
|
||||||
interface IAccountSearch {
|
interface IAccountSearch {
|
||||||
/** Callback when a searched account is chosen. */
|
/** Callback when a searched account is chosen. */
|
||||||
onSelected: (accountId: string) => void,
|
onSelected: (accountId: string) => void
|
||||||
/** Override the default placeholder of the input. */
|
/** Override the default placeholder of the input. */
|
||||||
placeholder?: string,
|
placeholder?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Input to search for accounts. */
|
/** Input to search for accounts. */
|
||||||
|
@ -72,17 +72,17 @@ const AccountSearch: React.FC<IAccountSearch> = ({ onSelected, ...rest }) => {
|
||||||
<div
|
<div
|
||||||
role='button'
|
role='button'
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className='absolute inset-y-0 right-0 px-3 flex items-center cursor-pointer'
|
className='absolute inset-y-0 right-0 flex cursor-pointer items-center px-3'
|
||||||
onClick={handleClear}
|
onClick={handleClear}
|
||||||
>
|
>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
src={require('@tabler/icons/search.svg')}
|
src={require('@tabler/icons/search.svg')}
|
||||||
className={classNames('h-4 w-4 text-gray-400', { hidden: !isEmpty() })}
|
className={clsx('h-4 w-4 text-gray-400', { hidden: !isEmpty() })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
src={require('@tabler/icons/x.svg')}
|
src={require('@tabler/icons/x.svg')}
|
||||||
className={classNames('h-4 w-4 text-gray-400', { hidden: isEmpty() })}
|
className={clsx('h-4 w-4 text-gray-400', { hidden: isEmpty() })}
|
||||||
aria-label={intl.formatMessage(messages.placeholder)}
|
aria-label={intl.formatMessage(messages.placeholder)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,23 +1,31 @@
|
||||||
import React from 'react';
|
import React, { useRef } from 'react';
|
||||||
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
|
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
|
||||||
import VerificationBadge from 'soapbox/components/verification-badge';
|
import VerificationBadge from 'soapbox/components/verification-badge';
|
||||||
import ActionButton from 'soapbox/features/ui/components/action-button';
|
import ActionButton from 'soapbox/features/ui/components/action-button';
|
||||||
import { useAppSelector, useOnScreen } from 'soapbox/hooks';
|
import { useAppSelector } from 'soapbox/hooks';
|
||||||
import { getAcct } from 'soapbox/utils/accounts';
|
import { getAcct } from 'soapbox/utils/accounts';
|
||||||
import { displayFqn } from 'soapbox/utils/state';
|
import { displayFqn } from 'soapbox/utils/state';
|
||||||
|
|
||||||
|
import Badge from './badge';
|
||||||
import RelativeTimestamp from './relative-timestamp';
|
import RelativeTimestamp from './relative-timestamp';
|
||||||
import { Avatar, Emoji, HStack, Icon, IconButton, Stack, Text } from './ui';
|
import { Avatar, Emoji, HStack, Icon, IconButton, Stack, Text } from './ui';
|
||||||
|
|
||||||
|
import type { StatusApprovalStatus } from 'soapbox/normalizers/status';
|
||||||
|
import type { Account as AccountSchema } from 'soapbox/schemas';
|
||||||
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
interface IInstanceFavicon {
|
interface IInstanceFavicon {
|
||||||
account: AccountEntity,
|
account: AccountEntity | AccountSchema
|
||||||
disabled?: boolean,
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
bot: { id: 'account.badges.bot', defaultMessage: 'Bot' },
|
||||||
|
});
|
||||||
|
|
||||||
const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
|
const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
|
@ -36,17 +44,17 @@ const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className='w-4 h-4 flex-none focus:ring-primary-500 focus:ring-2 focus:ring-offset-2'
|
className='h-4 w-4 flex-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2'
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<img src={account.favicon} alt='' title={account.domain} className='w-full max-h-full' />
|
<img src={account.favicon} alt='' title={account.domain} className='max-h-full w-full' />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IProfilePopper {
|
interface IProfilePopper {
|
||||||
condition: boolean,
|
condition: boolean
|
||||||
wrapper: (children: React.ReactNode) => React.ReactNode
|
wrapper: (children: React.ReactNode) => React.ReactNode
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
@ -60,29 +68,31 @@ const ProfilePopper: React.FC<IProfilePopper> = ({ condition, wrapper, children
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IAccount {
|
export interface IAccount {
|
||||||
account: AccountEntity,
|
account: AccountEntity | AccountSchema
|
||||||
action?: React.ReactElement,
|
action?: React.ReactElement
|
||||||
actionAlignment?: 'center' | 'top',
|
actionAlignment?: 'center' | 'top'
|
||||||
actionIcon?: string,
|
actionIcon?: string
|
||||||
actionTitle?: string,
|
actionTitle?: string
|
||||||
/** Override other actions for specificity like mute/unmute. */
|
/** Override other actions for specificity like mute/unmute. */
|
||||||
actionType?: 'muting' | 'blocking' | 'follow_request',
|
actionType?: 'muting' | 'blocking' | 'follow_request'
|
||||||
avatarSize?: number,
|
avatarSize?: number
|
||||||
hidden?: boolean,
|
hidden?: boolean
|
||||||
hideActions?: boolean,
|
hideActions?: boolean
|
||||||
id?: string,
|
id?: string
|
||||||
onActionClick?: (account: any) => void,
|
onActionClick?: (account: any) => void
|
||||||
showProfileHoverCard?: boolean,
|
showProfileHoverCard?: boolean
|
||||||
timestamp?: string,
|
timestamp?: string
|
||||||
timestampUrl?: string,
|
timestampUrl?: string
|
||||||
futureTimestamp?: boolean,
|
futureTimestamp?: boolean
|
||||||
withAccountNote?: boolean,
|
withAccountNote?: boolean
|
||||||
withDate?: boolean,
|
withDate?: boolean
|
||||||
withLinkToProfile?: boolean,
|
withLinkToProfile?: boolean
|
||||||
withRelationship?: boolean,
|
withRelationship?: boolean
|
||||||
showEdit?: boolean,
|
showEdit?: boolean
|
||||||
emoji?: string,
|
approvalStatus?: StatusApprovalStatus
|
||||||
note?: string,
|
emoji?: string
|
||||||
|
emojiUrl?: string
|
||||||
|
note?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const Account = ({
|
const Account = ({
|
||||||
|
@ -105,22 +115,19 @@ const Account = ({
|
||||||
withLinkToProfile = true,
|
withLinkToProfile = true,
|
||||||
withRelationship = true,
|
withRelationship = true,
|
||||||
showEdit = false,
|
showEdit = false,
|
||||||
|
approvalStatus,
|
||||||
emoji,
|
emoji,
|
||||||
|
emojiUrl,
|
||||||
note,
|
note,
|
||||||
}: IAccount) => {
|
}: IAccount) => {
|
||||||
const overflowRef = React.useRef<HTMLDivElement>(null);
|
const overflowRef = useRef<HTMLDivElement>(null);
|
||||||
const actionRef = React.useRef<HTMLDivElement>(null);
|
const actionRef = useRef<HTMLDivElement>(null);
|
||||||
// @ts-ignore
|
|
||||||
const isOnScreen = useOnScreen(overflowRef);
|
|
||||||
|
|
||||||
const [style, setStyle] = React.useState<React.CSSProperties>({ visibility: 'hidden' });
|
|
||||||
|
|
||||||
const me = useAppSelector((state) => state.me);
|
const me = useAppSelector((state) => state.me);
|
||||||
const username = useAppSelector((state) => account ? getAcct(account, displayFqn(state)) : null);
|
const username = useAppSelector((state) => account ? getAcct(account, displayFqn(state)) : null);
|
||||||
|
|
||||||
const handleAction = () => {
|
const handleAction = () => {
|
||||||
// @ts-ignore
|
onActionClick!(account);
|
||||||
onActionClick(account);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAction = () => {
|
const renderAction = () => {
|
||||||
|
@ -138,8 +145,8 @@ const Account = ({
|
||||||
src={actionIcon}
|
src={actionIcon}
|
||||||
title={actionTitle}
|
title={actionTitle}
|
||||||
onClick={handleAction}
|
onClick={handleAction}
|
||||||
className='bg-transparent text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-500'
|
className='bg-transparent text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-500'
|
||||||
iconClassName='w-4 h-4'
|
iconClassName='h-4 w-4'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -151,18 +158,7 @@ const Account = ({
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
const intl = useIntl();
|
||||||
const style: React.CSSProperties = {};
|
|
||||||
const actionWidth = actionRef.current?.clientWidth || 0;
|
|
||||||
|
|
||||||
if (overflowRef.current) {
|
|
||||||
style.maxWidth = overflowRef.current.clientWidth - 30 - avatarSize - actionWidth;
|
|
||||||
} else {
|
|
||||||
style.visibility = 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
setStyle(style);
|
|
||||||
}, [isOnScreen, overflowRef, actionRef]);
|
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -182,9 +178,9 @@ const Account = ({
|
||||||
const LinkEl: any = withLinkToProfile ? Link : 'div';
|
const LinkEl: any = withLinkToProfile ? Link : 'div';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid='account' className='flex-shrink-0 group block w-full' ref={overflowRef}>
|
<div data-testid='account' className='group block w-full shrink-0' ref={overflowRef}>
|
||||||
<HStack alignItems={actionAlignment} justifyContent='between'>
|
<HStack alignItems={actionAlignment} justifyContent='between'>
|
||||||
<HStack alignItems={withAccountNote || note ? 'top' : 'center'} space={3}>
|
<HStack alignItems={withAccountNote || note ? 'top' : 'center'} space={3} className='overflow-hidden'>
|
||||||
<ProfilePopper
|
<ProfilePopper
|
||||||
condition={showProfileHoverCard}
|
condition={showProfileHoverCard}
|
||||||
wrapper={(children) => <HoverRefWrapper className='relative' accountId={account.id} inline>{children}</HoverRefWrapper>}
|
wrapper={(children) => <HoverRefWrapper className='relative' accountId={account.id} inline>{children}</HoverRefWrapper>}
|
||||||
|
@ -197,14 +193,15 @@ const Account = ({
|
||||||
<Avatar src={account.avatar} size={avatarSize} />
|
<Avatar src={account.avatar} size={avatarSize} />
|
||||||
{emoji && (
|
{emoji && (
|
||||||
<Emoji
|
<Emoji
|
||||||
className='w-5 h-5 absolute -bottom-1.5 -right-1.5'
|
className='absolute -right-1.5 bottom-0 h-5 w-5'
|
||||||
emoji={emoji}
|
emoji={emoji}
|
||||||
|
src={emojiUrl}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</LinkEl>
|
</LinkEl>
|
||||||
</ProfilePopper>
|
</ProfilePopper>
|
||||||
|
|
||||||
<div className='flex-grow'>
|
<div className='grow overflow-hidden'>
|
||||||
<ProfilePopper
|
<ProfilePopper
|
||||||
condition={showProfileHoverCard}
|
condition={showProfileHoverCard}
|
||||||
wrapper={(children) => <HoverRefWrapper accountId={account.id} inline>{children}</HoverRefWrapper>}
|
wrapper={(children) => <HoverRefWrapper accountId={account.id} inline>{children}</HoverRefWrapper>}
|
||||||
|
@ -214,7 +211,7 @@ const Account = ({
|
||||||
title={account.acct}
|
title={account.acct}
|
||||||
onClick={(event: React.MouseEvent) => event.stopPropagation()}
|
onClick={(event: React.MouseEvent) => event.stopPropagation()}
|
||||||
>
|
>
|
||||||
<HStack space={1} alignItems='center' grow style={style}>
|
<HStack space={1} alignItems='center' grow>
|
||||||
<Text
|
<Text
|
||||||
size='sm'
|
size='sm'
|
||||||
weight='semibold'
|
weight='semibold'
|
||||||
|
@ -223,12 +220,14 @@ const Account = ({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{account.verified && <VerificationBadge />}
|
{account.verified && <VerificationBadge />}
|
||||||
|
|
||||||
|
{account.bot && <Badge slug='bot' title={intl.formatMessage(messages.bot)} />}
|
||||||
</HStack>
|
</HStack>
|
||||||
</LinkEl>
|
</LinkEl>
|
||||||
</ProfilePopper>
|
</ProfilePopper>
|
||||||
|
|
||||||
<Stack space={withAccountNote || note ? 1 : 0}>
|
<Stack space={withAccountNote || note ? 1 : 0}>
|
||||||
<HStack alignItems='center' space={1} style={style}>
|
<HStack alignItems='center' space={1}>
|
||||||
<Text theme='muted' size='sm' direction='ltr' truncate>@{username}</Text>
|
<Text theme='muted' size='sm' direction='ltr' truncate>@{username}</Text>
|
||||||
|
|
||||||
{account.favicon && (
|
{account.favicon && (
|
||||||
|
@ -249,6 +248,18 @@ const Account = ({
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{approvalStatus && ['pending', 'rejected'].includes(approvalStatus) && (
|
||||||
|
<>
|
||||||
|
<Text tag='span' theme='muted' size='sm'>·</Text>
|
||||||
|
|
||||||
|
<Text tag='span' theme='muted' size='sm'>
|
||||||
|
{approvalStatus === 'pending'
|
||||||
|
? <FormattedMessage id='status.approval.pending' defaultMessage='Pending approval' />
|
||||||
|
: <FormattedMessage id='status.approval.rejected' defaultMessage='Rejected' />}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{showEdit ? (
|
{showEdit ? (
|
||||||
<>
|
<>
|
||||||
<Text tag='span' theme='muted' size='sm'>·</Text>
|
<Text tag='span' theme='muted' size='sm'>·</Text>
|
||||||
|
|
|
@ -15,8 +15,8 @@ const obfuscatedCount = (count: number) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IAnimatedNumber {
|
interface IAnimatedNumber {
|
||||||
value: number;
|
value: number
|
||||||
obfuscate?: boolean;
|
obfuscate?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const AnimatedNumber: React.FC<IAnimatedNumber> = ({ value, obfuscate }) => {
|
const AnimatedNumber: React.FC<IAnimatedNumber> = ({ value, obfuscate }) => {
|
||||||
|
@ -50,7 +50,7 @@ const AnimatedNumber: React.FC<IAnimatedNumber> = ({ value, obfuscate }) => {
|
||||||
return (
|
return (
|
||||||
<TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
|
<TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
|
||||||
{items => (
|
{items => (
|
||||||
<span className='inline-flex flex-col items-stretch relative overflow-hidden'>
|
<span className='relative inline-flex flex-col items-stretch overflow-hidden'>
|
||||||
{items.map(({ key, data, style }) => (
|
{items.map(({ key, data, style }) => (
|
||||||
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <FormattedNumber value={data} />}</span>
|
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <FormattedNumber value={data} />}</span>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useHistory } from 'react-router-dom';
|
||||||
import type { Announcement as AnnouncementEntity, Mention as MentionEntity } from 'soapbox/types/entities';
|
import type { Announcement as AnnouncementEntity, Mention as MentionEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
interface IAnnouncementContent {
|
interface IAnnouncementContent {
|
||||||
announcement: AnnouncementEntity;
|
announcement: AnnouncementEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
const AnnouncementContent: React.FC<IAnnouncementContent> = ({ announcement }) => {
|
const AnnouncementContent: React.FC<IAnnouncementContent> = ({ announcement }) => {
|
||||||
|
|
|
@ -11,10 +11,10 @@ import type { Map as ImmutableMap } from 'immutable';
|
||||||
import type { Announcement as AnnouncementEntity } from 'soapbox/types/entities';
|
import type { Announcement as AnnouncementEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
interface IAnnouncement {
|
interface IAnnouncement {
|
||||||
announcement: AnnouncementEntity;
|
announcement: AnnouncementEntity
|
||||||
addReaction: (id: string, name: string) => void;
|
addReaction: (id: string, name: string) => void
|
||||||
removeReaction: (id: string, name: string) => void;
|
removeReaction: (id: string, name: string) => void
|
||||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const Announcement: React.FC<IAnnouncement> = ({ announcement, addReaction, removeReaction, emojiMap }) => {
|
const Announcement: React.FC<IAnnouncement> = ({ announcement, addReaction, removeReaction, emojiMap }) => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import classNames from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
@ -52,7 +52,7 @@ const AnnouncementsPanel = () => {
|
||||||
key={i}
|
key={i}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => setIndex(i)}
|
onClick={() => setIndex(i)}
|
||||||
className={classNames({
|
className={clsx({
|
||||||
'w-2 h-2 rounded-full focus:ring-primary-600 focus:ring-2 focus:ring-offset-2': true,
|
'w-2 h-2 rounded-full focus:ring-primary-600 focus:ring-2 focus:ring-offset-2': true,
|
||||||
'bg-gray-200 hover:bg-gray-300': i !== index,
|
'bg-gray-200 hover:bg-gray-300': i !== index,
|
||||||
'bg-primary-600': i === index,
|
'bg-primary-600': i === index,
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import unicodeMapping from 'soapbox/features/emoji/emoji-unicode-mapping-light';
|
import unicodeMapping from 'soapbox/features/emoji/mapping';
|
||||||
import { useSettings } from 'soapbox/hooks';
|
import { useSettings } from 'soapbox/hooks';
|
||||||
import { joinPublicPath } from 'soapbox/utils/static';
|
import { joinPublicPath } from 'soapbox/utils/static';
|
||||||
|
|
||||||
import type { Map as ImmutableMap } from 'immutable';
|
import type { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
interface IEmoji {
|
interface IEmoji {
|
||||||
emoji: string;
|
emoji: string
|
||||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
|
||||||
hovered: boolean;
|
hovered: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Emoji: React.FC<IEmoji> = ({ emoji, emojiMap, hovered }) => {
|
const Emoji: React.FC<IEmoji> = ({ emoji, emojiMap, hovered }) => {
|
||||||
|
@ -24,7 +24,7 @@ const Emoji: React.FC<IEmoji> = ({ emoji, emojiMap, hovered }) => {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
draggable='false'
|
draggable='false'
|
||||||
className='emojione block m-0'
|
className='emojione m-0 block'
|
||||||
alt={emoji}
|
alt={emoji}
|
||||||
title={title}
|
title={title}
|
||||||
src={joinPublicPath(`packs/emoji/${filename}.svg`)}
|
src={joinPublicPath(`packs/emoji/${filename}.svg`)}
|
||||||
|
@ -37,7 +37,7 @@ const Emoji: React.FC<IEmoji> = ({ emoji, emojiMap, hovered }) => {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
draggable='false'
|
draggable='false'
|
||||||
className='emojione block m-0'
|
className='emojione m-0 block'
|
||||||
alt={shortCode}
|
alt={shortCode}
|
||||||
title={shortCode}
|
title={shortCode}
|
||||||
src={filename as string}
|
src={filename as string}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import classNames from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import AnimatedNumber from 'soapbox/components/animated-number';
|
import AnimatedNumber from 'soapbox/components/animated-number';
|
||||||
import unicodeMapping from 'soapbox/features/emoji/emoji-unicode-mapping-light';
|
import unicodeMapping from 'soapbox/features/emoji/mapping';
|
||||||
|
|
||||||
import Emoji from './emoji';
|
import Emoji from './emoji';
|
||||||
|
|
||||||
|
@ -10,12 +10,12 @@ import type { Map as ImmutableMap } from 'immutable';
|
||||||
import type { AnnouncementReaction } from 'soapbox/types/entities';
|
import type { AnnouncementReaction } from 'soapbox/types/entities';
|
||||||
|
|
||||||
interface IReaction {
|
interface IReaction {
|
||||||
announcementId: string;
|
announcementId: string
|
||||||
reaction: AnnouncementReaction;
|
reaction: AnnouncementReaction
|
||||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
|
||||||
addReaction: (id: string, name: string) => void;
|
addReaction: (id: string, name: string) => void
|
||||||
removeReaction: (id: string, name: string) => void;
|
removeReaction: (id: string, name: string) => void
|
||||||
style: React.CSSProperties;
|
style: React.CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
const Reaction: React.FC<IReaction> = ({ announcementId, reaction, addReaction, removeReaction, emojiMap, style }) => {
|
const Reaction: React.FC<IReaction> = ({ announcementId, reaction, addReaction, removeReaction, emojiMap, style }) => {
|
||||||
|
@ -43,7 +43,7 @@ const Reaction: React.FC<IReaction> = ({ announcementId, reaction, addReaction,
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={classNames('flex shrink-0 items-center gap-1.5 bg-gray-100 dark:bg-primary-900 rounded-sm px-1.5 py-1 transition-colors', {
|
className={clsx('flex shrink-0 items-center gap-1.5 rounded-sm bg-gray-100 px-1.5 py-1 transition-colors dark:bg-primary-900', {
|
||||||
'bg-gray-200 dark:bg-primary-800': hovered,
|
'bg-gray-200 dark:bg-primary-800': hovered,
|
||||||
'bg-primary-200 dark:bg-primary-500': reaction.me,
|
'bg-primary-200 dark:bg-primary-500': reaction.me,
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -1,30 +1,29 @@
|
||||||
import classNames from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TransitionMotion, spring } from 'react-motion';
|
import { TransitionMotion, spring } from 'react-motion';
|
||||||
|
|
||||||
import { Icon } from 'soapbox/components/ui';
|
import EmojiPickerDropdown from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container';
|
||||||
import EmojiPickerDropdown from 'soapbox/features/compose/components/emoji-picker/emoji-picker-dropdown';
|
|
||||||
import { useSettings } from 'soapbox/hooks';
|
import { useSettings } from 'soapbox/hooks';
|
||||||
|
|
||||||
import Reaction from './reaction';
|
import Reaction from './reaction';
|
||||||
|
|
||||||
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||||
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
|
import type { Emoji, NativeEmoji } from 'soapbox/features/emoji';
|
||||||
import type { AnnouncementReaction } from 'soapbox/types/entities';
|
import type { AnnouncementReaction } from 'soapbox/types/entities';
|
||||||
|
|
||||||
interface IReactionsBar {
|
interface IReactionsBar {
|
||||||
announcementId: string;
|
announcementId: string
|
||||||
reactions: ImmutableList<AnnouncementReaction>;
|
reactions: ImmutableList<AnnouncementReaction>
|
||||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
|
||||||
addReaction: (id: string, name: string) => void;
|
addReaction: (id: string, name: string) => void
|
||||||
removeReaction: (id: string, name: string) => void;
|
removeReaction: (id: string, name: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addReaction, removeReaction, emojiMap }) => {
|
const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addReaction, removeReaction, emojiMap }) => {
|
||||||
const reduceMotion = useSettings().get('reduceMotion');
|
const reduceMotion = useSettings().get('reduceMotion');
|
||||||
|
|
||||||
const handleEmojiPick = (data: Emoji) => {
|
const handleEmojiPick = (data: Emoji) => {
|
||||||
addReaction(announcementId, data.native.replace(/:/g, ''));
|
addReaction(announcementId, (data as NativeEmoji).native.replace(/:/g, ''));
|
||||||
};
|
};
|
||||||
|
|
||||||
const willEnter = () => ({ scale: reduceMotion ? 1 : 0 });
|
const willEnter = () => ({ scale: reduceMotion ? 1 : 0 });
|
||||||
|
@ -42,7 +41,7 @@ const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addR
|
||||||
return (
|
return (
|
||||||
<TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
|
<TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
|
||||||
{items => (
|
{items => (
|
||||||
<div className={classNames('flex flex-wrap items-center gap-1', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
|
<div className={clsx('flex flex-wrap items-center gap-1', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
|
||||||
{items.map(({ key, data, style }) => (
|
{items.map(({ key, data, style }) => (
|
||||||
<Reaction
|
<Reaction
|
||||||
key={key}
|
key={key}
|
||||||
|
@ -55,7 +54,7 @@ const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addR
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{visibleReactions.size < 8 && <EmojiPickerDropdown onPickEmoji={handleEmojiPick} button={<Icon className='h-4 w-4 text-gray-400 hover:text-gray-600 dark:hover:text-white' src={require('@tabler/icons/plus.svg')} />} />}
|
{visibleReactions.size < 8 && <EmojiPickerDropdown onPickEmoji={handleEmojiPick} />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TransitionMotion>
|
</TransitionMotion>
|
||||||
|
|
|
@ -0,0 +1,180 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { HStack, IconButton, Text } from 'soapbox/components/ui';
|
||||||
|
|
||||||
|
interface IAuthorizeRejectButtons {
|
||||||
|
onAuthorize(): Promise<unknown> | unknown
|
||||||
|
onReject(): Promise<unknown> | unknown
|
||||||
|
countdown?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Buttons to approve or reject a pending item, usually an account. */
|
||||||
|
const AuthorizeRejectButtons: React.FC<IAuthorizeRejectButtons> = ({ onAuthorize, onReject, countdown }) => {
|
||||||
|
const [state, setState] = useState<'authorizing' | 'rejecting' | 'authorized' | 'rejected' | 'pending'>('pending');
|
||||||
|
const timeout = useRef<NodeJS.Timeout>();
|
||||||
|
const interval = useRef<ReturnType<typeof setInterval>>();
|
||||||
|
|
||||||
|
const [progress, setProgress] = useState<number>(0);
|
||||||
|
|
||||||
|
const startProgressInterval = () => {
|
||||||
|
let startValue = 1;
|
||||||
|
interval.current = setInterval(() => {
|
||||||
|
startValue++;
|
||||||
|
const newValue = startValue * 3.6; // get to 360 (deg)
|
||||||
|
setProgress(newValue);
|
||||||
|
|
||||||
|
if (newValue >= 360) {
|
||||||
|
clearInterval(interval.current as NodeJS.Timeout);
|
||||||
|
setProgress(0);
|
||||||
|
}
|
||||||
|
}, (countdown as number) / 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleAction(
|
||||||
|
present: 'authorizing' | 'rejecting',
|
||||||
|
past: 'authorized' | 'rejected',
|
||||||
|
action: () => Promise<unknown> | unknown,
|
||||||
|
): void {
|
||||||
|
if (state === present) {
|
||||||
|
if (interval.current) {
|
||||||
|
clearInterval(interval.current);
|
||||||
|
}
|
||||||
|
if (timeout.current) {
|
||||||
|
clearTimeout(timeout.current);
|
||||||
|
}
|
||||||
|
setState('pending');
|
||||||
|
} else {
|
||||||
|
const doAction = async () => {
|
||||||
|
try {
|
||||||
|
await action();
|
||||||
|
setState(past);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (typeof countdown === 'number') {
|
||||||
|
setState(present);
|
||||||
|
timeout.current = setTimeout(doAction, countdown);
|
||||||
|
startProgressInterval();
|
||||||
|
} else {
|
||||||
|
doAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAuthorize = async () => handleAction('authorizing', 'authorized', onAuthorize);
|
||||||
|
const handleReject = async () => handleAction('rejecting', 'rejected', onReject);
|
||||||
|
|
||||||
|
const renderStyle = (selectedState: typeof state) => {
|
||||||
|
if (state === 'authorizing' && selectedState === 'authorizing') {
|
||||||
|
return {
|
||||||
|
background: `conic-gradient(rgb(var(--color-primary-500)) ${progress}deg, rgb(var(--color-primary-500) / 0.1) 0deg)`,
|
||||||
|
};
|
||||||
|
} else if (state === 'rejecting' && selectedState === 'rejecting') {
|
||||||
|
return {
|
||||||
|
background: `conic-gradient(rgb(var(--color-danger-600)) ${progress}deg, rgb(var(--color-danger-600) / 0.1) 0deg)`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (timeout.current) {
|
||||||
|
clearTimeout(timeout.current);
|
||||||
|
}
|
||||||
|
if (interval.current) {
|
||||||
|
clearInterval(interval.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case 'authorized':
|
||||||
|
return (
|
||||||
|
<ActionEmblem text={<FormattedMessage id='authorize.success' defaultMessage='Approved' />} />
|
||||||
|
);
|
||||||
|
case 'rejected':
|
||||||
|
return (
|
||||||
|
<ActionEmblem text={<FormattedMessage id='reject.success' defaultMessage='Rejected' />} />
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<HStack space={3} alignItems='center'>
|
||||||
|
<AuthorizeRejectButton
|
||||||
|
theme='danger'
|
||||||
|
icon={require('@tabler/icons/x.svg')}
|
||||||
|
action={handleReject}
|
||||||
|
isLoading={state === 'rejecting'}
|
||||||
|
disabled={state === 'authorizing'}
|
||||||
|
style={renderStyle('rejecting')}
|
||||||
|
/>
|
||||||
|
<AuthorizeRejectButton
|
||||||
|
theme='primary'
|
||||||
|
icon={require('@tabler/icons/check.svg')}
|
||||||
|
action={handleAuthorize}
|
||||||
|
isLoading={state === 'authorizing'}
|
||||||
|
disabled={state === 'rejecting'}
|
||||||
|
style={renderStyle('authorizing')}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IActionEmblem {
|
||||||
|
text: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActionEmblem: React.FC<IActionEmblem> = ({ text }) => {
|
||||||
|
return (
|
||||||
|
<div className='rounded-full bg-gray-100 px-4 py-2 dark:bg-gray-800'>
|
||||||
|
<Text theme='muted' size='sm'>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IAuthorizeRejectButton {
|
||||||
|
theme: 'primary' | 'danger'
|
||||||
|
icon: string
|
||||||
|
action(): void
|
||||||
|
isLoading?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
style: React.CSSProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthorizeRejectButton: React.FC<IAuthorizeRejectButton> = ({ theme, icon, action, isLoading, style, disabled }) => {
|
||||||
|
return (
|
||||||
|
<div className='relative'>
|
||||||
|
<div
|
||||||
|
style={style}
|
||||||
|
className={
|
||||||
|
clsx({
|
||||||
|
'flex h-11 w-11 items-center justify-center rounded-full': true,
|
||||||
|
'bg-danger-600/10': theme === 'danger',
|
||||||
|
'bg-primary-500/10': theme === 'primary',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
src={isLoading ? require('@tabler/icons/player-stop-filled.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', {
|
||||||
|
'text-primary-500': theme === 'primary',
|
||||||
|
'text-danger-600': theme === 'danger',
|
||||||
|
})}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { AuthorizeRejectButtons };
|
|
@ -12,16 +12,16 @@ import type { InputThemes } from 'soapbox/components/ui/input/input';
|
||||||
const noOp = () => { };
|
const noOp = () => { };
|
||||||
|
|
||||||
interface IAutosuggestAccountInput {
|
interface IAutosuggestAccountInput {
|
||||||
onChange: React.ChangeEventHandler<HTMLInputElement>,
|
onChange: React.ChangeEventHandler<HTMLInputElement>
|
||||||
onSelected: (accountId: string) => void,
|
onSelected: (accountId: string) => void
|
||||||
autoFocus?: boolean,
|
autoFocus?: boolean
|
||||||
value: string,
|
value: string
|
||||||
limit?: number,
|
limit?: number
|
||||||
className?: string,
|
className?: string
|
||||||
autoSelect?: boolean,
|
autoSelect?: boolean
|
||||||
menu?: Menu,
|
menu?: Menu
|
||||||
onKeyDown?: React.KeyboardEventHandler,
|
onKeyDown?: React.KeyboardEventHandler
|
||||||
theme?: InputThemes,
|
theme?: InputThemes
|
||||||
}
|
}
|
||||||
|
|
||||||
const AutosuggestAccountInput: React.FC<IAutosuggestAccountInput> = ({
|
const AutosuggestAccountInput: React.FC<IAutosuggestAccountInput> = ({
|
||||||
|
|
|
@ -1,38 +1,30 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import unicodeMapping from 'soapbox/features/emoji/emoji-unicode-mapping-light';
|
import { isCustomEmoji } from 'soapbox/features/emoji';
|
||||||
|
import unicodeMapping from 'soapbox/features/emoji/mapping';
|
||||||
import { joinPublicPath } from 'soapbox/utils/static';
|
import { joinPublicPath } from 'soapbox/utils/static';
|
||||||
|
|
||||||
export type Emoji = {
|
import type { Emoji } from 'soapbox/features/emoji';
|
||||||
id: string,
|
|
||||||
custom: boolean,
|
|
||||||
imageUrl: string,
|
|
||||||
native: string,
|
|
||||||
colons: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
type UnicodeMapping = {
|
|
||||||
filename: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IAutosuggestEmoji {
|
interface IAutosuggestEmoji {
|
||||||
emoji: Emoji,
|
emoji: Emoji
|
||||||
}
|
}
|
||||||
|
|
||||||
const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => {
|
const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => {
|
||||||
let url;
|
let url, alt;
|
||||||
|
|
||||||
if (emoji.custom) {
|
if (isCustomEmoji(emoji)) {
|
||||||
url = emoji.imageUrl;
|
url = emoji.imageUrl;
|
||||||
|
alt = emoji.colons;
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
|
||||||
const mapping: UnicodeMapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
|
|
||||||
|
|
||||||
if (!mapping) {
|
if (!mapping) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
url = joinPublicPath(`packs/emoji/${mapping.filename}.svg`);
|
url = joinPublicPath(`packs/emoji/${mapping.unified}.svg`);
|
||||||
|
alt = emoji.native;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -40,7 +32,7 @@ const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => {
|
||||||
<img
|
<img
|
||||||
className='emojione'
|
className='emojione'
|
||||||
src={url}
|
src={url}
|
||||||
alt={emoji.native || emoji.colons}
|
alt={alt}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{emoji.colons}
|
{emoji.colons}
|
||||||
|
|
|
@ -1,39 +1,39 @@
|
||||||
import { Portal } from '@reach/portal';
|
import clsx from 'clsx';
|
||||||
import classNames from 'clsx';
|
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import AutosuggestEmoji, { Emoji } from 'soapbox/components/autosuggest-emoji';
|
import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji';
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
import { Input } from 'soapbox/components/ui';
|
import { Input, Portal } from 'soapbox/components/ui';
|
||||||
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
|
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
|
||||||
import { isRtl } from 'soapbox/rtl';
|
import { isRtl } from 'soapbox/rtl';
|
||||||
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
||||||
|
|
||||||
import type { Menu, MenuItem } from 'soapbox/components/dropdown-menu';
|
import type { Menu, MenuItem } from 'soapbox/components/dropdown-menu';
|
||||||
import type { InputThemes } from 'soapbox/components/ui/input/input';
|
import type { InputThemes } from 'soapbox/components/ui/input/input';
|
||||||
|
import type { Emoji } from 'soapbox/features/emoji';
|
||||||
|
|
||||||
export type AutoSuggestion = string | Emoji;
|
export type AutoSuggestion = string | Emoji;
|
||||||
|
|
||||||
export interface IAutosuggestInput extends Pick<React.HTMLAttributes<HTMLInputElement>, 'onChange' | 'onKeyUp' | 'onKeyDown'> {
|
export interface IAutosuggestInput extends Pick<React.HTMLAttributes<HTMLInputElement>, 'onChange' | 'onKeyUp' | 'onKeyDown'> {
|
||||||
value: string,
|
value: string
|
||||||
suggestions: ImmutableList<any>,
|
suggestions: ImmutableList<any>
|
||||||
disabled?: boolean,
|
disabled?: boolean
|
||||||
placeholder?: string,
|
placeholder?: string
|
||||||
onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void,
|
onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void
|
||||||
onSuggestionsClearRequested: () => void,
|
onSuggestionsClearRequested: () => void
|
||||||
onSuggestionsFetchRequested: (token: string) => void,
|
onSuggestionsFetchRequested: (token: string) => void
|
||||||
autoFocus: boolean,
|
autoFocus: boolean
|
||||||
autoSelect: boolean,
|
autoSelect: boolean
|
||||||
className?: string,
|
className?: string
|
||||||
id?: string,
|
id?: string
|
||||||
searchTokens: string[],
|
searchTokens: string[]
|
||||||
maxLength?: number,
|
maxLength?: number
|
||||||
menu?: Menu,
|
menu?: Menu
|
||||||
renderSuggestion?: React.FC<{ id: string }>,
|
renderSuggestion?: React.FC<{ id: string }>
|
||||||
hidePortal?: boolean,
|
hidePortal?: boolean
|
||||||
theme?: InputThemes,
|
theme?: InputThemes
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AutosuggestInput extends ImmutablePureComponent<IAutosuggestInput> {
|
export default class AutosuggestInput extends ImmutablePureComponent<IAutosuggestInput> {
|
||||||
|
@ -199,7 +199,7 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
key={key}
|
key={key}
|
||||||
data-index={i}
|
data-index={i}
|
||||||
className={classNames({
|
className={clsx({
|
||||||
'px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800 group': true,
|
'px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800 group': true,
|
||||||
'bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800': i === selectedSuggestion,
|
'bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800': i === selectedSuggestion,
|
||||||
})}
|
})}
|
||||||
|
@ -235,7 +235,7 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
||||||
|
|
||||||
return menu.map((item, i) => (
|
return menu.map((item, i) => (
|
||||||
<a
|
<a
|
||||||
className={classNames('flex items-center space-x-2 px-4 py-2.5 text-sm cursor-pointer text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800', { selected: suggestions.size - selectedSuggestion === i })}
|
className={clsx('flex cursor-pointer items-center space-x-2 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-100 focus:bg-gray-100 dark:text-gray-500 dark:hover:bg-gray-800 dark:focus:bg-primary-800', { selected: suggestions.size - selectedSuggestion === i })}
|
||||||
href='#'
|
href='#'
|
||||||
role='button'
|
role='button'
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
@ -302,7 +302,7 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
||||||
<Portal key='portal'>
|
<Portal key='portal'>
|
||||||
<div
|
<div
|
||||||
style={this.setPortalPosition()}
|
style={this.setPortalPosition()}
|
||||||
className={classNames({
|
className={clsx({
|
||||||
'fixed w-full z-[1001] shadow bg-white dark:bg-gray-900 rounded-lg py-1 dark:ring-2 dark:ring-primary-700 focus:outline-none': true,
|
'fixed w-full z-[1001] shadow bg-white dark:bg-gray-900 rounded-lg py-1 dark:ring-2 dark:ring-primary-700 focus:outline-none': true,
|
||||||
hidden: !visible,
|
hidden: !visible,
|
||||||
block: visible,
|
block: visible,
|
||||||
|
|
|
@ -19,7 +19,7 @@ export const ADDRESS_ICONS: Record<string, string> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IAutosuggestLocation {
|
interface IAutosuggestLocation {
|
||||||
id: string,
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const AutosuggestLocation: React.FC<IAutosuggestLocation> = ({ id }) => {
|
const AutosuggestLocation: React.FC<IAutosuggestLocation> = ({ id }) => {
|
||||||
|
|
|
@ -1,36 +1,36 @@
|
||||||
import { Portal } from '@reach/portal';
|
import clsx from 'clsx';
|
||||||
import classNames from 'clsx';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import Textarea from 'react-textarea-autosize';
|
import Textarea from 'react-textarea-autosize';
|
||||||
|
|
||||||
|
import { Portal } from 'soapbox/components/ui';
|
||||||
|
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
|
||||||
|
import { isRtl } from 'soapbox/rtl';
|
||||||
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
||||||
|
|
||||||
import AutosuggestAccount from '../features/compose/components/autosuggest-account';
|
import AutosuggestEmoji from './autosuggest-emoji';
|
||||||
import { isRtl } from '../rtl';
|
|
||||||
|
|
||||||
import AutosuggestEmoji, { Emoji } from './autosuggest-emoji';
|
|
||||||
|
|
||||||
import type { List as ImmutableList } from 'immutable';
|
import type { List as ImmutableList } from 'immutable';
|
||||||
|
import type { Emoji } from 'soapbox/features/emoji';
|
||||||
|
|
||||||
interface IAutosuggesteTextarea {
|
interface IAutosuggesteTextarea {
|
||||||
id?: string,
|
id?: string
|
||||||
value: string,
|
value: string
|
||||||
suggestions: ImmutableList<string>,
|
suggestions: ImmutableList<string>
|
||||||
disabled: boolean,
|
disabled: boolean
|
||||||
placeholder: string,
|
placeholder: string
|
||||||
onSuggestionSelected: (tokenStart: number, token: string | null, value: string | undefined) => void,
|
onSuggestionSelected: (tokenStart: number, token: string | null, value: string | undefined) => void
|
||||||
onSuggestionsClearRequested: () => void,
|
onSuggestionsClearRequested: () => void
|
||||||
onSuggestionsFetchRequested: (token: string | number) => void,
|
onSuggestionsFetchRequested: (token: string | number) => void
|
||||||
onChange: React.ChangeEventHandler<HTMLTextAreaElement>,
|
onChange: React.ChangeEventHandler<HTMLTextAreaElement>
|
||||||
onKeyUp?: React.KeyboardEventHandler<HTMLTextAreaElement>,
|
onKeyUp?: React.KeyboardEventHandler<HTMLTextAreaElement>
|
||||||
onKeyDown?: React.KeyboardEventHandler<HTMLTextAreaElement>,
|
onKeyDown?: React.KeyboardEventHandler<HTMLTextAreaElement>
|
||||||
onPaste: (files: FileList) => void,
|
onPaste: (files: FileList) => void
|
||||||
autoFocus: boolean,
|
autoFocus: boolean
|
||||||
onFocus: () => void,
|
onFocus: () => void
|
||||||
onBlur?: () => void,
|
onBlur?: () => void
|
||||||
condensed?: boolean,
|
condensed?: boolean
|
||||||
children: React.ReactNode,
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea> {
|
class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea> {
|
||||||
|
@ -157,7 +157,8 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
|
||||||
if (lastTokenUpdated && !valueUpdated) {
|
if (lastTokenUpdated && !valueUpdated) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
return super.shouldComponentUpdate!(nextProps, nextState, undefined);
|
// https://stackoverflow.com/a/35962835
|
||||||
|
return super.shouldComponentUpdate!.bind(this)(nextProps, nextState, undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,7 +201,7 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
key={key}
|
key={key}
|
||||||
data-index={i}
|
data-index={i}
|
||||||
className={classNames({
|
className={clsx({
|
||||||
'px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800 group': true,
|
'px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800 group': true,
|
||||||
'bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800': i === selectedSuggestion,
|
'bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800': i === selectedSuggestion,
|
||||||
})}
|
})}
|
||||||
|
@ -243,7 +244,7 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
ref={this.setTextarea}
|
ref={this.setTextarea}
|
||||||
className={classNames('transition-[min-height] motion-reduce:transition-none dark:bg-transparent px-0 border-0 text-gray-800 dark:text-white placeholder:text-gray-600 dark:placeholder:text-gray-600 resize-none w-full focus:shadow-none focus:border-0 focus:ring-0', {
|
className={clsx('w-full resize-none border-0 px-0 text-gray-800 transition-[min-height] placeholder:text-gray-600 focus:border-0 focus:shadow-none focus:ring-0 motion-reduce:transition-none dark:bg-transparent dark:text-white dark:placeholder:text-gray-600', {
|
||||||
'min-h-[40px]': condensed,
|
'min-h-[40px]': condensed,
|
||||||
'min-h-[100px]': !condensed,
|
'min-h-[100px]': !condensed,
|
||||||
})}
|
})}
|
||||||
|
@ -270,7 +271,7 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
|
||||||
<Portal key='portal'>
|
<Portal key='portal'>
|
||||||
<div
|
<div
|
||||||
style={this.setPortalPosition()}
|
style={this.setPortalPosition()}
|
||||||
className={classNames({
|
className={clsx({
|
||||||
'fixed z-1000 shadow bg-white dark:bg-gray-900 rounded-lg py-1 space-y-0 dark:ring-2 dark:ring-primary-700 focus:outline-none': true,
|
'fixed z-1000 shadow bg-white dark:bg-gray-900 rounded-lg py-1 space-y-0 dark:ring-2 dark:ring-primary-700 focus:outline-none': true,
|
||||||
hidden: suggestionsHidden || suggestions.isEmpty(),
|
hidden: suggestionsHidden || suggestions.isEmpty(),
|
||||||
block: !suggestionsHidden && !suggestions.isEmpty(),
|
block: !suggestionsHidden && !suggestions.isEmpty(),
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import classNames from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface IBadge {
|
interface IBadge {
|
||||||
title: React.ReactNode,
|
title: React.ReactNode
|
||||||
slug: string,
|
slug: string
|
||||||
}
|
}
|
||||||
/** Badge to display on a user's profile. */
|
/** Badge to display on a user's profile. */
|
||||||
const Badge: React.FC<IBadge> = ({ title, slug }) => {
|
const Badge: React.FC<IBadge> = ({ title, slug }) => {
|
||||||
|
@ -12,13 +12,13 @@ const Badge: React.FC<IBadge> = ({ title, slug }) => {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-testid='badge'
|
data-testid='badge'
|
||||||
className={classNames('inline-flex items-center px-2 py-0.5 rounded text-xs font-medium', {
|
className={clsx('inline-flex items-center rounded px-2 py-0.5 text-xs font-medium', {
|
||||||
'bg-fuchsia-700 text-white': slug === 'patron',
|
'bg-fuchsia-700 text-white': slug === 'patron',
|
||||||
'bg-emerald-800 text-white': slug === 'badge:donor',
|
'bg-emerald-800 text-white': slug === 'badge:donor',
|
||||||
'bg-black text-white': slug === 'admin',
|
'bg-black text-white': slug === 'admin',
|
||||||
'bg-cyan-600 text-white': slug === 'moderator',
|
'bg-cyan-600 text-white': slug === 'moderator',
|
||||||
'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100': fallback,
|
'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100': fallback,
|
||||||
'bg-white bg-opacity-75 text-gray-900': slug === 'opaque',
|
'bg-white/75 text-gray-900': slug === 'opaque',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
|
|
|
@ -15,9 +15,9 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IBirthdayInput {
|
interface IBirthdayInput {
|
||||||
value?: string,
|
value?: string
|
||||||
onChange: (value: string) => void,
|
onChange: (value: string) => void
|
||||||
required?: boolean,
|
required?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required }) => {
|
const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required }) => {
|
||||||
|
@ -56,15 +56,15 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
|
||||||
nextYearButtonDisabled,
|
nextYearButtonDisabled,
|
||||||
date,
|
date,
|
||||||
}: {
|
}: {
|
||||||
decreaseMonth(): void,
|
decreaseMonth(): void
|
||||||
increaseMonth(): void,
|
increaseMonth(): void
|
||||||
prevMonthButtonDisabled: boolean,
|
prevMonthButtonDisabled: boolean
|
||||||
nextMonthButtonDisabled: boolean,
|
nextMonthButtonDisabled: boolean
|
||||||
decreaseYear(): void,
|
decreaseYear(): void
|
||||||
increaseYear(): void,
|
increaseYear(): void
|
||||||
prevYearButtonDisabled: boolean,
|
prevYearButtonDisabled: boolean
|
||||||
nextYearButtonDisabled: boolean,
|
nextYearButtonDisabled: boolean
|
||||||
date: Date,
|
date: Date
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-2'>
|
<div className='flex flex-col gap-2'>
|
||||||
|
@ -113,7 +113,7 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
|
||||||
const handleChange = (date: Date) => onChange(date ? new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) : '');
|
const handleChange = (date: Date) => onChange(date ? new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) : '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-1 relative rounded-md shadow-sm'>
|
<div className='relative mt-1 rounded-md shadow-sm'>
|
||||||
<BundleContainer fetchComponent={DatePicker}>
|
<BundleContainer fetchComponent={DatePicker}>
|
||||||
{Component => (<Component
|
{Component => (<Component
|
||||||
selected={selected}
|
selected={selected}
|
||||||
|
|
|
@ -3,18 +3,18 @@ import React, { useRef, useEffect } from 'react';
|
||||||
|
|
||||||
interface IBlurhash {
|
interface IBlurhash {
|
||||||
/** Hash to render */
|
/** Hash to render */
|
||||||
hash: string | null | undefined,
|
hash: string | null | undefined
|
||||||
/** Width of the blurred region in pixels. Defaults to 32. */
|
/** Width of the blurred region in pixels. Defaults to 32. */
|
||||||
width?: number,
|
width?: number
|
||||||
/** Height of the blurred region in pixels. Defaults to width. */
|
/** Height of the blurred region in pixels. Defaults to width. */
|
||||||
height?: number,
|
height?: number
|
||||||
/**
|
/**
|
||||||
* Whether dummy mode is enabled. If enabled, nothing is rendered
|
* Whether dummy mode is enabled. If enabled, nothing is rendered
|
||||||
* and canvas left untouched.
|
* and canvas left untouched.
|
||||||
*/
|
*/
|
||||||
dummy?: boolean,
|
dummy?: boolean
|
||||||
/** className of the canvas element. */
|
/** className of the canvas element. */
|
||||||
className?: string,
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Button, HStack, Input } from './ui';
|
||||||
|
|
||||||
interface ICopyableInput {
|
interface ICopyableInput {
|
||||||
/** Text to be copied. */
|
/** Text to be copied. */
|
||||||
value: string,
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An input with copy abilities. */
|
/** An input with copy abilities. */
|
||||||
|
@ -29,7 +29,7 @@ const CopyableInput: React.FC<ICopyableInput> = ({ value }) => {
|
||||||
type='text'
|
type='text'
|
||||||
value={value}
|
value={value}
|
||||||
className='rounded-r-none rtl:rounded-l-none rtl:rounded-r-lg'
|
className='rounded-r-none rtl:rounded-l-none rtl:rounded-r-lg'
|
||||||
outerClassName='flex-grow'
|
outerClassName='grow'
|
||||||
onClick={selectInput}
|
onClick={selectInput}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -5,8 +5,6 @@ import { useSoapboxConfig } from 'soapbox/hooks';
|
||||||
|
|
||||||
import { getAcct } from '../utils/accounts';
|
import { getAcct } from '../utils/accounts';
|
||||||
|
|
||||||
import Icon from './icon';
|
|
||||||
import RelativeTimestamp from './relative-timestamp';
|
|
||||||
import { HStack, Text } from './ui';
|
import { HStack, Text } from './ui';
|
||||||
import VerificationBadge from './verification-badge';
|
import VerificationBadge from './verification-badge';
|
||||||
|
|
||||||
|
@ -15,20 +13,12 @@ import type { Account } from 'soapbox/types/entities';
|
||||||
interface IDisplayName {
|
interface IDisplayName {
|
||||||
account: Account
|
account: Account
|
||||||
withSuffix?: boolean
|
withSuffix?: boolean
|
||||||
withDate?: boolean
|
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const DisplayName: React.FC<IDisplayName> = ({ account, children, withSuffix = true, withDate = false }) => {
|
const DisplayName: React.FC<IDisplayName> = ({ account, children, withSuffix = true }) => {
|
||||||
const { displayFqn = false } = useSoapboxConfig();
|
const { displayFqn = false } = useSoapboxConfig();
|
||||||
const { created_at: createdAt, verified } = account;
|
const { verified } = account;
|
||||||
|
|
||||||
const joinedAt = createdAt ? (
|
|
||||||
<div className='account__joined-at'>
|
|
||||||
<Icon src={require('@tabler/icons/clock.svg')} />
|
|
||||||
<RelativeTimestamp timestamp={createdAt} />
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const displayName = (
|
const displayName = (
|
||||||
<HStack space={1} alignItems='center' grow>
|
<HStack space={1} alignItems='center' grow>
|
||||||
|
@ -40,7 +30,6 @@ const DisplayName: React.FC<IDisplayName> = ({ account, children, withSuffix = t
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{verified && <VerificationBadge />}
|
{verified && <VerificationBadge />}
|
||||||
{withDate && joinedAt}
|
|
||||||
</HStack>
|
</HStack>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IDomain {
|
interface IDomain {
|
||||||
domain: string,
|
domain: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const Domain: React.FC<IDomain> = ({ domain }) => {
|
const Domain: React.FC<IDomain> = ({ domain }) => {
|
||||||
|
|
|
@ -1,420 +0,0 @@
|
||||||
import classNames from 'clsx';
|
|
||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
|
||||||
import React from 'react';
|
|
||||||
import { spring } from 'react-motion';
|
|
||||||
// @ts-ignore: TODO: upgrade react-overlays. v3.1 and above have TS definitions
|
|
||||||
import Overlay from 'react-overlays/lib/Overlay';
|
|
||||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { Counter, IconButton } from 'soapbox/components/ui';
|
|
||||||
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
|
||||||
import Motion from 'soapbox/features/ui/util/optional-motion';
|
|
||||||
|
|
||||||
import type { Status } from 'soapbox/types/entities';
|
|
||||||
|
|
||||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
|
||||||
let id = 0;
|
|
||||||
|
|
||||||
export interface MenuItem {
|
|
||||||
action?: React.EventHandler<React.KeyboardEvent | React.MouseEvent>,
|
|
||||||
middleClick?: React.EventHandler<React.MouseEvent>,
|
|
||||||
text: string,
|
|
||||||
href?: string,
|
|
||||||
to?: string,
|
|
||||||
newTab?: boolean,
|
|
||||||
isLogout?: boolean,
|
|
||||||
icon?: string,
|
|
||||||
count?: number,
|
|
||||||
destructive?: boolean,
|
|
||||||
meta?: string,
|
|
||||||
active?: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Menu = Array<MenuItem | null>;
|
|
||||||
|
|
||||||
interface IDropdownMenu extends RouteComponentProps {
|
|
||||||
items: Menu,
|
|
||||||
onClose: () => void,
|
|
||||||
style?: React.CSSProperties,
|
|
||||||
placement?: DropdownPlacement,
|
|
||||||
arrowOffsetLeft?: string,
|
|
||||||
arrowOffsetTop?: string,
|
|
||||||
openedViaKeyboard: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IDropdownMenuState {
|
|
||||||
mounted: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
class DropdownMenu extends React.PureComponent<IDropdownMenu, IDropdownMenuState> {
|
|
||||||
|
|
||||||
static defaultProps: Partial<IDropdownMenu> = {
|
|
||||||
style: {},
|
|
||||||
placement: 'bottom',
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
mounted: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
node: HTMLDivElement | null = null;
|
|
||||||
focusedItem: HTMLAnchorElement | null = null;
|
|
||||||
|
|
||||||
handleDocumentClick = (e: Event) => {
|
|
||||||
if (this.node && !this.node.contains(e.target as Node)) {
|
|
||||||
this.props.onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
document.addEventListener('click', this.handleDocumentClick, false);
|
|
||||||
document.addEventListener('keydown', this.handleKeyDown, false);
|
|
||||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
|
||||||
if (this.focusedItem && this.props.openedViaKeyboard) {
|
|
||||||
this.focusedItem.focus({ preventScroll: true });
|
|
||||||
}
|
|
||||||
this.setState({ mounted: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
document.removeEventListener('click', this.handleDocumentClick);
|
|
||||||
document.removeEventListener('keydown', this.handleKeyDown);
|
|
||||||
document.removeEventListener('touchend', this.handleDocumentClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
setRef: React.RefCallback<HTMLDivElement> = c => {
|
|
||||||
this.node = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
setFocusRef: React.RefCallback<HTMLAnchorElement> = c => {
|
|
||||||
this.focusedItem = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (!this.node) return;
|
|
||||||
|
|
||||||
const items = Array.from(this.node.getElementsByTagName('a'));
|
|
||||||
const index = items.indexOf(document.activeElement as any);
|
|
||||||
let element = null;
|
|
||||||
|
|
||||||
switch (e.key) {
|
|
||||||
case 'ArrowDown':
|
|
||||||
element = items[index + 1] || items[0];
|
|
||||||
break;
|
|
||||||
case 'ArrowUp':
|
|
||||||
element = items[index - 1] || items[items.length - 1];
|
|
||||||
break;
|
|
||||||
case 'Tab':
|
|
||||||
if (e.shiftKey) {
|
|
||||||
element = items[index - 1] || items[items.length - 1];
|
|
||||||
} else {
|
|
||||||
element = items[index + 1] || items[0];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'Home':
|
|
||||||
element = items[0];
|
|
||||||
break;
|
|
||||||
case 'End':
|
|
||||||
element = items[items.length - 1];
|
|
||||||
break;
|
|
||||||
case 'Escape':
|
|
||||||
this.props.onClose();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element) {
|
|
||||||
element.focus();
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleItemKeyPress: React.EventHandler<React.KeyboardEvent> = e => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
this.handleClick(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClick: React.EventHandler<React.MouseEvent | React.KeyboardEvent> = e => {
|
|
||||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
|
||||||
const item = this.props.items[i];
|
|
||||||
if (!item) return;
|
|
||||||
const { action, to } = item;
|
|
||||||
|
|
||||||
this.props.onClose();
|
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (to) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(to);
|
|
||||||
} else if (typeof action === 'function') {
|
|
||||||
e.preventDefault();
|
|
||||||
action(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMiddleClick: React.EventHandler<React.MouseEvent> = e => {
|
|
||||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
|
||||||
const item = this.props.items[i];
|
|
||||||
if (!item) return;
|
|
||||||
const { middleClick } = item;
|
|
||||||
|
|
||||||
this.props.onClose();
|
|
||||||
|
|
||||||
if (e.button === 1 && typeof middleClick === 'function') {
|
|
||||||
e.preventDefault();
|
|
||||||
middleClick(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleAuxClick: React.EventHandler<React.MouseEvent> = e => {
|
|
||||||
if (e.button === 1) {
|
|
||||||
this.handleMiddleClick(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
renderItem(option: MenuItem | null, i: number): JSX.Element {
|
|
||||||
if (option === null) {
|
|
||||||
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { text, href, to, newTab, isLogout, icon, count, destructive } = option;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li className={classNames('dropdown-menu__item truncate', { destructive })} key={`${text}-${i}`}>
|
|
||||||
<a
|
|
||||||
href={href || to || '#'}
|
|
||||||
role='button'
|
|
||||||
tabIndex={0}
|
|
||||||
ref={i === 0 ? this.setFocusRef : null}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
onAuxClick={this.handleAuxClick}
|
|
||||||
onKeyPress={this.handleItemKeyPress}
|
|
||||||
data-index={i}
|
|
||||||
target={newTab ? '_blank' : undefined}
|
|
||||||
data-method={isLogout ? 'delete' : undefined}
|
|
||||||
title={text}
|
|
||||||
>
|
|
||||||
{icon && <SvgIcon src={icon} className='mr-3 rtl:ml-3 rtl:mr-0 h-5 w-5 flex-none' />}
|
|
||||||
|
|
||||||
<span className='truncate'>{text}</span>
|
|
||||||
|
|
||||||
{count ? (
|
|
||||||
<span className='ml-auto h-5 w-5 flex-none'>
|
|
||||||
<Counter count={count} />
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
|
|
||||||
const { mounted } = this.state;
|
|
||||||
return (
|
|
||||||
<Motion defaultStyle={{ opacity: 0, scaleX: 1, scaleY: 1 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
|
||||||
{({ opacity, scaleX, scaleY }) => (
|
|
||||||
// It should not be transformed when mounting because the resulting
|
|
||||||
// size will be used to determine the coordinate of the menu by
|
|
||||||
// react-overlays
|
|
||||||
<div
|
|
||||||
className={`dropdown-menu ${placement}`}
|
|
||||||
style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : undefined }}
|
|
||||||
ref={this.setRef}
|
|
||||||
data-testid='dropdown-menu'
|
|
||||||
>
|
|
||||||
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
|
|
||||||
<ul>
|
|
||||||
{items.map((option, i) => this.renderItem(option, i))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Motion>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const RouterDropdownMenu = withRouter(DropdownMenu);
|
|
||||||
|
|
||||||
export interface IDropdown extends RouteComponentProps {
|
|
||||||
icon?: string,
|
|
||||||
src?: string,
|
|
||||||
items: Menu,
|
|
||||||
size?: number,
|
|
||||||
active?: boolean,
|
|
||||||
pressed?: boolean,
|
|
||||||
title?: string,
|
|
||||||
disabled?: boolean,
|
|
||||||
status?: Status,
|
|
||||||
isUserTouching?: () => boolean,
|
|
||||||
isModalOpen?: boolean,
|
|
||||||
onOpen?: (
|
|
||||||
id: number,
|
|
||||||
onItemClick: React.EventHandler<React.MouseEvent | React.KeyboardEvent>,
|
|
||||||
dropdownPlacement: DropdownPlacement,
|
|
||||||
keyboard: boolean,
|
|
||||||
) => void,
|
|
||||||
onClose?: (id: number) => void,
|
|
||||||
dropdownPlacement?: string,
|
|
||||||
openDropdownId?: number | null,
|
|
||||||
openedViaKeyboard?: boolean,
|
|
||||||
text?: string,
|
|
||||||
onShiftClick?: React.EventHandler<React.MouseEvent | React.KeyboardEvent>,
|
|
||||||
children?: JSX.Element,
|
|
||||||
dropdownMenuStyle?: React.CSSProperties,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IDropdownState {
|
|
||||||
id: number,
|
|
||||||
open: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DropdownPlacement = 'top' | 'bottom';
|
|
||||||
|
|
||||||
class Dropdown extends React.PureComponent<IDropdown, IDropdownState> {
|
|
||||||
|
|
||||||
static defaultProps: Partial<IDropdown> = {
|
|
||||||
title: 'Menu',
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
id: id++,
|
|
||||||
open: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
target: HTMLButtonElement | null = null;
|
|
||||||
activeElement: Element | null = null;
|
|
||||||
|
|
||||||
handleClick: React.EventHandler<React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>> = e => {
|
|
||||||
const { onOpen, onShiftClick, openDropdownId } = this.props;
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (onShiftClick && e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
onShiftClick(e);
|
|
||||||
} else if (this.state.id === openDropdownId) {
|
|
||||||
this.handleClose();
|
|
||||||
} else if (onOpen) {
|
|
||||||
const { top } = e.currentTarget.getBoundingClientRect();
|
|
||||||
const placement: DropdownPlacement = top * 2 < innerHeight ? 'bottom' : 'top';
|
|
||||||
|
|
||||||
onOpen(this.state.id, this.handleItemClick, placement, e.type !== 'click');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClose = () => {
|
|
||||||
if (this.activeElement && this.activeElement === this.target) {
|
|
||||||
(this.activeElement as HTMLButtonElement).focus();
|
|
||||||
this.activeElement = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.onClose) {
|
|
||||||
this.props.onClose(this.state.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseDown: React.EventHandler<React.MouseEvent | React.KeyboardEvent> = () => {
|
|
||||||
if (!this.state.open) {
|
|
||||||
this.activeElement = document.activeElement;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleButtonKeyDown: React.EventHandler<React.KeyboardEvent> = (e) => {
|
|
||||||
switch (e.key) {
|
|
||||||
case ' ':
|
|
||||||
case 'Enter':
|
|
||||||
this.handleMouseDown(e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleKeyPress: React.EventHandler<React.KeyboardEvent<HTMLButtonElement>> = (e) => {
|
|
||||||
switch (e.key) {
|
|
||||||
case ' ':
|
|
||||||
case 'Enter':
|
|
||||||
this.handleClick(e);
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleItemClick: React.EventHandler<React.MouseEvent> = e => {
|
|
||||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
|
||||||
const item = this.props.items[i];
|
|
||||||
if (!item) return;
|
|
||||||
|
|
||||||
const { action, to } = item;
|
|
||||||
|
|
||||||
this.handleClose();
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (typeof action === 'function') {
|
|
||||||
action(e);
|
|
||||||
} else if (to) {
|
|
||||||
this.props.history?.push(to);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setTargetRef: React.RefCallback<HTMLButtonElement> = c => {
|
|
||||||
this.target = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
findTarget = () => {
|
|
||||||
return this.target;
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
|
||||||
if (this.state.id === this.props.openDropdownId) {
|
|
||||||
this.handleClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { src = require('@tabler/icons/dots.svg'), items, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard = false, pressed, text, children, dropdownMenuStyle } = this.props;
|
|
||||||
const open = this.state.id === openDropdownId;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{children ? (
|
|
||||||
React.cloneElement(children, {
|
|
||||||
disabled,
|
|
||||||
onClick: this.handleClick,
|
|
||||||
onMouseDown: this.handleMouseDown,
|
|
||||||
onKeyDown: this.handleButtonKeyDown,
|
|
||||||
onKeyPress: this.handleKeyPress,
|
|
||||||
ref: this.setTargetRef,
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<IconButton
|
|
||||||
disabled={disabled}
|
|
||||||
className={classNames({
|
|
||||||
'text-gray-600 hover:text-gray-700 dark:hover:text-gray-500': true,
|
|
||||||
'text-gray-700 dark:text-gray-500': open,
|
|
||||||
})}
|
|
||||||
title={title}
|
|
||||||
src={src}
|
|
||||||
aria-pressed={pressed}
|
|
||||||
text={text}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
onMouseDown={this.handleMouseDown}
|
|
||||||
onKeyDown={this.handleButtonKeyDown}
|
|
||||||
onKeyPress={this.handleKeyPress}
|
|
||||||
ref={this.setTargetRef}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
|
|
||||||
<RouterDropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} style={dropdownMenuStyle} />
|
|
||||||
</Overlay>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withRouter(Dropdown);
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Counter, Icon } from '../ui';
|
||||||
|
|
||||||
|
export interface MenuItem {
|
||||||
|
action?: React.EventHandler<React.KeyboardEvent | React.MouseEvent>
|
||||||
|
active?: boolean
|
||||||
|
count?: number
|
||||||
|
destructive?: boolean
|
||||||
|
href?: string
|
||||||
|
icon?: string
|
||||||
|
meta?: string
|
||||||
|
middleClick?(event: React.MouseEvent): void
|
||||||
|
target?: React.HTMLAttributeAnchorTarget
|
||||||
|
text: string
|
||||||
|
to?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDropdownMenuItem {
|
||||||
|
index: number
|
||||||
|
item: MenuItem | null
|
||||||
|
onClick?(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const itemRef = useRef<HTMLAnchorElement>(null);
|
||||||
|
|
||||||
|
const handleClick: React.EventHandler<React.MouseEvent | React.KeyboardEvent> = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (!item) return;
|
||||||
|
if (onClick) onClick();
|
||||||
|
|
||||||
|
|
||||||
|
if (item.to) {
|
||||||
|
event.preventDefault();
|
||||||
|
history.push(item.to);
|
||||||
|
} else if (typeof item.action === 'function') {
|
||||||
|
event.preventDefault();
|
||||||
|
item.action(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAuxClick: React.EventHandler<React.MouseEvent> = (event) => {
|
||||||
|
if (!item) return;
|
||||||
|
if (onClick) onClick();
|
||||||
|
|
||||||
|
if (event.button === 1 && item.middleClick) {
|
||||||
|
item.middleClick(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleItemKeyPress: React.EventHandler<React.KeyboardEvent> = (event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
handleClick(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const firstItem = index === 0;
|
||||||
|
|
||||||
|
if (itemRef.current && firstItem) {
|
||||||
|
itemRef.current.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
|
}, [itemRef.current, index]);
|
||||||
|
|
||||||
|
if (item === null) {
|
||||||
|
return <li className='mx-2 my-1 h-[2px] bg-gray-100 dark:bg-gray-800' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className='truncate focus-visible:ring-2 focus-visible:ring-primary-500'>
|
||||||
|
<a
|
||||||
|
href={item.href || item.to || '#'}
|
||||||
|
role='button'
|
||||||
|
tabIndex={0}
|
||||||
|
ref={itemRef}
|
||||||
|
data-index={index}
|
||||||
|
onClick={handleClick}
|
||||||
|
onAuxClick={handleAuxClick}
|
||||||
|
onKeyPress={handleItemKeyPress}
|
||||||
|
target={item.target}
|
||||||
|
title={item.text}
|
||||||
|
className={
|
||||||
|
clsx({
|
||||||
|
'flex px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none cursor-pointer': true,
|
||||||
|
'text-danger-600 dark:text-danger-400': item.destructive,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.icon && <Icon src={item.icon} className='mr-3 h-5 w-5 flex-none rtl:ml-3 rtl:mr-0' />}
|
||||||
|
|
||||||
|
<span className='truncate'>{item.text}</span>
|
||||||
|
|
||||||
|
{item.count ? (
|
||||||
|
<span className='ml-auto h-5 w-5 flex-none'>
|
||||||
|
<Counter count={item.count} />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropdownMenuItem;
|
|
@ -0,0 +1,346 @@
|
||||||
|
import { offset, Placement, useFloating, flip, arrow } from '@floating-ui/react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import {
|
||||||
|
closeDropdownMenu as closeDropdownMenuRedux,
|
||||||
|
openDropdownMenu,
|
||||||
|
} from 'soapbox/actions/dropdown-menu';
|
||||||
|
import { closeModal, openModal } from 'soapbox/actions/modals';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
import { isUserTouching } from 'soapbox/is-mobile';
|
||||||
|
|
||||||
|
import { IconButton, Portal } from '../ui';
|
||||||
|
|
||||||
|
import DropdownMenuItem, { MenuItem } from './dropdown-menu-item';
|
||||||
|
|
||||||
|
import type { Status } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
export type Menu = Array<MenuItem | null>;
|
||||||
|
|
||||||
|
interface IDropdownMenu {
|
||||||
|
children?: React.ReactElement
|
||||||
|
disabled?: boolean
|
||||||
|
items: Menu
|
||||||
|
onClose?: () => void
|
||||||
|
onOpen?: () => void
|
||||||
|
onShiftClick?: React.EventHandler<React.MouseEvent | React.KeyboardEvent>
|
||||||
|
placement?: Placement
|
||||||
|
src?: string
|
||||||
|
status?: Status
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||||
|
|
||||||
|
const DropdownMenu = (props: IDropdownMenu) => {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
disabled,
|
||||||
|
items,
|
||||||
|
onClose,
|
||||||
|
onOpen,
|
||||||
|
onShiftClick,
|
||||||
|
placement: initialPlacement = 'top',
|
||||||
|
src = require('@tabler/icons/dots.svg'),
|
||||||
|
title = 'Menu',
|
||||||
|
...filteredProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
const isOpenRedux = useAppSelector(state => state.dropdown_menu.isOpen);
|
||||||
|
|
||||||
|
const arrowRef = useRef<HTMLDivElement>(null);
|
||||||
|
const activeElement = useRef<Element | null>(null);
|
||||||
|
|
||||||
|
const isOnMobile = isUserTouching();
|
||||||
|
|
||||||
|
const { x, y, strategy, refs, middlewareData, placement } = useFloating<HTMLButtonElement>({
|
||||||
|
placement: initialPlacement,
|
||||||
|
middleware: [
|
||||||
|
offset(12),
|
||||||
|
flip(),
|
||||||
|
arrow({
|
||||||
|
element: arrowRef,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClick: React.EventHandler<
|
||||||
|
React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
|
||||||
|
> = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (onShiftClick && event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
onShiftClick(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
handleClose();
|
||||||
|
} else {
|
||||||
|
handleOpen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On mobile screens, let's replace the Popper dropdown with a Modal.
|
||||||
|
*/
|
||||||
|
const handleOpen = () => {
|
||||||
|
if (isOnMobile) {
|
||||||
|
dispatch(
|
||||||
|
openModal('ACTIONS', {
|
||||||
|
status: filteredProps.status,
|
||||||
|
actions: items,
|
||||||
|
onClick: handleItemClick,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dispatch(openDropdownMenu());
|
||||||
|
setIsOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onOpen) {
|
||||||
|
onOpen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (activeElement.current && activeElement.current === refs.reference.current) {
|
||||||
|
(activeElement.current as any).focus();
|
||||||
|
activeElement.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOnMobile) {
|
||||||
|
dispatch(closeModal('ACTIONS'));
|
||||||
|
} else {
|
||||||
|
closeDropdownMenu();
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onClose) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDropdownMenu = () => {
|
||||||
|
if (isOpenRedux) {
|
||||||
|
dispatch(closeDropdownMenuRedux());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown: React.EventHandler<React.MouseEvent | React.KeyboardEvent> = () => {
|
||||||
|
if (!isOpen) {
|
||||||
|
activeElement.current = document.activeElement;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleButtonKeyDown: React.EventHandler<React.KeyboardEvent> = (event) => {
|
||||||
|
switch (event.key) {
|
||||||
|
case ' ':
|
||||||
|
case 'Enter':
|
||||||
|
handleMouseDown(event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress: React.EventHandler<React.KeyboardEvent<HTMLButtonElement>> = (event) => {
|
||||||
|
switch (event.key) {
|
||||||
|
case ' ':
|
||||||
|
case 'Enter':
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
handleClick(event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleItemClick: React.EventHandler<React.MouseEvent> = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const i = Number(event.currentTarget.getAttribute('data-index'));
|
||||||
|
const item = items[i];
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const { action, to } = item;
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
|
||||||
|
if (typeof action === 'function') {
|
||||||
|
action(event);
|
||||||
|
} else if (to) {
|
||||||
|
history.push(to);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDocumentClick = (event: Event) => {
|
||||||
|
if (refs.floating.current && !refs.floating.current.contains(event.target as Node)) {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (!refs.floating.current) return;
|
||||||
|
|
||||||
|
const items = Array.from(refs.floating.current.getElementsByTagName('a'));
|
||||||
|
const index = items.indexOf(document.activeElement as any);
|
||||||
|
|
||||||
|
let element = null;
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
element = items[index + 1] || items[0];
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
element = items[index - 1] || items[items.length - 1];
|
||||||
|
break;
|
||||||
|
case 'Tab':
|
||||||
|
if (e.shiftKey) {
|
||||||
|
element = items[index - 1] || items[items.length - 1];
|
||||||
|
} else {
|
||||||
|
element = items[index + 1] || items[0];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Home':
|
||||||
|
element = items[0];
|
||||||
|
break;
|
||||||
|
case 'End':
|
||||||
|
element = items[items.length - 1];
|
||||||
|
break;
|
||||||
|
case 'Escape':
|
||||||
|
handleClose();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const arrowProps: React.CSSProperties = useMemo(() => {
|
||||||
|
if (middlewareData.arrow) {
|
||||||
|
const { x, y } = middlewareData.arrow;
|
||||||
|
|
||||||
|
const staticPlacement = {
|
||||||
|
top: 'bottom',
|
||||||
|
right: 'left',
|
||||||
|
bottom: 'top',
|
||||||
|
left: 'right',
|
||||||
|
}[placement.split('-')[0]];
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: x !== null ? `${x}px` : '',
|
||||||
|
top: y !== null ? `${y}px` : '',
|
||||||
|
// Ensure the static side gets unset when
|
||||||
|
// flipping to other placements' axes.
|
||||||
|
right: '',
|
||||||
|
bottom: '',
|
||||||
|
[staticPlacement as string]: `${(-(arrowRef.current?.offsetWidth || 0)) / 2}px`,
|
||||||
|
transform: 'rotate(45deg)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}, [middlewareData.arrow, placement]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
closeDropdownMenu();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('click', handleDocumentClick, false);
|
||||||
|
document.addEventListener('keydown', handleKeyDown, false);
|
||||||
|
document.addEventListener('touchend', handleDocumentClick, listenerOptions);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('click', handleDocumentClick);
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
document.removeEventListener('touchend', handleDocumentClick);
|
||||||
|
};
|
||||||
|
}, [refs.floating.current]);
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children ? (
|
||||||
|
React.cloneElement(children, {
|
||||||
|
disabled,
|
||||||
|
onClick: handleClick,
|
||||||
|
onMouseDown: handleMouseDown,
|
||||||
|
onKeyDown: handleButtonKeyDown,
|
||||||
|
onKeyPress: handleKeyPress,
|
||||||
|
ref: refs.setReference,
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<IconButton
|
||||||
|
disabled={disabled}
|
||||||
|
className={clsx({
|
||||||
|
'text-gray-600 hover:text-gray-700 dark:hover:text-gray-500': true,
|
||||||
|
'text-gray-700 dark:text-gray-500': isOpen,
|
||||||
|
})}
|
||||||
|
title={title}
|
||||||
|
src={src}
|
||||||
|
onClick={handleClick}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onKeyDown={handleButtonKeyDown}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
ref={refs.setReference}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isOpen ? (
|
||||||
|
<Portal>
|
||||||
|
<div
|
||||||
|
data-testid='dropdown-menu'
|
||||||
|
ref={refs.setFloating}
|
||||||
|
className={
|
||||||
|
clsx('z-[1001] w-56 rounded-md bg-white py-1 shadow-lg transition-opacity duration-100 focus:outline-none dark:bg-gray-900 dark:ring-2 dark:ring-primary-700', {
|
||||||
|
'opacity-0 pointer-events-none': !isOpen,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
position: strategy,
|
||||||
|
top: y ?? 0,
|
||||||
|
left: x ?? 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
{items.map((item, idx) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={idx}
|
||||||
|
item={item}
|
||||||
|
index={idx}
|
||||||
|
onClick={handleClose}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* Arrow */}
|
||||||
|
<div
|
||||||
|
ref={arrowRef}
|
||||||
|
style={arrowProps}
|
||||||
|
className='pointer-events-none absolute z-[-1] h-3 w-3 bg-white dark:bg-gray-900'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropdownMenu;
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { default } from './dropdown-menu';
|
||||||
|
export type { Menu } from './dropdown-menu';
|
||||||
|
export type { MenuItem } from './dropdown-menu-item';
|
|
@ -1,142 +0,0 @@
|
||||||
// import classNames from 'clsx';
|
|
||||||
import React from 'react';
|
|
||||||
import { HotKeys } from 'react-hotkeys';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
|
||||||
import { EmojiSelector as RealEmojiSelector } from 'soapbox/components/ui';
|
|
||||||
|
|
||||||
import type { List as ImmutableList } from 'immutable';
|
|
||||||
import type { RootState } from 'soapbox/store';
|
|
||||||
|
|
||||||
const mapStateToProps = (state: RootState) => ({
|
|
||||||
allowedEmoji: getSoapboxConfig(state).allowedEmoji,
|
|
||||||
});
|
|
||||||
|
|
||||||
interface IEmojiSelector {
|
|
||||||
allowedEmoji: ImmutableList<string>,
|
|
||||||
onReact: (emoji: string) => void,
|
|
||||||
onUnfocus: () => void,
|
|
||||||
visible: boolean,
|
|
||||||
focused?: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
class EmojiSelector extends ImmutablePureComponent<IEmojiSelector> {
|
|
||||||
|
|
||||||
static defaultProps: Partial<IEmojiSelector> = {
|
|
||||||
onReact: () => { },
|
|
||||||
onUnfocus: () => { },
|
|
||||||
visible: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
node?: HTMLDivElement = undefined;
|
|
||||||
|
|
||||||
handleBlur: React.FocusEventHandler<HTMLDivElement> = e => {
|
|
||||||
const { focused, onUnfocus } = this.props;
|
|
||||||
|
|
||||||
if (focused && (!e.currentTarget || !e.currentTarget.classList.contains('emoji-react-selector__emoji'))) {
|
|
||||||
onUnfocus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_selectPreviousEmoji = (i: number): void => {
|
|
||||||
if (!this.node) return;
|
|
||||||
|
|
||||||
if (i !== 0) {
|
|
||||||
const button: HTMLButtonElement | null = this.node.querySelector(`.emoji-react-selector__emoji:nth-child(${i})`);
|
|
||||||
button?.focus();
|
|
||||||
} else {
|
|
||||||
const button: HTMLButtonElement | null = this.node.querySelector('.emoji-react-selector__emoji:last-child');
|
|
||||||
button?.focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_selectNextEmoji = (i: number) => {
|
|
||||||
if (!this.node) return;
|
|
||||||
|
|
||||||
if (i !== this.props.allowedEmoji.size - 1) {
|
|
||||||
const button: HTMLButtonElement | null = this.node.querySelector(`.emoji-react-selector__emoji:nth-child(${i + 2})`);
|
|
||||||
button?.focus();
|
|
||||||
} else {
|
|
||||||
const button: HTMLButtonElement | null = this.node.querySelector('.emoji-react-selector__emoji:first-child');
|
|
||||||
button?.focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleKeyDown = (i: number): React.KeyboardEventHandler => e => {
|
|
||||||
const { onUnfocus } = this.props;
|
|
||||||
|
|
||||||
switch (e.key) {
|
|
||||||
case 'Tab':
|
|
||||||
e.preventDefault();
|
|
||||||
if (e.shiftKey) this._selectPreviousEmoji(i);
|
|
||||||
else this._selectNextEmoji(i);
|
|
||||||
break;
|
|
||||||
case 'Left':
|
|
||||||
case 'ArrowLeft':
|
|
||||||
this._selectPreviousEmoji(i);
|
|
||||||
break;
|
|
||||||
case 'Right':
|
|
||||||
case 'ArrowRight':
|
|
||||||
this._selectNextEmoji(i);
|
|
||||||
break;
|
|
||||||
case 'Escape':
|
|
||||||
onUnfocus();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleReact = (emoji: string) => (): void => {
|
|
||||||
const { onReact, focused, onUnfocus } = this.props;
|
|
||||||
|
|
||||||
onReact(emoji);
|
|
||||||
|
|
||||||
if (focused) {
|
|
||||||
onUnfocus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handlers = {
|
|
||||||
open: () => { },
|
|
||||||
};
|
|
||||||
|
|
||||||
setRef = (c: HTMLDivElement): void => {
|
|
||||||
this.node = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { visible, focused, allowedEmoji, onReact } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HotKeys handlers={this.handlers}>
|
|
||||||
{/*<div
|
|
||||||
className={classNames('flex absolute bg-white dark:bg-gray-500 px-2 py-3 rounded-full shadow-md opacity-0 pointer-events-none duration-100 w-max', { 'opacity-100 pointer-events-auto z-[999]': visible || focused })}
|
|
||||||
onBlur={this.handleBlur}
|
|
||||||
ref={this.setRef}
|
|
||||||
>
|
|
||||||
{allowedEmoji.map((emoji, i) => (
|
|
||||||
<button
|
|
||||||
key={i}
|
|
||||||
className='emoji-react-selector__emoji'
|
|
||||||
onClick={this.handleReact(emoji)}
|
|
||||||
onKeyDown={this.handleKeyDown(i)}
|
|
||||||
tabIndex={(visible || focused) ? 0 : -1}
|
|
||||||
>
|
|
||||||
<Emoji emoji={emoji} />
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>*/}
|
|
||||||
<RealEmojiSelector
|
|
||||||
emojis={allowedEmoji.toArray()}
|
|
||||||
onReact={onReact}
|
|
||||||
visible={visible}
|
|
||||||
focused={focused}
|
|
||||||
/>
|
|
||||||
</HotKeys>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(EmojiSelector);
|
|
|
@ -31,10 +31,10 @@ interface Props extends ReturnType<typeof mapStateToProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
hasError: boolean,
|
hasError: boolean
|
||||||
error: any,
|
error: any
|
||||||
componentStack: any,
|
componentStack: any
|
||||||
browser?: Bowser.Parser.Parser,
|
browser?: Bowser.Parser.Parser
|
||||||
}
|
}
|
||||||
|
|
||||||
class ErrorBoundary extends React.PureComponent<Props, State> {
|
class ErrorBoundary extends React.PureComponent<Props, State> {
|
||||||
|
@ -113,17 +113,17 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
||||||
const errorText = this.getErrorText();
|
const errorText = this.getErrorText();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-screen pt-16 pb-12 flex flex-col bg-white dark:bg-primary-900'>
|
<div className='flex h-screen flex-col bg-white pb-12 pt-16 dark:bg-primary-900'>
|
||||||
<main className='flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8'>
|
<main className='mx-auto flex w-full max-w-7xl grow flex-col justify-center px-4 sm:px-6 lg:px-8'>
|
||||||
<div className='flex-shrink-0 flex justify-center'>
|
<div className='flex shrink-0 justify-center'>
|
||||||
<a href='/' className='inline-flex'>
|
<a href='/' className='inline-flex'>
|
||||||
<SiteLogo alt='Logo' className='h-12 w-auto cursor-pointer' />
|
<SiteLogo alt='Logo' className='h-12 w-auto cursor-pointer' />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='py-8'>
|
<div className='py-8'>
|
||||||
<div className='text-center max-w-xl mx-auto space-y-2'>
|
<div className='mx-auto max-w-xl space-y-2 text-center'>
|
||||||
<h1 className='text-3xl font-extrabold text-gray-900 dark:text-gray-500 tracking-tight sm:text-4xl'>
|
<h1 className='text-3xl font-extrabold tracking-tight text-gray-900 dark:text-gray-500 sm:text-4xl'>
|
||||||
<FormattedMessage id='alert.unexpected.message' defaultMessage='Something went wrong.' />
|
<FormattedMessage id='alert.unexpected.message' defaultMessage='Something went wrong.' />
|
||||||
</h1>
|
</h1>
|
||||||
<p className='text-lg text-gray-700 dark:text-gray-600'>
|
<p className='text-lg text-gray-700 dark:text-gray-600'>
|
||||||
|
@ -132,7 +132,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
||||||
defaultMessage="We're sorry for the interruption. If the problem persists, please reach out to our support team. You may also try to {clearCookies} (this will log you out)."
|
defaultMessage="We're sorry for the interruption. If the problem persists, please reach out to our support team. You may also try to {clearCookies} (this will log you out)."
|
||||||
values={{
|
values={{
|
||||||
clearCookies: (
|
clearCookies: (
|
||||||
<a href='/' onClick={this.clearCookies} className='text-primary-600 dark:text-accent-blue hover:underline'>
|
<a href='/' onClick={this.clearCookies} className='text-primary-600 hover:underline dark:text-accent-blue'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='alert.unexpected.clear_cookies'
|
id='alert.unexpected.clear_cookies'
|
||||||
defaultMessage='clear cookies and browser data'
|
defaultMessage='clear cookies and browser data'
|
||||||
|
@ -150,7 +150,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<div className='mt-10'>
|
<div className='mt-10'>
|
||||||
<a href='/' className='text-base font-medium text-primary-600 dark:text-accent-blue hover:underline'>
|
<a href='/' className='text-base font-medium text-primary-600 hover:underline dark:text-accent-blue'>
|
||||||
<FormattedMessage id='alert.unexpected.return_home' defaultMessage='Return Home' />
|
<FormattedMessage id='alert.unexpected.return_home' defaultMessage='Return Home' />
|
||||||
<span aria-hidden='true'> →</span>
|
<span aria-hidden='true'> →</span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -158,11 +158,11 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isProduction && (
|
{!isProduction && (
|
||||||
<div className='py-16 max-w-lg mx-auto space-y-4'>
|
<div className='mx-auto max-w-lg space-y-4 py-16'>
|
||||||
{errorText && (
|
{errorText && (
|
||||||
<textarea
|
<textarea
|
||||||
ref={this.setTextareaRef}
|
ref={this.setTextareaRef}
|
||||||
className='h-48 p-4 shadow-sm bg-gray-100 text-gray-900 dark:text-gray-100 dark:bg-gray-800 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 dark:border-gray-700 rounded-md font-mono'
|
className='block h-48 w-full rounded-md border-gray-300 bg-gray-100 p-4 font-mono text-gray-900 shadow-sm focus:border-primary-500 focus:ring-2 focus:ring-primary-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 sm:text-sm'
|
||||||
value={errorText}
|
value={errorText}
|
||||||
onClick={this.handleCopy}
|
onClick={this.handleCopy}
|
||||||
readOnly
|
readOnly
|
||||||
|
@ -180,11 +180,11 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className='flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8'>
|
<footer className='mx-auto w-full max-w-7xl shrink-0 px-4 sm:px-6 lg:px-8'>
|
||||||
<HStack justifyContent='center' space={4} element='nav'>
|
<HStack justifyContent='center' space={4} element='nav'>
|
||||||
{links.get('status') && (
|
{links.get('status') && (
|
||||||
<>
|
<>
|
||||||
<a href={links.get('status')} className='text-sm font-medium text-gray-700 dark:text-gray-600 hover:underline'>
|
<a href={links.get('status')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||||
<FormattedMessage id='alert.unexpected.links.status' defaultMessage='Status' />
|
<FormattedMessage id='alert.unexpected.links.status' defaultMessage='Status' />
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
|
@ -193,7 +193,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
||||||
{links.get('help') && (
|
{links.get('help') && (
|
||||||
<>
|
<>
|
||||||
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
|
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
|
||||||
<a href={links.get('help')} className='text-sm font-medium text-gray-700 dark:text-gray-600 hover:underline'>
|
<a href={links.get('help')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||||
<FormattedMessage id='alert.unexpected.links.help' defaultMessage='Help Center' />
|
<FormattedMessage id='alert.unexpected.links.help' defaultMessage='Help Center' />
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
|
@ -202,7 +202,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
||||||
{links.get('support') && (
|
{links.get('support') && (
|
||||||
<>
|
<>
|
||||||
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
|
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
|
||||||
<a href={links.get('support')} className='text-sm font-medium text-gray-700 dark:text-gray-600 hover:underline'>
|
<a href={links.get('support')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||||
<FormattedMessage id='alert.unexpected.links.support' defaultMessage='Support' />
|
<FormattedMessage id='alert.unexpected.links.support' defaultMessage='Support' />
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import classNames from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import VerificationBadge from './verification-badge';
|
||||||
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
bannerHeader: { id: 'event.banner', defaultMessage: 'Event banner' },
|
eventBanner: { id: 'event.banner', defaultMessage: 'Event banner' },
|
||||||
leaveConfirm: { id: 'confirmations.leave_event.confirm', defaultMessage: 'Leave event' },
|
leaveConfirm: { id: 'confirmations.leave_event.confirm', defaultMessage: 'Leave event' },
|
||||||
leaveMessage: { id: 'confirmations.leave_event.message', defaultMessage: 'If you want to rejoin the event, the request will be manually reviewed again. Are you sure you want to proceed?' },
|
leaveMessage: { id: 'confirmations.leave_event.message', defaultMessage: 'If you want to rejoin the event, the request will be manually reviewed again. Are you sure you want to proceed?' },
|
||||||
});
|
});
|
||||||
|
@ -51,12 +51,12 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction,
|
||||||
));
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('w-full rounded-lg bg-gray-100 dark:bg-primary-800 relative overflow-hidden', className)}>
|
<div className={clsx('relative w-full overflow-hidden rounded-lg bg-gray-100 dark:bg-primary-800', className)}>
|
||||||
<div className='absolute top-28 right-3'>
|
<div className='absolute right-3 top-28'>
|
||||||
{floatingAction && action}
|
{floatingAction && action}
|
||||||
</div>
|
</div>
|
||||||
<div className='bg-primary-200 dark:bg-gray-600 h-40'>
|
<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.bannerHeader)} />}
|
{banner && <img className='h-full w-full object-cover' src={banner.url} alt={intl.formatMessage(messages.eventBanner)} />}
|
||||||
</div>
|
</div>
|
||||||
<Stack className='p-2.5' space={2}>
|
<Stack className='p-2.5' space={2}>
|
||||||
<HStack space={2} alignItems='center' justifyContent='between'>
|
<HStack space={2} alignItems='center' justifyContent='between'>
|
||||||
|
@ -65,7 +65,7 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction,
|
||||||
{!floatingAction && action}
|
{!floatingAction && action}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
<div className='flex gap-y-1 gap-x-2 flex-wrap text-gray-700 dark:text-gray-600'>
|
<div className='flex flex-wrap gap-x-2 gap-y-1 text-gray-700 dark:text-gray-600'>
|
||||||
<HStack alignItems='center' space={2}>
|
<HStack alignItems='center' space={2}>
|
||||||
<Icon src={require('@tabler/icons/user.svg')} />
|
<Icon src={require('@tabler/icons/user.svg')} />
|
||||||
<HStack space={1} alignItems='center' grow>
|
<HStack space={1} alignItems='center' grow>
|
||||||
|
|
|
@ -3,14 +3,14 @@ import React, { useEffect, useRef } from 'react';
|
||||||
import { isIOS } from 'soapbox/is-mobile';
|
import { isIOS } from 'soapbox/is-mobile';
|
||||||
|
|
||||||
interface IExtendedVideoPlayer {
|
interface IExtendedVideoPlayer {
|
||||||
src: string,
|
src: string
|
||||||
alt?: string,
|
alt?: string
|
||||||
width?: number,
|
width?: number
|
||||||
height?: number,
|
height?: number
|
||||||
time?: number,
|
time?: number
|
||||||
controls?: boolean,
|
controls?: boolean
|
||||||
muted?: boolean,
|
muted?: boolean
|
||||||
onClick?: () => void,
|
onClick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExtendedVideoPlayer: React.FC<IExtendedVideoPlayer> = ({ src, alt, time, controls, muted, onClick }) => {
|
const ExtendedVideoPlayer: React.FC<IExtendedVideoPlayer> = ({ src, alt, time, controls, muted, onClick }) => {
|
||||||
|
|
|
@ -5,13 +5,13 @@
|
||||||
* @see soapbox/components/icon
|
* @see soapbox/components/icon
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import classNames from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export interface IForkAwesomeIcon extends React.HTMLAttributes<HTMLLIElement> {
|
export interface IForkAwesomeIcon extends React.HTMLAttributes<HTMLLIElement> {
|
||||||
id: string,
|
id: string
|
||||||
className?: string,
|
className?: string
|
||||||
fixedWidth?: boolean,
|
fixedWidth?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const ForkAwesomeIcon: React.FC<IForkAwesomeIcon> = ({ id, className, fixedWidth, ...rest }) => {
|
const ForkAwesomeIcon: React.FC<IForkAwesomeIcon> = ({ id, className, fixedWidth, ...rest }) => {
|
||||||
|
@ -25,7 +25,7 @@ const ForkAwesomeIcon: React.FC<IForkAwesomeIcon> = ({ id, className, fixedWidth
|
||||||
<i
|
<i
|
||||||
role='img'
|
role='img'
|
||||||
// alt={alt}
|
// alt={alt}
|
||||||
className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
|
className={clsx('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue