diff --git a/app/soapbox/actions/alerts.ts b/app/soapbox/actions/alerts.ts index 3e5aed4b3..8f200563a 100644 --- a/app/soapbox/actions/alerts.ts +++ b/app/soapbox/actions/alerts.ts @@ -5,7 +5,7 @@ import { httpErrorMessages } from 'soapbox/utils/errors'; import type { SnackbarActionSeverity } from './snackbar'; import type { AnyAction } from '@reduxjs/toolkit'; import type { AxiosError } from 'axios'; -import type { NotificationObject } from 'soapbox/react-notification'; +import type { NotificationObject } from 'react-notification'; const messages = defineMessages({ unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, diff --git a/app/soapbox/actions/group_editor.ts b/app/soapbox/actions/group_editor.ts deleted file mode 100644 index 23f3491ad..000000000 --- a/app/soapbox/actions/group_editor.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { isLoggedIn } from 'soapbox/utils/auth'; - -import api from '../api'; - -import type { AxiosError } from 'axios'; -import type { History } from 'history'; -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_EDITOR_VALUE_CHANGE = 'GROUP_EDITOR_VALUE_CHANGE'; -const GROUP_EDITOR_RESET = 'GROUP_EDITOR_RESET'; -const GROUP_EDITOR_SETUP = 'GROUP_EDITOR_SETUP'; - -const submit = (routerHistory: History) => - (dispatch: AppDispatch, getState: () => RootState) => { - const groupId = getState().group_editor.get('groupId') as string; - const title = getState().group_editor.get('title') as string; - const description = getState().group_editor.get('description') as string; - const coverImage = getState().group_editor.get('coverImage') as any; - - if (groupId === null) { - dispatch(create(title, description, coverImage, routerHistory)); - } else { - dispatch(update(groupId, title, description, coverImage, routerHistory)); - } - }; - -const create = (title: string, description: string, coverImage: File, routerHistory: History) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(createRequest()); - - const formData = new FormData(); - formData.append('title', title); - formData.append('description', description); - - if (coverImage !== null) { - formData.append('cover_image', coverImage); - } - - api(getState).post('/api/v1/groups', formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(({ data }) => { - dispatch(createSuccess(data)); - routerHistory.push(`/groups/${data.id}`); - }).catch(err => dispatch(createFail(err))); - }; - -const createRequest = (id?: string) => ({ - type: GROUP_CREATE_REQUEST, - id, -}); - -const createSuccess = (group: APIEntity) => ({ - type: GROUP_CREATE_SUCCESS, - group, -}); - -const createFail = (error: AxiosError) => ({ - type: GROUP_CREATE_FAIL, - error, -}); - -const update = (groupId: string, title: string, description: string, coverImage: File, routerHistory: History) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(updateRequest(groupId)); - - const formData = new FormData(); - formData.append('title', title); - formData.append('description', description); - - if (coverImage !== null) { - formData.append('cover_image', coverImage); - } - - api(getState).put(`/api/v1/groups/${groupId}`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(({ data }) => { - dispatch(updateSuccess(data)); - routerHistory.push(`/groups/${data.id}`); - }).catch(err => dispatch(updateFail(err))); - }; - -const updateRequest = (id: string) => ({ - type: GROUP_UPDATE_REQUEST, - id, -}); - -const updateSuccess = (group: APIEntity) => ({ - type: GROUP_UPDATE_SUCCESS, - group, -}); - -const updateFail = (error: AxiosError) => ({ - type: GROUP_UPDATE_FAIL, - error, -}); - -const changeValue = (field: string, value: string | File) => ({ - type: GROUP_EDITOR_VALUE_CHANGE, - field, - value, -}); - -const reset = () => ({ - type: GROUP_EDITOR_RESET, -}); - -const setUp = (group: string) => ({ - type: GROUP_EDITOR_SETUP, - group, -}); - -export { - GROUP_CREATE_REQUEST, - GROUP_CREATE_SUCCESS, - GROUP_CREATE_FAIL, - GROUP_UPDATE_REQUEST, - GROUP_UPDATE_SUCCESS, - GROUP_UPDATE_FAIL, - GROUP_EDITOR_VALUE_CHANGE, - GROUP_EDITOR_RESET, - GROUP_EDITOR_SETUP, - submit, - create, - createRequest, - createSuccess, - createFail, - update, - updateRequest, - updateSuccess, - updateFail, - changeValue, - reset, - setUp, -}; diff --git a/app/soapbox/actions/groups.ts b/app/soapbox/actions/groups.ts deleted file mode 100644 index 808cc3204..000000000 --- a/app/soapbox/actions/groups.ts +++ /dev/null @@ -1,550 +0,0 @@ -import { AxiosError } from 'axios'; - -import { isLoggedIn } from 'soapbox/utils/auth'; - -import api, { getLinks } from '../api'; - -import { fetchRelationships } from './accounts'; -import { importFetchedAccounts } from './importer'; - -import type { AppDispatch, RootState } from 'soapbox/store'; -import type { APIEntity } from 'soapbox/types/entities'; - -const GROUP_FETCH_REQUEST = 'GROUP_FETCH_REQUEST'; -const GROUP_FETCH_SUCCESS = 'GROUP_FETCH_SUCCESS'; -const GROUP_FETCH_FAIL = 'GROUP_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 GROUPS_FETCH_REQUEST = 'GROUPS_FETCH_REQUEST'; -const GROUPS_FETCH_SUCCESS = 'GROUPS_FETCH_SUCCESS'; -const GROUPS_FETCH_FAIL = 'GROUPS_FETCH_FAIL'; - -const GROUP_JOIN_REQUEST = 'GROUP_JOIN_REQUEST'; -const GROUP_JOIN_SUCCESS = 'GROUP_JOIN_SUCCESS'; -const GROUP_JOIN_FAIL = 'GROUP_JOIN_FAIL'; - -const GROUP_LEAVE_REQUEST = 'GROUP_LEAVE_REQUEST'; -const GROUP_LEAVE_SUCCESS = 'GROUP_LEAVE_SUCCESS'; -const GROUP_LEAVE_FAIL = 'GROUP_LEAVE_FAIL'; - -const GROUP_MEMBERS_FETCH_REQUEST = 'GROUP_MEMBERS_FETCH_REQUEST'; -const GROUP_MEMBERS_FETCH_SUCCESS = 'GROUP_MEMBERS_FETCH_SUCCESS'; -const GROUP_MEMBERS_FETCH_FAIL = 'GROUP_MEMBERS_FETCH_FAIL'; - -const GROUP_MEMBERS_EXPAND_REQUEST = 'GROUP_MEMBERS_EXPAND_REQUEST'; -const GROUP_MEMBERS_EXPAND_SUCCESS = 'GROUP_MEMBERS_EXPAND_SUCCESS'; -const GROUP_MEMBERS_EXPAND_FAIL = 'GROUP_MEMBERS_EXPAND_FAIL'; - -const GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST = 'GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST'; -const GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS'; -const GROUP_REMOVED_ACCOUNTS_FETCH_FAIL = 'GROUP_REMOVED_ACCOUNTS_FETCH_FAIL'; - -const GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST = 'GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST'; -const GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS'; -const GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL = 'GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL'; - -const GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST'; -const GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS'; -const GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL = 'GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL'; - -const GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST'; -const GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS'; -const GROUP_REMOVED_ACCOUNTS_CREATE_FAIL = 'GROUP_REMOVED_ACCOUNTS_CREATE_FAIL'; - -const GROUP_REMOVE_STATUS_REQUEST = 'GROUP_REMOVE_STATUS_REQUEST'; -const GROUP_REMOVE_STATUS_SUCCESS = 'GROUP_REMOVE_STATUS_SUCCESS'; -const GROUP_REMOVE_STATUS_FAIL = 'GROUP_REMOVE_STATUS_FAIL'; - -const fetchGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(fetchGroupRelationships([id])); - - if (getState().groups.get(id)) { - return; - } - - dispatch(fetchGroupRequest(id)); - - api(getState).get(`/api/v1/groups/${id}`) - .then(({ 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 fetchGroupRelationships = (groupIds: string[]) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - const loadedRelationships = getState().group_relationships; - const newGroupIds = groupIds.filter(id => loadedRelationships.get(id, null) === null); - - if (newGroupIds.length === 0) { - return; - } - - dispatch(fetchGroupRelationshipsRequest(newGroupIds)); - - api(getState).get(`/api/v1/groups/${newGroupIds[0]}/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, -}); - -const fetchGroups = (tab: string) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(fetchGroupsRequest()); - - api(getState).get('/api/v1/groups?tab=' + tab) - .then(({ data }) => { - dispatch(fetchGroupsSuccess(data, tab)); - dispatch(fetchGroupRelationships(data.map((item: APIEntity) => item.id))); - }) - .catch(err => dispatch(fetchGroupsFail(err))); -}; - -const fetchGroupsRequest = () => ({ - type: GROUPS_FETCH_REQUEST, -}); - -const fetchGroupsSuccess = (groups: APIEntity[], tab: string) => ({ - type: GROUPS_FETCH_SUCCESS, - groups, - tab, -}); - -const fetchGroupsFail = (error: AxiosError) => ({ - type: GROUPS_FETCH_FAIL, - error, -}); - -const joinGroup = (id: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(joinGroupRequest(id)); - - api(getState).post(`/api/v1/groups/${id}/accounts`).then(response => { - dispatch(joinGroupSuccess(response.data)); - }).catch(error => { - dispatch(joinGroupFail(id, error)); - }); - }; - -const leaveGroup = (id: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(leaveGroupRequest(id)); - - api(getState).delete(`/api/v1/groups/${id}/accounts`).then(response => { - dispatch(leaveGroupSuccess(response.data)); - }).catch(error => { - dispatch(leaveGroupFail(id, error)); - }); - }; - -const joinGroupRequest = (id: string) => ({ - type: GROUP_JOIN_REQUEST, - id, -}); - -const joinGroupSuccess = (relationship: APIEntity) => ({ - type: GROUP_JOIN_SUCCESS, - relationship, -}); - -const joinGroupFail = (id: string, error: AxiosError) => ({ - type: GROUP_JOIN_FAIL, - id, - error, -}); - -const leaveGroupRequest = (id: string) => ({ - type: GROUP_LEAVE_REQUEST, - id, -}); - -const leaveGroupSuccess = (relationship: APIEntity) => ({ - type: GROUP_LEAVE_SUCCESS, - relationship, -}); - -const leaveGroupFail = (id: string, error: AxiosError) => ({ - type: GROUP_LEAVE_FAIL, - id, - error, -}); - -const fetchMembers = (id: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(fetchMembersRequest(id)); - - api(getState).get(`/api/v1/groups/${id}/accounts`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchMembersSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); - }).catch(error => { - dispatch(fetchMembersFail(id, error)); - }); - }; - -const fetchMembersRequest = (id: string) => ({ - type: GROUP_MEMBERS_FETCH_REQUEST, - id, -}); - -const fetchMembersSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ - type: GROUP_MEMBERS_FETCH_SUCCESS, - id, - accounts, - next, -}); - -const fetchMembersFail = (id: string, error: AxiosError) => ({ - type: GROUP_MEMBERS_FETCH_FAIL, - id, - error, -}); - -const expandMembers = (id: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - const url = getState().user_lists.groups.get(id)!.next; - - if (url === null) { - return; - } - - dispatch(expandMembersRequest(id)); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(importFetchedAccounts(response.data)); - dispatch(expandMembersSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); - }).catch(error => { - dispatch(expandMembersFail(id, error)); - }); - }; - -const expandMembersRequest = (id: string) => ({ - type: GROUP_MEMBERS_EXPAND_REQUEST, - id, -}); - -const expandMembersSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ - type: GROUP_MEMBERS_EXPAND_SUCCESS, - id, - accounts, - next, -}); - -const expandMembersFail = (id: string, error: AxiosError) => ({ - type: GROUP_MEMBERS_EXPAND_FAIL, - id, - error, -}); - -const fetchRemovedAccounts = (id: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(fetchRemovedAccountsRequest(id)); - - api(getState).get(`/api/v1/groups/${id}/removed_accounts`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchRemovedAccountsSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); - }).catch(error => { - dispatch(fetchRemovedAccountsFail(id, error)); - }); - }; - -const fetchRemovedAccountsRequest = (id: string) => ({ - type: GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST, - id, -}); - -const fetchRemovedAccountsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ - type: GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS, - id, - accounts, - next, -}); - -const fetchRemovedAccountsFail = (id: string, error: AxiosError) => ({ - type: GROUP_REMOVED_ACCOUNTS_FETCH_FAIL, - id, - error, -}); - -const expandRemovedAccounts = (id: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - const url = getState().user_lists.groups_removed_accounts.get(id)!.next; - - if (url === null) { - return; - } - - dispatch(expandRemovedAccountsRequest(id)); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(importFetchedAccounts(response.data)); - dispatch(expandRemovedAccountsSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); - }).catch(error => { - dispatch(expandRemovedAccountsFail(id, error)); - }); - }; - -const expandRemovedAccountsRequest = (id: string) => ({ - type: GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST, - id, -}); - -const expandRemovedAccountsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ - type: GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS, - id, - accounts, - next, -}); - -const expandRemovedAccountsFail = (id: string, error: AxiosError) => ({ - type: GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL, - id, - error, -}); - -const removeRemovedAccount = (groupId: string, id: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(removeRemovedAccountRequest(groupId, id)); - - api(getState).delete(`/api/v1/groups/${groupId}/removed_accounts?account_id=${id}`).then(response => { - dispatch(removeRemovedAccountSuccess(groupId, id)); - }).catch(error => { - dispatch(removeRemovedAccountFail(groupId, id, error)); - }); - }; - -const removeRemovedAccountRequest = (groupId: string, id: string) => ({ - type: GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST, - groupId, - id, -}); - -const removeRemovedAccountSuccess = (groupId: string, id: string) => ({ - type: GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS, - groupId, - id, -}); - -const removeRemovedAccountFail = (groupId: string, id: string, error: AxiosError) => ({ - type: GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL, - groupId, - id, - error, -}); - -const createRemovedAccount = (groupId: string, id: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(createRemovedAccountRequest(groupId, id)); - - api(getState).post(`/api/v1/groups/${groupId}/removed_accounts?account_id=${id}`).then(response => { - dispatch(createRemovedAccountSuccess(groupId, id)); - }).catch(error => { - dispatch(createRemovedAccountFail(groupId, id, error)); - }); - }; - -const createRemovedAccountRequest = (groupId: string, id: string) => ({ - type: GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST, - groupId, - id, -}); - -const createRemovedAccountSuccess = (groupId: string, id: string) => ({ - type: GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS, - groupId, - id, -}); - -const createRemovedAccountFail = (groupId: string, id: string, error: AxiosError) => ({ - type: GROUP_REMOVED_ACCOUNTS_CREATE_FAIL, - groupId, - id, - error, -}); - -const groupRemoveStatus = (groupId: string, id: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(groupRemoveStatusRequest(groupId, id)); - - api(getState).delete(`/api/v1/groups/${groupId}/statuses/${id}`).then(response => { - dispatch(groupRemoveStatusSuccess(groupId, id)); - }).catch(error => { - dispatch(groupRemoveStatusFail(groupId, id, error)); - }); - }; - -const groupRemoveStatusRequest = (groupId: string, id: string) => ({ - type: GROUP_REMOVE_STATUS_REQUEST, - groupId, - id, -}); - -const groupRemoveStatusSuccess = (groupId: string, id: string) => ({ - type: GROUP_REMOVE_STATUS_SUCCESS, - groupId, - id, -}); - -const groupRemoveStatusFail = (groupId: string, id: string, error: AxiosError) => ({ - type: GROUP_REMOVE_STATUS_FAIL, - groupId, - id, - error, -}); - -export { - GROUP_FETCH_REQUEST, - GROUP_FETCH_SUCCESS, - GROUP_FETCH_FAIL, - GROUP_RELATIONSHIPS_FETCH_REQUEST, - GROUP_RELATIONSHIPS_FETCH_SUCCESS, - GROUP_RELATIONSHIPS_FETCH_FAIL, - GROUPS_FETCH_REQUEST, - GROUPS_FETCH_SUCCESS, - GROUPS_FETCH_FAIL, - GROUP_JOIN_REQUEST, - GROUP_JOIN_SUCCESS, - GROUP_JOIN_FAIL, - GROUP_LEAVE_REQUEST, - GROUP_LEAVE_SUCCESS, - GROUP_LEAVE_FAIL, - GROUP_MEMBERS_FETCH_REQUEST, - GROUP_MEMBERS_FETCH_SUCCESS, - GROUP_MEMBERS_FETCH_FAIL, - GROUP_MEMBERS_EXPAND_REQUEST, - GROUP_MEMBERS_EXPAND_SUCCESS, - GROUP_MEMBERS_EXPAND_FAIL, - GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST, - GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS, - GROUP_REMOVED_ACCOUNTS_FETCH_FAIL, - GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST, - GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS, - GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL, - GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST, - GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS, - GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL, - GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST, - GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS, - GROUP_REMOVED_ACCOUNTS_CREATE_FAIL, - GROUP_REMOVE_STATUS_REQUEST, - GROUP_REMOVE_STATUS_SUCCESS, - GROUP_REMOVE_STATUS_FAIL, - fetchGroup, - fetchGroupRequest, - fetchGroupSuccess, - fetchGroupFail, - fetchGroupRelationships, - fetchGroupRelationshipsRequest, - fetchGroupRelationshipsSuccess, - fetchGroupRelationshipsFail, - fetchGroups, - fetchGroupsRequest, - fetchGroupsSuccess, - fetchGroupsFail, - joinGroup, - leaveGroup, - joinGroupRequest, - joinGroupSuccess, - joinGroupFail, - leaveGroupRequest, - leaveGroupSuccess, - leaveGroupFail, - fetchMembers, - fetchMembersRequest, - fetchMembersSuccess, - fetchMembersFail, - expandMembers, - expandMembersRequest, - expandMembersSuccess, - expandMembersFail, - fetchRemovedAccounts, - fetchRemovedAccountsRequest, - fetchRemovedAccountsSuccess, - fetchRemovedAccountsFail, - expandRemovedAccounts, - expandRemovedAccountsRequest, - expandRemovedAccountsSuccess, - expandRemovedAccountsFail, - removeRemovedAccount, - removeRemovedAccountRequest, - removeRemovedAccountSuccess, - removeRemovedAccountFail, - createRemovedAccount, - createRemovedAccountRequest, - createRemovedAccountSuccess, - createRemovedAccountFail, - groupRemoveStatus, - groupRemoveStatusRequest, - groupRemoveStatusSuccess, - groupRemoveStatusFail, -}; diff --git a/app/soapbox/components/avatar_composite.js b/app/soapbox/components/avatar_composite.js deleted file mode 100644 index 59e4bab96..000000000 --- a/app/soapbox/components/avatar_composite.js +++ /dev/null @@ -1,89 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -import StillImage from 'soapbox/components/still_image'; - -export default class AvatarComposite extends React.PureComponent { - - static propTypes = { - accounts: ImmutablePropTypes.list.isRequired, - size: PropTypes.number.isRequired, - }; - - renderItem(account, size, index) { - - let width = 50; - let height = 100; - let top = 'auto'; - let left = 'auto'; - let bottom = 'auto'; - let right = 'auto'; - - if (size === 1) { - width = 100; - } - - if (size === 4 || (size === 3 && index > 0)) { - height = 50; - } - - if (size === 2) { - if (index === 0) { - right = '2px'; - } else { - left = '2px'; - } - } else if (size === 3) { - if (index === 0) { - right = '2px'; - } else if (index > 0) { - left = '2px'; - } - - if (index === 1) { - bottom = '2px'; - } else if (index > 1) { - top = '2px'; - } - } else if (size === 4) { - if (index === 0 || index === 2) { - right = '2px'; - } - - if (index === 1 || index === 3) { - left = '2px'; - } - - if (index < 2) { - bottom = '2px'; - } else { - top = '2px'; - } - } - - const style = { - left: left, - top: top, - right: right, - bottom: bottom, - width: `${width}%`, - height: `${height}%`, - }; - - return ( - - ); - } - - render() { - const { accounts, size } = this.props; - - return ( -
- {accounts.take(4).map((account, i) => this.renderItem(account, accounts.size, i))} -
- ); - } - -} diff --git a/app/soapbox/components/error_boundary.tsx b/app/soapbox/components/error_boundary.tsx index 436b40134..76adf9728 100644 --- a/app/soapbox/components/error_boundary.tsx +++ b/app/soapbox/components/error_boundary.tsx @@ -8,6 +8,7 @@ import { Text, Stack } from 'soapbox/components/ui'; import { captureException } from 'soapbox/monitoring'; import KVStore from 'soapbox/storage/kv_store'; import sourceCode from 'soapbox/utils/code'; +import { unregisterSw } from 'soapbox/utils/sw'; import SiteLogo from './site-logo'; @@ -15,16 +16,6 @@ import type { RootState } from 'soapbox/store'; const goHome = () => location.href = '/'; -/** Unregister the ServiceWorker */ -// https://stackoverflow.com/a/49771828/8811886 -const unregisterSw = async(): Promise => { - if (navigator.serviceWorker) { - const registrations = await navigator.serviceWorker.getRegistrations(); - const unregisterAll = registrations.map(r => r.unregister()); - await Promise.all(unregisterAll); - } -}; - const mapStateToProps = (state: RootState) => { const { links, logo } = getSoapboxConfig(state); diff --git a/app/soapbox/components/filter_bar.js b/app/soapbox/components/filter_bar.js deleted file mode 100644 index 2d6b71d9d..000000000 --- a/app/soapbox/components/filter_bar.js +++ /dev/null @@ -1,156 +0,0 @@ -import classNames from 'clsx'; -import debounce from 'lodash/debounce'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { withRouter } from 'react-router-dom'; - -class FilterBar extends React.PureComponent { - - static propTypes = { - items: PropTypes.array.isRequired, - active: PropTypes.string, - className: PropTypes.string, - history: PropTypes.object, - }; - - state = { - mounted: false, - }; - - componentDidMount() { - this.node.addEventListener('keydown', this.handleKeyDown, false); - window.addEventListener('resize', this.handleResize, { passive: true }); - - const { left, width } = this.getActiveTabIndicationSize(); - this.setState({ mounted: true, left, width }); - } - - componentWillUnmount() { - this.node.removeEventListener('keydown', this.handleKeyDown, false); - document.removeEventListener('resize', this.handleResize, false); - } - - handleResize = debounce(() => { - this.setState(this.getActiveTabIndicationSize()); - }, 300, { - trailing: true, - }); - - componentDidUpdate(prevProps) { - if (this.props.active !== prevProps.active) { - this.setState(this.getActiveTabIndicationSize()); - } - } - - setRef = c => { - this.node = c; - } - - setFocusRef = c => { - this.focusedItem = c; - } - - handleKeyDown = e => { - const items = Array.from(this.node.getElementsByTagName('a')); - const index = items.indexOf(document.activeElement); - let element = null; - - switch (e.key) { - case 'ArrowRight': - element = items[index + 1] || items[0]; - break; - case 'ArrowLeft': - element = items[index - 1] || items[items.length - 1]; - break; - } - - if (element) { - element.focus(); - e.preventDefault(); - e.stopPropagation(); - } - } - - handleItemKeyPress = e => { - if (e.key === 'Enter' || e.key === ' ') { - this.handleClick(e); - } - } - - handleClick = e => { - const i = Number(e.currentTarget.getAttribute('data-index')); - const { action, to } = this.props.items[i]; - - if (typeof action === 'function') { - e.preventDefault(); - action(e); - } else if (to) { - e.preventDefault(); - this.props.history.push(to); - } - } - - getActiveTabIndicationSize() { - const { active, items } = this.props; - - if (!active || !this.node) return { width: null }; - - const index = items.findIndex(({ name }) => name === active); - const elements = Array.from(this.node.getElementsByTagName('a')); - const element = elements[index]; - - if (!element) return { width: null }; - - const left = element.offsetLeft; - const { width } = element.getBoundingClientRect(); - - return { left, width }; - } - - renderActiveTabIndicator() { - const { left, width } = this.state; - - return ( -
- ); - } - - renderItem(option, i) { - if (option === null) { - return
  • ; - } - - const { name, text, href, to, title } = option; - - return ( - - {text} - - ); - } - - render() { - const { className, items } = this.props; - const { mounted } = this.state; - - return ( -
    - {mounted && this.renderActiveTabIndicator()} - {items.map((option, i) => this.renderItem(option, i))} -
    - ); - } - -} - -export default withRouter(FilterBar); \ No newline at end of file diff --git a/app/soapbox/components/quoted-status.tsx b/app/soapbox/components/quoted-status.tsx index d7ecfaf05..9755e7120 100644 --- a/app/soapbox/components/quoted-status.tsx +++ b/app/soapbox/components/quoted-status.tsx @@ -1,15 +1,18 @@ import classNames from 'clsx'; -import React, { useState } from 'react'; -import { defineMessages, useIntl, FormattedMessage, FormattedList } from 'react-intl'; +import React, { MouseEventHandler, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; import StatusMedia from 'soapbox/components/status-media'; -import { Stack, Text } from 'soapbox/components/ui'; +import { Stack } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account_container'; import { useSettings } from 'soapbox/hooks'; import { defaultMediaVisibility } from 'soapbox/utils/status'; import OutlineBox from './outline-box'; +import StatusReplyMentions from './status-reply-mentions'; +import StatusContent from './status_content'; +import SensitiveContentOverlay from './statuses/sensitive-content-overlay'; import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities'; @@ -36,7 +39,7 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => const [showMedia, setShowMedia] = useState(defaultMediaVisibility(status, displayMedia)); - const handleExpandClick = (e: React.MouseEvent) => { + const handleExpandClick: MouseEventHandler = (e) => { if (!status) return; const account = status.account as AccountEntity; @@ -57,57 +60,6 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => setShowMedia(!showMedia); }; - const renderReplyMentions = () => { - if (!status?.in_reply_to_id) { - return null; - } - - const account = status.account as AccountEntity; - const to = status.mentions || []; - - if (to.size === 0) { - if (status.in_reply_to_account_id === account.id) { - return ( -
    - -
    - ); - } else { - return ( -
    - -
    - ); - } - } - - const accounts = to.slice(0, 2).map(account => <>@{account.username}).toArray(); - - if (to.size > 2) { - accounts.push( - , - ); - } - - return ( -
    - , - }} - /> -
    - ); - }; - if (!status) { return null; } @@ -127,7 +79,7 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => return ( @@ -144,20 +96,36 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => withLinkToProfile={!compose} /> - {renderReplyMentions()} + - + + {(status.hidden) && ( + + )} - + + + + {(status.media_attachments.size > 0) && ( + + )} + + ); diff --git a/app/soapbox/components/setting_text.js b/app/soapbox/components/setting_text.js deleted file mode 100644 index e4972f102..000000000 --- a/app/soapbox/components/setting_text.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -export default class SettingText extends React.PureComponent { - - static propTypes = { - settings: ImmutablePropTypes.map.isRequired, - settingKey: PropTypes.array.isRequired, - label: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - }; - - handleChange = (e) => { - this.props.onChange(this.props.settingKey, e.target.value); - } - - render() { - const { settings, settingKey, label } = this.props; - - return ( - - ); - } - -} diff --git a/app/soapbox/components/status-content.css b/app/soapbox/components/status-content.css index 7c1317d16..997df73f4 100644 --- a/app/soapbox/components/status-content.css +++ b/app/soapbox/components/status-content.css @@ -1,9 +1,9 @@ .status-content p { - @apply mb-5 whitespace-pre-wrap; + @apply mb-4 whitespace-pre-wrap; } .status-content p:last-child { - @apply mb-0.5; + @apply mb-0; } .status-content a { @@ -20,7 +20,7 @@ .status-content ul, .status-content ol { - @apply pl-10 mb-5; + @apply pl-10 mb-4; } .status-content ul { @@ -32,7 +32,7 @@ } .status-content blockquote { - @apply py-1 pl-4 mb-5 border-l-4 border-solid border-gray-400 text-gray-500 dark:text-gray-400; + @apply py-1 pl-4 mb-4 border-l-4 border-solid border-gray-400 text-gray-500 dark:text-gray-400; } .status-content code { @@ -51,7 +51,7 @@ /* Code block */ .status-content pre { - @apply py-2 px-3 mb-5 leading-6 overflow-x-auto rounded-md break-all; + @apply py-2 px-3 mb-4 leading-6 overflow-x-auto rounded-md break-all; } .status-content pre:last-child { @@ -60,7 +60,7 @@ /* Markdown images */ .status-content img:not(.emojione):not([width][height]) { - @apply w-full h-72 object-contain rounded-lg overflow-hidden my-5 block; + @apply w-full h-72 object-contain rounded-lg overflow-hidden my-4 block; } /* User setting to underline links */ diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 3e30a3032..02eca62fe 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -65,7 +65,6 @@ const Status: React.FC = (props) => { hidden, featured, unread, - group, hideActionBar, variant = 'rounded', withDismiss, @@ -296,8 +295,8 @@ const Status: React.FC = (props) => { const accountAction = props.accountAction || reblogElement; - const inReview = status.visibility === 'self'; - const isSensitive = status.hidden; + const inReview = actualStatus.visibility === 'self'; + const isSensitive = actualStatus.hidden; return ( @@ -349,6 +348,8 @@ const Status: React.FC = (props) => {
  • + + = (props) => { }) } > - {(inReview || isSensitive) ? ( + {(inReview || isSensitive) && ( - ) : null} - - {!group && actualStatus.group && ( -
    - Posted in {String(actualStatus.getIn(['group', 'title']))} -
    )} - + + - + {(quote || actualStatus.media_attachments.size > 0) && ( + + - - - {quote} + {quote} + + )} +
    {!hideActionBar && ( diff --git a/app/soapbox/components/sub_navigation.tsx b/app/soapbox/components/sub_navigation.tsx index b8e2b310d..1e6afb85a 100644 --- a/app/soapbox/components/sub_navigation.tsx +++ b/app/soapbox/components/sub_navigation.tsx @@ -4,34 +4,22 @@ import { defineMessages, useIntl } from 'react-intl'; // import { connect } from 'react-redux'; import { useHistory } from 'react-router-dom'; -// import { openModal } from 'soapbox/actions/modals'; -// import { useAppDispatch } from 'soapbox/hooks'; - import { CardHeader, CardTitle } from './ui'; const messages = defineMessages({ back: { id: 'column_back_button.label', defaultMessage: 'Back' }, - settings: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, }); interface ISubNavigation { - message: String, + message: React.ReactNode, + /** @deprecated Unused. */ settings?: React.ComponentType, } const SubNavigation: React.FC = ({ message }) => { const intl = useIntl(); - // const dispatch = useAppDispatch(); const history = useHistory(); - // const ref = useRef(null); - - // const [scrolled, setScrolled] = useState(false); - - // const onOpenSettings = () => { - // dispatch(openModal('COMPONENT', { component: Settings })); - // }; - const handleBackClick = () => { if (window.history && window.history.length === 1) { history.push('/'); @@ -40,36 +28,6 @@ const SubNavigation: React.FC = ({ message }) => { } }; - // const handleBackKeyUp = (e) => { - // if (e.key === 'Enter') { - // handleClick(); - // } - // } - - // const handleOpenSettings = () => { - // onOpenSettings(); - // } - - // useEffect(() => { - // const handleScroll = throttle(() => { - // if (this.node) { - // const { offsetTop } = this.node; - - // if (offsetTop > 0) { - // setScrolled(true); - // } else { - // setScrolled(false); - // } - // } - // }, 150, { trailing: true }); - - // window.addEventListener('scroll', handleScroll); - - // return () => { - // window.removeEventListener('scroll', handleScroll); - // }; - // }, []); - return ( -
    -

    - -

    -
    - -
    -
    - -
    -
    - } /> -
    - -
    - } /> -
    -
    -
    - ); - } - -} - -export default injectIntl(ColumnSettings); \ No newline at end of file diff --git a/app/soapbox/features/community_timeline/containers/column_settings_container.js b/app/soapbox/features/community_timeline/containers/column_settings_container.js deleted file mode 100644 index d20838089..000000000 --- a/app/soapbox/features/community_timeline/containers/column_settings_container.js +++ /dev/null @@ -1,18 +0,0 @@ -import { connect } from 'react-redux'; - -import { getSettings, changeSetting } from '../../../actions/settings'; -import ColumnSettings from '../components/column_settings'; - -const mapStateToProps = state => ({ - settings: getSettings(state).get('community'), -}); - -const mapDispatchToProps = (dispatch) => { - return { - onChange(key, checked) { - dispatch(changeSetting(['community', ...key], checked)); - }, - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/soapbox/features/community_timeline/index.tsx b/app/soapbox/features/community_timeline/index.tsx index 8c0adc2cb..4fd4e1d0a 100644 --- a/app/soapbox/features/community_timeline/index.tsx +++ b/app/soapbox/features/community_timeline/index.tsx @@ -10,8 +10,6 @@ import { useAppDispatch, useSettings } from 'soapbox/hooks'; import Timeline from '../ui/components/timeline'; -import ColumnSettings from './containers/column_settings_container'; - const messages = defineMessages({ title: { id: 'column.community', defaultMessage: 'Local timeline' }, }); @@ -44,7 +42,10 @@ const CommunityTimeline = () => { return ( - +
    + +
    + = ({ state = 'inactive', size = 'sm' }) => { + return ( +
    + ); +}; + +export default Indicator; \ No newline at end of file diff --git a/app/soapbox/features/developers/developers-menu.tsx b/app/soapbox/features/developers/developers-menu.tsx index 76320e530..a94eb09a6 100644 --- a/app/soapbox/features/developers/developers-menu.tsx +++ b/app/soapbox/features/developers/developers-menu.tsx @@ -89,6 +89,14 @@ const Developers: React.FC = () => { + + + + + + + + diff --git a/app/soapbox/features/developers/service-worker-info.tsx b/app/soapbox/features/developers/service-worker-info.tsx new file mode 100644 index 000000000..9b7751587 --- /dev/null +++ b/app/soapbox/features/developers/service-worker-info.tsx @@ -0,0 +1,140 @@ +import React, { useEffect, useState } from 'react'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import List, { ListItem } from 'soapbox/components/list'; +import { HStack, Text, Column, FormActions, Button, Stack, Icon } from 'soapbox/components/ui'; +import { unregisterSw } from 'soapbox/utils/sw'; + +import Indicator from './components/indicator'; + +const messages = defineMessages({ + heading: { id: 'column.developers.service_worker', defaultMessage: 'Service Worker' }, + status: { id: 'sw.status', defaultMessage: 'Status' }, + url: { id: 'sw.url', defaultMessage: 'Script URL' }, +}); + +/** Hook that returns the active ServiceWorker registration. */ +const useRegistration = () => { + const [isLoading, setLoading] = useState(true); + const [registration, setRegistration] = useState(); + + const isSupported = 'serviceWorker' in navigator; + + useEffect(() => { + if (isSupported) { + navigator.serviceWorker.getRegistration() + .then(r => { + setRegistration(r); + setLoading(false); + }) + .catch(() => setLoading(false)); + } else { + setLoading(false); + } + }, []); + + return { + isLoading, + registration, + }; +}; + +interface IServiceWorkerInfo { +} + +/** Mini ServiceWorker debugging component. */ +const ServiceWorkerInfo: React.FC = () => { + const intl = useIntl(); + const { isLoading, registration } = useRegistration(); + + const url = registration?.active?.scriptURL; + + const getState = () => { + if (registration?.waiting) { + return 'pending'; + } else if (registration?.active) { + return 'active'; + } else { + return 'inactive'; + } + }; + + const getMessage = () => { + if (isLoading) { + return ( + + ); + } else if (!isLoading && !registration) { + return ( + + ); + } else if (registration?.waiting) { + return ( + + ); + } else if (registration?.active) { + return ( + + ); + } else { + return ( + + ); + } + }; + + const handleRestart = async() => { + await unregisterSw(); + window.location.reload(); + }; + + return ( + + + + + + + {getMessage()} + + + + {url && ( + + + {url} + + + + )} + + + + + + + + ); +}; + +export default ServiceWorkerInfo; \ No newline at end of file diff --git a/app/soapbox/features/hashtag-timeline/index.tsx b/app/soapbox/features/hashtag-timeline/index.tsx index a8b97cbab..7ead1a9f5 100644 --- a/app/soapbox/features/hashtag-timeline/index.tsx +++ b/app/soapbox/features/hashtag-timeline/index.tsx @@ -3,10 +3,10 @@ import { FormattedMessage } from 'react-intl'; import { connectHashtagStream } from 'soapbox/actions/streaming'; import { expandHashtagTimeline, clearTimeline } from 'soapbox/actions/timelines'; -import ColumnHeader from 'soapbox/components/column_header'; +import SubNavigation from 'soapbox/components/sub_navigation'; import { Column } from 'soapbox/components/ui'; import Timeline from 'soapbox/features/ui/components/timeline'; -import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { useAppDispatch } from 'soapbox/hooks'; import type { Tag as TagEntity } from 'soapbox/types/entities'; @@ -27,7 +27,6 @@ export const HashtagTimeline: React.FC = ({ params }) => { const tags = params?.tags || { any: [], all: [], none: [] }; const dispatch = useAppDispatch(); - const hasUnread = useAppSelector(state => (state.timelines.getIn([`hashtag:${id}`, 'unread']) as number) > 0); const disconnects = useRef<(() => void)[]>([]); // Mastodon supports displaying results from multiple hashtags. @@ -100,7 +99,10 @@ export const HashtagTimeline: React.FC = ({ params }) => { return ( - +
    + +
    + -
    -

    - -

    -
    - -
    -
    - -
    -
    - } /> -
    - -
    - } /> -
    - -
    - } /> -
    -
    -
    - ); - } - -} - -export default injectIntl(ColumnSettings); \ No newline at end of file diff --git a/app/soapbox/features/home_timeline/containers/column_settings_container.js b/app/soapbox/features/home_timeline/containers/column_settings_container.js deleted file mode 100644 index 0bcbafae8..000000000 --- a/app/soapbox/features/home_timeline/containers/column_settings_container.js +++ /dev/null @@ -1,26 +0,0 @@ -import { connect } from 'react-redux'; - -import { - getSettings, - changeSetting, - saveSettings, -} from '../../../actions/settings'; -import ColumnSettings from '../components/column_settings'; - -const mapStateToProps = state => ({ - settings: getSettings(state).get('home'), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange(key, checked) { - dispatch(changeSetting(['home', ...key], checked)); - }, - - onSave() { - dispatch(saveSettings()); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/soapbox/features/notifications/components/column_settings.js b/app/soapbox/features/notifications/components/column_settings.js deleted file mode 100644 index 9656e67fc..000000000 --- a/app/soapbox/features/notifications/components/column_settings.js +++ /dev/null @@ -1,193 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; - -import IconButton from 'soapbox/components/icon_button'; - -import ClearColumnButton from './clear_column_button'; -import MultiSettingToggle from './multi_setting_toggle'; -import SettingToggle from './setting_toggle'; - -const messages = defineMessages({ - close: { id: 'lightbox.close', defaultMessage: 'Close' }, -}); - -class ColumnSettings extends React.PureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - settings: ImmutablePropTypes.map.isRequired, - pushSettings: ImmutablePropTypes.map.isRequired, - onChange: PropTypes.func.isRequired, - onClear: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - supportsEmojiReacts: PropTypes.bool, - supportsBirthdays: PropTypes.bool, - }; - - onPushChange = (path, checked) => { - this.props.onChange(['push', ...path], checked); - } - - onAllSoundsChange = (path, checked) => { - const soundSettings = [['sounds', 'follow'], ['sounds', 'favourite'], ['sounds', 'pleroma:emoji_reaction'], ['sounds', 'mention'], ['sounds', 'reblog'], ['sounds', 'poll'], ['sounds', 'move']]; - - for (let i = 0; i < soundSettings.length; i++) { - this.props.onChange(soundSettings[i], checked); - } - } - - render() { - const { intl, settings, pushSettings, onChange, onClear, onClose, supportsEmojiReacts, supportsBirthdays } = this.props; - - const filterShowStr = ; - const filterAdvancedStr = ; - const alertStr = ; - const allSoundsStr = ; - const showStr = ; - const soundStr = ; - const soundSettings = [['sounds', 'follow'], ['sounds', 'favourite'], ['sounds', 'pleroma:emoji_reaction'], ['sounds', 'mention'], ['sounds', 'reblog'], ['sounds', 'poll'], ['sounds', 'move']]; - const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed'); - const pushStr = showPushSettings && ; - const birthdaysStr = ; - - return ( -
    -
    -

    - -

    -
    - -
    -
    - -
    -
    - -
    - -
    - - - - -
    - -
    - - - -
    - - -
    -
    - - {supportsBirthdays && -
    - - - -
    - -
    -
    - } - -
    - - -
    - - {showPushSettings && } - - -
    -
    - -
    - - -
    - - {showPushSettings && } - - -
    -
    - -
    - - -
    - - {showPushSettings && } - - -
    -
    - - {supportsEmojiReacts &&
    - - -
    - - {showPushSettings && } - - -
    -
    } - -
    - - -
    - - {showPushSettings && } - - -
    -
    - -
    - - -
    - - {showPushSettings && } - - -
    -
    - -
    - - -
    - - {showPushSettings && } - - -
    -
    - -
    - - -
    - - {showPushSettings && } - - -
    -
    -
    -
    - ); - } - -} - -export default injectIntl(ColumnSettings); \ No newline at end of file diff --git a/app/soapbox/features/notifications/components/follow_request.js b/app/soapbox/features/notifications/components/follow_request.js deleted file mode 100644 index 6fa90b648..000000000 --- a/app/soapbox/features/notifications/components/follow_request.js +++ /dev/null @@ -1,61 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Fragment } from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl } from 'react-intl'; - -import Avatar from 'soapbox/components/avatar'; -import DisplayName from 'soapbox/components/display-name'; -import IconButton from 'soapbox/components/icon_button'; -import Permalink from 'soapbox/components/permalink'; - -const messages = defineMessages({ - authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' }, - reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }, -}); - -class FollowRequest extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.record.isRequired, - onAuthorize: PropTypes.func.isRequired, - onReject: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - render() { - const { intl, hidden, account, onAuthorize, onReject } = this.props; - - if (!account) { - return
    ; - } - - if (hidden) { - return ( - - {account.get('display_name')} - {account.get('username')} - - ); - } - - return ( -
    -
    - -
    - -
    - -
    - - -
    -
    -
    - ); - } - -} - -export default injectIntl(FollowRequest); \ No newline at end of file diff --git a/app/soapbox/features/notifications/components/multi_setting_toggle.js b/app/soapbox/features/notifications/components/multi_setting_toggle.js deleted file mode 100644 index 68c382872..000000000 --- a/app/soapbox/features/notifications/components/multi_setting_toggle.js +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Toggle from 'react-toggle'; - -export default class MultiSettingToggle extends React.PureComponent { - - static propTypes = { - prefix: PropTypes.string, - settings: ImmutablePropTypes.map.isRequired, - settingPaths: PropTypes.array.isRequired, - label: PropTypes.node, - onChange: PropTypes.func.isRequired, - ariaLabel: PropTypes.string, - } - - onChange = ({ target }) => { - for (let i = 0; i < this.props.settingPaths.length; i++) { - this.props.onChange(this.props.settingPaths[i], target.checked); - } - } - - areTrue = (settingPath) => { - return this.props.settings.getIn(settingPath) === true; - } - - render() { - const { prefix, settingPaths, label, ariaLabel } = this.props; - const id = ['setting-toggle', prefix].filter(Boolean).join('-'); - - return ( -
    - - {label && ()} -
    - ); - } - -} diff --git a/app/soapbox/features/notifications/containers/column_settings_container.js b/app/soapbox/features/notifications/containers/column_settings_container.js deleted file mode 100644 index 6375e59f5..000000000 --- a/app/soapbox/features/notifications/containers/column_settings_container.js +++ /dev/null @@ -1,55 +0,0 @@ -import { defineMessages, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; - -import { openModal } from 'soapbox/actions/modals'; -import { clearNotifications, setFilter } from 'soapbox/actions/notifications'; -import { changeAlerts as changePushNotifications } from 'soapbox/actions/push_notifications'; -import { getSettings, changeSetting } from 'soapbox/actions/settings'; -import { getFeatures } from 'soapbox/utils/features'; - -import ColumnSettings from '../components/column_settings'; - -const messages = defineMessages({ - clearHeading: { id: 'notifications.clear_heading', defaultMessage: 'Clear notifications' }, - clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' }, - clearConfirm: { id: 'notifications.clear', defaultMessage: 'Clear notifications' }, -}); - -const mapStateToProps = state => { - const instance = state.get('instance'); - const features = getFeatures(instance); - - return { - settings: getSettings(state).get('notifications'), - pushSettings: state.get('push_notifications'), - supportsEmojiReacts: features.emojiReacts, - supportsBirthdays: features.birthdays, - }; -}; - -const mapDispatchToProps = (dispatch, { intl }) => ({ - - onChange(path, checked) { - if (path[0] === 'push') { - dispatch(changePushNotifications(path.slice(1), checked)); - } else if (path[0] === 'quickFilter') { - dispatch(changeSetting(['notifications', ...path], checked)); - dispatch(setFilter('all')); - } else { - dispatch(changeSetting(['notifications', ...path], checked)); - } - }, - - onClear() { - dispatch(openModal('CONFIRM', { - icon: require('@tabler/icons/eraser.svg'), - heading: intl.formatMessage(messages.clearHeading), - message: intl.formatMessage(messages.clearMessage), - confirm: intl.formatMessage(messages.clearConfirm), - onConfirm: () => dispatch(clearNotifications()), - })); - }, - -}); - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings)); diff --git a/app/soapbox/features/notifications/containers/follow_request_container.js b/app/soapbox/features/notifications/containers/follow_request_container.js deleted file mode 100644 index c793ac3de..000000000 --- a/app/soapbox/features/notifications/containers/follow_request_container.js +++ /dev/null @@ -1,28 +0,0 @@ -import { connect } from 'react-redux'; - -import { authorizeFollowRequest, rejectFollowRequest } from 'soapbox/actions/accounts'; -import { makeGetAccount } from 'soapbox/selectors'; - -import FollowRequest from '../components/follow_request'; - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, props) => ({ - account: getAccount(state, props.id), - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch, { id }) => ({ - onAuthorize() { - dispatch(authorizeFollowRequest(id)); - }, - - onReject() { - dispatch(rejectFollowRequest(id)); - }, -}); - -export default connect(makeMapStateToProps, mapDispatchToProps)(FollowRequest); diff --git a/app/soapbox/features/public_timeline/components/column_settings.js b/app/soapbox/features/public_timeline/components/column_settings.js deleted file mode 100644 index b9f2de6ae..000000000 --- a/app/soapbox/features/public_timeline/components/column_settings.js +++ /dev/null @@ -1,56 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; - -import IconButton from 'soapbox/components/icon_button'; - -import SettingToggle from '../../notifications/components/setting_toggle'; - -const messages = defineMessages({ - close: { id: 'lightbox.close', defaultMessage: 'Close' }, -}); - -class ColumnSettings extends React.PureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - settings: ImmutablePropTypes.map.isRequired, - onChange: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - }; - - render() { - const { intl, settings, onChange, onClose } = this.props; - - return ( -
    -
    -

    - -

    -
    - -
    -
    - -
    -
    - } /> -
    - -
    - } /> -
    - -
    - } /> -
    -
    -
    - ); - } - -} - -export default injectIntl(ColumnSettings); \ No newline at end of file diff --git a/app/soapbox/features/public_timeline/containers/column_settings_container.js b/app/soapbox/features/public_timeline/containers/column_settings_container.js deleted file mode 100644 index 63a629007..000000000 --- a/app/soapbox/features/public_timeline/containers/column_settings_container.js +++ /dev/null @@ -1,18 +0,0 @@ -import { connect } from 'react-redux'; - -import { getSettings, changeSetting } from '../../../actions/settings'; -import ColumnSettings from '../components/column_settings'; - -const mapStateToProps = state => ({ - settings: getSettings(state).get('public'), -}); - -const mapDispatchToProps = (dispatch) => { - return { - onChange(key, checked) { - dispatch(changeSetting(['public', ...key], checked)); - }, - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/soapbox/features/public_timeline/index.tsx b/app/soapbox/features/public_timeline/index.tsx index 6fe5afec8..34017a0d3 100644 --- a/app/soapbox/features/public_timeline/index.tsx +++ b/app/soapbox/features/public_timeline/index.tsx @@ -14,8 +14,6 @@ import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks'; import PinnedHostsPicker from '../remote_timeline/components/pinned_hosts_picker'; import Timeline from '../ui/components/timeline'; -import ColumnSettings from './containers/column_settings_container'; - const messages = defineMessages({ title: { id: 'column.public', defaultMessage: 'Fediverse timeline' }, dismiss: { id: 'fediverse_tab.explanation_box.dismiss', defaultMessage: 'Don\'t show again' }, @@ -65,8 +63,12 @@ const CommunityTimeline = () => { return ( - +
    + +
    + + {showExplanationBox &&
    } diff --git a/app/soapbox/features/status/components/detailed-status.tsx b/app/soapbox/features/status/components/detailed-status.tsx index 4896d1a91..28c2d020c 100644 --- a/app/soapbox/features/status/components/detailed-status.tsx +++ b/app/soapbox/features/status/components/detailed-status.tsx @@ -29,7 +29,6 @@ interface IDetailedStatus { const DetailedStatus: React.FC = ({ status, - onToggleHidden, onOpenCompareHistoryModal, onToggleMediaVisibility, showMedia, @@ -93,23 +92,29 @@ const DetailedStatus: React.FC = ({ }) } > - {(isUnderReview || isSensitive) ? ( + {(isUnderReview || isSensitive) && ( - ) : null} + )} - + + - + {(quote || actualStatus.media_attachments.size > 0) && ( + + - {quote} + {quote} + + )} + diff --git a/app/soapbox/features/ui/components/account_list_panel.js b/app/soapbox/features/ui/components/account_list_panel.js deleted file mode 100644 index 61b703e4f..000000000 --- a/app/soapbox/features/ui/components/account_list_panel.js +++ /dev/null @@ -1,57 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { Link } from 'react-router-dom'; - -import Icon from 'soapbox/components/icon'; -import AccountContainer from 'soapbox/containers/account_container'; - -export default class AccountListPanel extends ImmutablePureComponent { - - static propTypes = { - title: PropTypes.node.isRequired, - accountIds: ImmutablePropTypes.orderedSet.isRequired, - icon: PropTypes.string.isRequired, - limit: PropTypes.number, - total: PropTypes.number, - expandMessage: PropTypes.string, - expandRoute: PropTypes.string, - }; - - static defaultProps = { - limit: Infinity, - } - - render() { - const { title, icon, accountIds, limit, total, expandMessage, expandRoute, ...props } = this.props; - - if (!accountIds || accountIds.isEmpty()) { - return null; - } - - const canExpand = expandMessage && expandRoute && (accountIds.size < total); - - return ( -
    -
    - - - {title} - -
    -
    -
    - {accountIds.take(limit).map(accountId => ( - - ))} -
    -
    - {canExpand && - {expandMessage} - } -
    - ); - } - -} diff --git a/app/soapbox/features/ui/containers/notifications_container.tsx b/app/soapbox/features/ui/containers/notifications_container.tsx index 79d8724c1..2119228bf 100644 --- a/app/soapbox/features/ui/containers/notifications_container.tsx +++ b/app/soapbox/features/ui/containers/notifications_container.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { useIntl, MessageDescriptor } from 'react-intl'; +import { NotificationStack, NotificationObject, StyleFactoryFn } from 'react-notification'; import { useHistory } from 'react-router-dom'; import { dismissAlert } from 'soapbox/actions/alerts'; import { Button } from 'soapbox/components/ui'; import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; -import { NotificationStack, NotificationObject, StyleFactoryFn } from 'soapbox/react-notification'; import type { Alert } from 'soapbox/reducers/alerts'; diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 08d96e920..973a2187c 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -112,6 +112,7 @@ import { TestTimeline, LogoutPage, AuthTokenList, + ServiceWorkerInfo, } from './util/async-components'; import { WrappedRoute } from './util/react_router_helpers'; @@ -311,6 +312,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => { + new Promise((_resolve, reject) => reject())} content={children} /> diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts index f416c859a..773eed795 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -470,6 +470,10 @@ export function TestTimeline() { return import(/* webpackChunkName: "features/test_timeline" */'../../test_timeline'); } +export function ServiceWorkerInfo() { + return import(/* webpackChunkName: "features/developers" */'../../developers/service-worker-info'); +} + export function DatePicker() { return import(/* webpackChunkName: "date_picker" */'../../birthdays/date_picker'); } diff --git a/app/soapbox/react-notification/defaultPropTypes.js b/app/soapbox/react-notification/defaultPropTypes.js deleted file mode 100644 index 1a3dd9d4e..000000000 --- a/app/soapbox/react-notification/defaultPropTypes.js +++ /dev/null @@ -1,31 +0,0 @@ -import PropTypes from 'prop-types'; - -export default { - message: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.element, - ]).isRequired, - action: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.string, - PropTypes.node, - ]), - onClick: PropTypes.func, - style: PropTypes.bool, - actionStyle: PropTypes.object, - titleStyle: PropTypes.object, - barStyle: PropTypes.object, - activeBarStyle: PropTypes.object, - dismissAfter: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.number, - ]), - onDismiss: PropTypes.func, - className: PropTypes.string, - activeClassName: PropTypes.string, - isActive: PropTypes.bool, - title: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.node, - ]), -}; diff --git a/app/soapbox/react-notification/index.d.ts b/app/soapbox/react-notification/index.d.ts deleted file mode 100644 index 22f0211c8..000000000 --- a/app/soapbox/react-notification/index.d.ts +++ /dev/null @@ -1,88 +0,0 @@ -declare module 'soapbox/react-notification' { - import { Component, ReactElement } from 'react'; - - interface StyleFactoryFn { - (index: number, style: object | void, notification: NotificationProps): object; - } - - interface OnClickNotificationProps { - /** - * Callback function to run when the action is clicked. - * @param notification Notification currently being clicked - * @param deactivate Function that can be called to set the notification to inactive. - * Used to activate notification exit animation on click. - */ - onClick?(notification: NotificationProps, deactivate: () => void): void; - } - - interface NotificationProps extends OnClickNotificationProps { - /** The name of the action, e.g., "close" or "undo". */ - action?: string; - /** Custom action styles. */ - actionStyle?: object; - /** Custom snackbar styles when the bar is active. */ - activeBarStyle?: object; - /** - * Custom class to apply to the top-level component when active. - * @default 'notification-bar-active' - */ - activeClassName?: string; - /** Custom snackbar styles. */ - barStyle?: object; - /** Custom class to apply to the top-level component. */ - className?: string; - /** - * Timeout for onDismiss event. - * @default 2000 - */ - dismissAfter?: boolean | number; - /** - * If true, the notification is visible. - * @default false - */ - isActive?: boolean; - /** The message or component for the notification. */ - message: string | ReactElement; - /** Setting this prop to `false` will disable all inline styles. */ - style?: boolean; - /** The title for the notification. */ - title?: string | ReactElement; - /** Custom title styles. */ - titleStyle?: object; - - /** - * Callback function to run when dismissAfter timer runs out - * @param notification Notification currently being dismissed. - */ - onDismiss?(notification: NotificationProps): void; - } - - interface NotificationStackProps extends OnClickNotificationProps { - /** Create the style of the actions. */ - actionStyleFactory?: StyleFactoryFn; - /** Create the style of the active notification. */ - activeBarStyleFactory?: StyleFactoryFn; - /** Create the style of the notification. */ - barStyleFactory?: StyleFactoryFn; - /** - * If false, notification dismiss timers start immediately. - * @default true - */ - dismissInOrder?: boolean; - /** Array of notifications to render. */ - notifications: NotificationObject[]; - /** - * Callback function to run when dismissAfter timer runs out - * @param notification Notification currently being dismissed. - */ - onDismiss?(notification: NotificationObject): void; - } - - export interface NotificationObject extends NotificationProps { - key: number | string; - } - - export class Notification extends Component {} - - export class NotificationStack extends Component {} -} diff --git a/app/soapbox/react-notification/index.js b/app/soapbox/react-notification/index.js deleted file mode 100644 index 3d7da7cee..000000000 --- a/app/soapbox/react-notification/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as Notification } from './notification'; -export { default as NotificationStack } from './notificationStack'; diff --git a/app/soapbox/react-notification/notification.js b/app/soapbox/react-notification/notification.js deleted file mode 100644 index ab1cddf9b..000000000 --- a/app/soapbox/react-notification/notification.js +++ /dev/null @@ -1,175 +0,0 @@ -/* linting temp disabled while working on updates */ -/* eslint-disable */ -import React, { Component } from 'react'; -import defaultPropTypes from './defaultPropTypes'; - -class Notification extends Component { - constructor(props) { - super(props); - - this.getBarStyle = this.getBarStyle.bind(this); - this.getActionStyle = this.getActionStyle.bind(this); - this.getTitleStyle = this.getTitleStyle.bind(this); - this.handleClick = this.handleClick.bind(this); - - if (props.onDismiss && props.isActive) { - this.dismissTimeout = setTimeout( - props.onDismiss, - props.dismissAfter - ); - } - } - - componentWillReceiveProps(nextProps) { - if (nextProps.dismissAfter === false) return; - - // See http://eslint.org/docs/rules/no-prototype-builtins - if (!{}.hasOwnProperty.call(nextProps, 'isLast')) { - clearTimeout(this.dismissTimeout); - } - - if (nextProps.onDismiss) { - if ( - (nextProps.isActive && !this.props.isActive) || - (nextProps.dismissAfter && this.props.dismissAfter === false) - ) { - this.dismissTimeout = setTimeout( - nextProps.onDismiss, - nextProps.dismissAfter - ); - } - } - } - - componentWillUnmount() { - if (this.props.dismissAfter) clearTimeout(this.dismissTimeout); - } - - /* - * @description Dynamically get the styles for the bar. - * @returns {object} result The style. - */ - getBarStyle() { - if (this.props.style === false) return {}; - - const { isActive, barStyle, activeBarStyle } = this.props; - - const baseStyle = { - position: 'fixed', - bottom: '2rem', - left: '-100%', - width: 'auto', - padding: '1rem', - margin: 0, - color: '#fafafa', - font: '1rem normal Roboto, sans-serif', - borderRadius: '5px', - background: '#212121', - borderSizing: 'border-box', - boxShadow: '0 0 1px 1px rgba(10, 10, 11, .125)', - cursor: 'default', - WebKitTransition: '.5s cubic-bezier(0.89, 0.01, 0.5, 1.1)', - MozTransition: '.5s cubic-bezier(0.89, 0.01, 0.5, 1.1)', - msTransition: '.5s cubic-bezier(0.89, 0.01, 0.5, 1.1)', - OTransition: '.5s cubic-bezier(0.89, 0.01, 0.5, 1.1)', - transition: '.5s cubic-bezier(0.89, 0.01, 0.5, 1.1)', - WebkitTransform: 'translatez(0)', - MozTransform: 'translatez(0)', - msTransform: 'translatez(0)', - OTransform: 'translatez(0)', - transform: 'translatez(0)' - }; - - return isActive ? - Object.assign({}, baseStyle, { left: '1rem' }, barStyle, activeBarStyle) : - Object.assign({}, baseStyle, barStyle); - } - - /* - * @function getActionStyle - * @description Dynamically get the styles for the action text. - * @returns {object} result The style. - */ - getActionStyle() { - return this.props.style !== false ? Object.assign({}, { - padding: '0.125rem', - marginLeft: '1rem', - color: '#f44336', - font: '.75rem normal Roboto, sans-serif', - lineHeight: '1rem', - letterSpacing: '.125ex', - textTransform: 'uppercase', - borderRadius: '5px', - cursor: 'pointer' - }, this.props.actionStyle) : {}; - } - - /* - * @function getTitleStyle - * @description Dynamically get the styles for the title. - * @returns {object} result The style. - */ - getTitleStyle() { - return this.props.style !== false ? Object.assign({}, { - fontWeight: '700', - marginRight: '.5rem' - }, this.props.titleStyle) : {}; - } - - /* - * @function handleClick - * @description Handle click events on the action button. - */ - handleClick() { - if (this.props.onClick && typeof this.props.onClick === 'function') { - return this.props.onClick(); - } - } - - render() { - let className = 'notification-bar'; - - if (this.props.isActive) className += ` ${this.props.activeClassName}`; - if (this.props.className) className += ` ${this.props.className}`; - - return ( -
    -
    - {this.props.title ? ( - - {this.props.title} - - ) : null} - - {/* eslint-disable */} - - {this.props.message} - - - {this.props.action ? ( - - {this.props.action} - - ) : null} -
    -
    - ); - } -} - -Notification.propTypes = defaultPropTypes; - -Notification.defaultProps = { - isActive: false, - dismissAfter: 2000, - activeClassName: 'notification-bar-active' -}; - -export default Notification; diff --git a/app/soapbox/react-notification/notificationStack.js b/app/soapbox/react-notification/notificationStack.js deleted file mode 100644 index dc9c2459b..000000000 --- a/app/soapbox/react-notification/notificationStack.js +++ /dev/null @@ -1,95 +0,0 @@ -/* linting temp disabled while working on updates */ -/* eslint-disable */ -import React from 'react'; -import PropTypes from 'prop-types'; -import StackedNotification from './stackedNotification'; -import defaultPropTypes from './defaultPropTypes'; - -function defaultBarStyleFactory(index, style) { - return Object.assign( - {}, - style, - { bottom: `${2 + (index * 4)}rem` } - ); -} - -function defaultActionStyleFactory(index, style) { - return Object.assign( - {}, - style, - {} - ); -} - -/** -* The notification list does not have any state, so use a -* pure function here. It just needs to return the stacked array -* of notification components. -*/ -const NotificationStack = props => ( -
    - {props.notifications.map((notification, index) => { - const isLast = index === 0 && props.notifications.length === 1; - const dismissNow = isLast || !props.dismissInOrder; - - // Handle styles - const barStyle = props.barStyleFactory(index, notification.barStyle, notification); - const actionStyle = props.actionStyleFactory(index, notification.actionStyle, notification); - const activeBarStyle = props.activeBarStyleFactory( - index, - notification.activeBarStyle, - notification - ); - - // Allow onClick from notification stack or individual notifications - const onClick = notification.onClick || props.onClick; - const onDismiss = props.onDismiss; - - let { dismissAfter } = notification; - - if (dismissAfter !== false) { - if (dismissAfter == null) dismissAfter = props.dismissAfter; - if (!dismissNow) dismissAfter += index * 1000; - } - - return ( - - ); - })} -
    -); - -/* eslint-disable react/no-unused-prop-types, react/forbid-prop-types */ -NotificationStack.propTypes = { - activeBarStyleFactory: PropTypes.func, - barStyleFactory: PropTypes.func, - actionStyleFactory: PropTypes.func, - dismissInOrder: PropTypes.bool, - notifications: PropTypes.array.isRequired, - onDismiss: PropTypes.func.isRequired, - onClick: PropTypes.func, - action: defaultPropTypes.action -}; - -NotificationStack.defaultProps = { - activeBarStyleFactory: defaultBarStyleFactory, - barStyleFactory: defaultBarStyleFactory, - actionStyleFactory: defaultActionStyleFactory, - dismissInOrder: true, - dismissAfter: 1000, - onClick: () => {} -}; -/* eslint-enable no-alert, no-console */ - -export default NotificationStack; diff --git a/app/soapbox/react-notification/stackedNotification.js b/app/soapbox/react-notification/stackedNotification.js deleted file mode 100644 index c8d7200d4..000000000 --- a/app/soapbox/react-notification/stackedNotification.js +++ /dev/null @@ -1,69 +0,0 @@ -/* linting temp disabled while working on updates */ -/* eslint-disable */ -import React, { Component } from 'react'; -import defaultPropTypes from './defaultPropTypes'; -import Notification from './notification'; - -class StackedNotification extends Component { - constructor(props) { - super(props); - - this.state = { - isActive: false - }; - - this.handleClick = this.handleClick.bind(this); - } - - componentDidMount() { - this.activeTimeout = setTimeout(this.setState.bind(this, { - isActive: true - }), 1); - - this.dismiss(this.props.dismissAfter); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.dismissAfter !== this.props.dismissAfter) { - this.dismiss(nextProps.dismissAfter); - } - } - - componentWillUnmount() { - clearTimeout(this.activeTimeout); - clearTimeout(this.dismissTimeout); - } - - dismiss(dismissAfter) { - if (dismissAfter === false) return; - - this.dismissTimeout = setTimeout(this.setState.bind(this, { - isActive: false - }), dismissAfter); - } - - /* - * @function handleClick - * @description Bind deactivate Notification function to Notification click handler - */ - handleClick() { - if (this.props.onClick && typeof this.props.onClick === 'function') { - return this.props.onClick(this.setState.bind(this, { isActive: false })); - } - } - - render() { - return ( - setTimeout(this.props.onDismiss, 300)} - isActive={this.state.isActive} - /> - ); - } -} - -StackedNotification.propTypes = defaultPropTypes; - -export default StackedNotification; diff --git a/app/soapbox/reducers/__tests__/group_editor.test.ts b/app/soapbox/reducers/__tests__/group_editor.test.ts deleted file mode 100644 index 516b6df43..000000000 --- a/app/soapbox/reducers/__tests__/group_editor.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; - -import reducer from '../group_editor'; - -describe('group_editor reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {} as any)).toEqual(ImmutableMap({ - groupId: null, - isSubmitting: false, - isChanged: false, - title: '', - description: '', - coverImage: null, - })); - }); -}); diff --git a/app/soapbox/reducers/__tests__/group_lists.test.ts b/app/soapbox/reducers/__tests__/group_lists.test.ts deleted file mode 100644 index 46527f682..000000000 --- a/app/soapbox/reducers/__tests__/group_lists.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; - -import reducer from '../group_lists'; - -describe('group_lists reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {} as any)).toEqual(ImmutableMap({ - featured: ImmutableList(), - member: ImmutableList(), - admin: ImmutableList(), - })); - }); -}); diff --git a/app/soapbox/reducers/__tests__/group_relationships.test.ts b/app/soapbox/reducers/__tests__/group_relationships.test.ts deleted file mode 100644 index 31e3e354f..000000000 --- a/app/soapbox/reducers/__tests__/group_relationships.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; - -import reducer from '../group_relationships'; - -describe('group_relationships reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {} as any)).toEqual(ImmutableMap()); - }); -}); diff --git a/app/soapbox/reducers/__tests__/groups.test.ts b/app/soapbox/reducers/__tests__/groups.test.ts deleted file mode 100644 index 05b88402f..000000000 --- a/app/soapbox/reducers/__tests__/groups.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; - -import reducer from '../groups'; - -describe('groups reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {} as any)).toEqual(ImmutableMap()); - }); -}); diff --git a/app/soapbox/reducers/__tests__/list_editor-test.js b/app/soapbox/reducers/__tests__/list_editor-test.js deleted file mode 100644 index 1b351bca1..000000000 --- a/app/soapbox/reducers/__tests__/list_editor-test.js +++ /dev/null @@ -1,153 +0,0 @@ -import { Map as ImmutableMap, List as ImmutableList, Record as ImmutableRecord } from 'immutable'; - -import * as actions from 'soapbox/actions/lists'; - -import reducer from '../list_editor'; - -describe('list_editor reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {})).toMatchObject({ - listId: null, - isSubmitting: false, - isChanged: false, - title: '', - - accounts: { - items: ImmutableList(), - loaded: false, - isLoading: false, - }, - - suggestions: { - value: '', - items: ImmutableList(), - }, - }); - }); - - it('should handle LIST_EDITOR_RESET', () => { - const state = ImmutableRecord({ - listId: null, - isSubmitting: false, - isChanged: false, - title: '', - - accounts: ImmutableRecord({ - items: ImmutableList(), - loaded: false, - isLoading: false, - })(), - - suggestions: ImmutableRecord({ - value: '', - items: ImmutableList(), - })(), - })(); - const action = { - type: actions.LIST_EDITOR_RESET, - }; - expect(reducer(state, action)).toMatchObject({ - listId: null, - isSubmitting: false, - isChanged: false, - title: '', - - accounts: { - items: ImmutableList(), - loaded: false, - isLoading: false, - }, - - suggestions: { - value: '', - items: ImmutableList(), - }, - }); - }); - - it('should handle LIST_EDITOR_SETUP', () => { - const state = ImmutableRecord({ - listId: null, - isSubmitting: false, - isChanged: false, - title: '', - - accounts: ImmutableRecord({ - items: ImmutableList(), - loaded: false, - isLoading: false, - })(), - - suggestions: ImmutableRecord({ - value: '', - items: ImmutableList(), - })(), - })(); - const action = { - type: actions.LIST_EDITOR_SETUP, - list: ImmutableMap({ - id: '22', - title: 'list 1', - }), - }; - expect(reducer(state, action)).toMatchObject({ - listId: '22', - isSubmitting: false, - isChanged: false, - title: 'list 1', - - accounts: { - items: ImmutableList(), - loaded: false, - isLoading: false, - }, - - suggestions: { - value: '', - items: ImmutableList(), - }, - }); - }); - - it('should handle LIST_EDITOR_TITLE_CHANGE', () => { - const state = ImmutableMap({ - title: 'list 1', - isChanged: false, - }); - const action = { - type: actions.LIST_EDITOR_TITLE_CHANGE, - value: 'list 1 edited', - }; - expect(reducer(state, action).toJS()).toMatchObject({ - isChanged: true, - title: 'list 1 edited', - }); - }); - - it('should handle LIST_UPDATE_REQUEST', () => { - const state = ImmutableMap({ - isSubmitting: false, - isChanged: true, - }); - const action = { - type: actions.LIST_UPDATE_REQUEST, - }; - expect(reducer(state, action).toJS()).toMatchObject({ - isSubmitting: true, - isChanged: false, - }); - }); - - it('should handle LIST_UPDATE_FAIL', () => { - const state = ImmutableMap({ - isSubmitting: true, - }); - const action = { - type: actions.LIST_UPDATE_FAIL, - }; - expect(reducer(state, action).toJS()).toMatchObject({ - isSubmitting: false, - }); - }); - -}); diff --git a/app/soapbox/reducers/__tests__/notifications-test.js b/app/soapbox/reducers/__tests__/notifications-test.js deleted file mode 100644 index 1ea296c60..000000000 --- a/app/soapbox/reducers/__tests__/notifications-test.js +++ /dev/null @@ -1,673 +0,0 @@ -import { - Map as ImmutableMap, - OrderedMap as ImmutableOrderedMap, - Record as ImmutableRecord, -} from 'immutable'; -import take from 'lodash/take'; - -import intlMessages from 'soapbox/__fixtures__/intlMessages.json'; -import notification from 'soapbox/__fixtures__/notification.json'; -import notifications from 'soapbox/__fixtures__/notifications.json'; -import relationship from 'soapbox/__fixtures__/relationship.json'; -import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'soapbox/actions/accounts'; -import { - MARKER_FETCH_SUCCESS, - MARKER_SAVE_REQUEST, - MARKER_SAVE_SUCCESS, -} from 'soapbox/actions/markers'; -import { - NOTIFICATIONS_EXPAND_SUCCESS, - NOTIFICATIONS_EXPAND_REQUEST, - NOTIFICATIONS_EXPAND_FAIL, - NOTIFICATIONS_FILTER_SET, - NOTIFICATIONS_SCROLL_TOP, - NOTIFICATIONS_UPDATE, - NOTIFICATIONS_UPDATE_QUEUE, - NOTIFICATIONS_DEQUEUE, - NOTIFICATIONS_CLEAR, - NOTIFICATIONS_MARK_READ_REQUEST, -} from 'soapbox/actions/notifications'; -import { TIMELINE_DELETE } from 'soapbox/actions/timelines'; -import { applyActions } from 'soapbox/jest/test-helpers'; - -import reducer from '../notifications'; - -const initialState = reducer(undefined, {}); - -describe('notifications reducer', () => { - it('should return the initial state', () => { - const expected = { - items: {}, - hasMore: true, - top: false, - unread: 0, - isLoading: false, - queuedNotifications: {}, - totalQueuedNotificationsCount: 0, - lastRead: -1, - }; - - expect(ImmutableRecord.isRecord(initialState)).toBe(true); - expect(initialState.toJS()).toMatchObject(expected); - }); - - describe('NOTIFICATIONS_EXPAND_SUCCESS', () => { - it('imports the notifications', () => { - const action = { - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications: take(notifications, 3), - next: null, - skipLoading: true, - }; - - const result = reducer(undefined, action); - - // The items are parsed as records - expect(ImmutableOrderedMap.isOrderedMap(result.items)).toBe(true); - expect(ImmutableRecord.isRecord(result.items.get('10743'))).toBe(true); - - // We can get an item - expect(result.items.get('10744').emoji).toEqual('😢'); - - // hasMore is set to false because `next` is null - expect(result.hasMore).toBe(false); - }); - - it('drops invalid notifications', () => { - const action = { - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications: [ - { id: '1', type: 'mention', status: null, account: { id: '10' } }, - { id: '2', type: 'reblog', status: null, account: { id: '9' } }, - { id: '3', type: 'favourite', status: null, account: { id: '8' } }, - { id: '4', type: 'mention', status: { id: 'a' }, account: { id: '7' } }, - { id: '5', type: 'reblog', status: { id: 'b' }, account: null }, - ], - next: null, - skipLoading: true, - }; - - const result = reducer(undefined, action); - - // Only '4' is valid - expect(result.items.size).toEqual(1); - expect(result.items.get('4').id).toEqual('4'); - }); - - it('imports move notification', () => { - const action = { - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications: [ - require('soapbox/__fixtures__/pleroma-notification-move.json'), - ], - next: null, - skipLoading: true, - }; - - const result = reducer(undefined, action).items.get('406814'); - - expect(result.account).toEqual('AFmHQ18XZ7Lco68MW8'); - expect(result.target).toEqual('A5c5LK7EJTFR0u26Pg'); - }); - }); - - describe('NOTIFICATIONS_EXPAND_REQUEST', () => { - it('sets isLoading to true', () => { - const state = initialState.set('isLoading', false); - const action = { type: NOTIFICATIONS_EXPAND_REQUEST }; - - expect(reducer(state, action).isLoading).toBe(true); - }); - }); - - describe('NOTIFICATIONS_EXPAND_FAIL', () => { - it('sets isLoading to false', () => { - const state = initialState.set('isLoading', true); - const action = { type: NOTIFICATIONS_EXPAND_FAIL }; - - expect(reducer(state, action).isLoading).toBe(false); - }); - }); - - describe('NOTIFICATIONS_FILTER_SET', () => { - it('clears the items', () => { - const actions = [{ - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications: [ - { id: '1', type: 'mention', status: { id: '4' }, account: { id: '7' } }, - { id: '2', type: 'mention', status: { id: '5' }, account: { id: '8' } }, - { id: '3', type: 'mention', status: { id: '6' }, account: { id: '9' } }, - ], - next: null, - skipLoading: true, - }, { - type: NOTIFICATIONS_FILTER_SET, - }]; - - // Setup by expanding, then calling `NOTIFICATIONS_FILTER_SET` - const result = applyActions(initialState, actions, reducer); - - // Setting the filter wipes notifications - expect(result.items.isEmpty()).toBe(true); - }); - - it('sets hasMore to true', () => { - const state = initialState.set('hasMore', false); - const action = { type: NOTIFICATIONS_FILTER_SET }; - const result = reducer(state, action); - - expect(result.hasMore).toBe(true); - }); - }); - - describe('NOTIFICATIONS_SCROLL_TOP', () => { - it('resets `unread` counter to 0 when top is true (ie, scrolled to the top)', () => { - const state = initialState.set('unread', 1); - const action = { type: NOTIFICATIONS_SCROLL_TOP, top: true }; - const result = reducer(state, action); - - expect(result.unread).toEqual(0); - expect(result.top).toBe(true); - }); - - it('leaves `unread` alone when top is false (ie, not scrolled to top)', () => { - const state = initialState.set('unread', 3); - const action = { type: NOTIFICATIONS_SCROLL_TOP, top: false }; - const result = reducer(state, action); - - expect(result.unread).toEqual(3); - expect(result.top).toBe(false); - }); - }); - - describe('NOTIFICATIONS_UPDATE', () => { - it('imports the notification', () => { - const action = { type: NOTIFICATIONS_UPDATE, notification }; - const result = reducer(initialState, action); - - expect(result.items.get('10743').type).toEqual('favourite'); - }); - - it('imports follow_request notification', () => { - const action = { - type: NOTIFICATIONS_UPDATE, - notification: require('soapbox/__fixtures__/notification-follow_request.json'), - }; - - const result = reducer(initialState, action); - expect(result.items.get('87967').type).toEqual('follow_request'); - }); - - it('increments `unread` counter when top is false', () => { - const action = { type: NOTIFICATIONS_UPDATE, notification }; - const result = reducer(initialState, action); - - expect(result.unread).toEqual(1); - }); - }); - - describe('NOTIFICATIONS_UPDATE_QUEUE', () => { - it('adds the notification to the queue (and increases the counter)', () => { - const action = { - type: NOTIFICATIONS_UPDATE_QUEUE, - notification, - intlMessages, - intlLocale: 'en', - }; - - const result = reducer(initialState, action); - - // Doesn't add it as a regular item - expect(result.items.isEmpty()).toBe(true); - - // Adds it to the queued items - expect(result.queuedNotifications.size).toEqual(1); - expect(result.totalQueuedNotificationsCount).toEqual(1); - expect(result.queuedNotifications.getIn(['10743', 'notification', 'type'])).toEqual('favourite'); - }); - }); - - describe('NOTIFICATIONS_DEQUEUE', () => { - it('resets the queued counter to 0', () => { - const state = initialState.set('totalQueuedNotificationsCount', 1); - const action = { type: NOTIFICATIONS_DEQUEUE }; - const result = reducer(state, action); - - expect(result.totalQueuedNotificationsCount).toEqual(0); - }); - }); - - describe('NOTIFICATIONS_EXPAND_SUCCESS', () => { - it('with non-empty items and next set true', () => { - const state = ImmutableMap({ - items: ImmutableOrderedMap([ - ['10734', ImmutableMap({ - id: '10734', - type: 'pleroma:emoji_reaction', - account: '9vMAje101ngtjlMj7w', - target: null, - created_at: '2020-06-10T02:54:39.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: '😢', - chat_message: null, - })], - ]), - unread: 1, - hasMore: true, - isLoading: false, - }); - - const action = { - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications: take(notifications, 3), - next: true, - }; - - const expected = ImmutableMap({ - items: ImmutableOrderedMap([ - ['10744', ImmutableMap({ - id: '10744', - type: 'pleroma:emoji_reaction', - account: '9vMAje101ngtjlMj7w', - target: null, - created_at: '2020-06-10T02:54:39.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: '😢', - chat_message: null, - total_count: null, - })], - ['10743', ImmutableMap({ - id: '10743', - type: 'favourite', - account: '9v5c6xSEgAi3Zu1Lv6', - target: null, - created_at: '2020-06-10T02:51:05.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - total_count: null, - })], - ['10741', ImmutableMap({ - id: '10741', - type: 'favourite', - account: '9v5cKMOPGqPcgfcWp6', - target: null, - created_at: '2020-06-10T02:05:06.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - total_count: null, - })], - ['10734', ImmutableMap({ - id: '10734', - type: 'pleroma:emoji_reaction', - account: '9vMAje101ngtjlMj7w', - target: null, - created_at: '2020-06-10T02:54:39.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: '😢', - chat_message: null, - })], - ]), - unread: 1, - hasMore: true, - isLoading: false, - }); - - expect(reducer(state, action).toJS()).toEqual(expected.toJS()); - }); - - it('with empty items and next set true', () => { - const state = ImmutableMap({ - items: ImmutableOrderedMap(), - unread: 1, - hasMore: true, - isLoading: false, - }); - - const action = { - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications: take(notifications, 3), - next: true, - }; - - const expected = ImmutableMap({ - items: ImmutableOrderedMap([ - ['10744', ImmutableMap({ - id: '10744', - type: 'pleroma:emoji_reaction', - account: '9vMAje101ngtjlMj7w', - target: null, - created_at: '2020-06-10T02:54:39.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: '😢', - chat_message: null, - total_count: null, - })], - ['10743', ImmutableMap({ - id: '10743', - type: 'favourite', - account: '9v5c6xSEgAi3Zu1Lv6', - target: null, - created_at: '2020-06-10T02:51:05.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - total_count: null, - })], - ['10741', ImmutableMap({ - id: '10741', - type: 'favourite', - account: '9v5cKMOPGqPcgfcWp6', - target: null, - created_at: '2020-06-10T02:05:06.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - total_count: null, - })], - ]), - unread: 1, - hasMore: true, - isLoading: false, - }); - - expect(reducer(state, action).toJS()).toEqual(expected.toJS()); - }); - }); - - describe('ACCOUNT_BLOCK_SUCCESS', () => { - it('should handle', () => { - const state = ImmutableMap({ - items: ImmutableOrderedMap([ - ['10744', ImmutableMap({ - id: '10744', - type: 'pleroma:emoji_reaction', - account: '9vMAje101ngtjlMj7w', - target: null, - created_at: '2020-06-10T02:54:39.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: '😢', - chat_message: null, - })], - ['10743', ImmutableMap({ - id: '10743', - type: 'favourite', - account: '9v5c6xSEgAi3Zu1Lv6', - target: null, - created_at: '2020-06-10T02:51:05.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - })], - ['10741', ImmutableMap({ - id: '10741', - type: 'favourite', - account: '9v5cKMOPGqPcgfcWp6', - target: null, - created_at: '2020-06-10T02:05:06.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - })], - ]), - }); - const action = { - type: ACCOUNT_BLOCK_SUCCESS, - relationship, - }; - expect(reducer(state, action)).toEqual(ImmutableMap({ - items: ImmutableOrderedMap([ - ['10743', ImmutableMap({ - id: '10743', - type: 'favourite', - account: '9v5c6xSEgAi3Zu1Lv6', - target: null, - created_at: '2020-06-10T02:51:05.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - })], - ['10741', ImmutableMap({ - id: '10741', - type: 'favourite', - account: '9v5cKMOPGqPcgfcWp6', - target: null, - created_at: '2020-06-10T02:05:06.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - })], - ]), - })); - }); - }); - - describe('ACCOUNT_MUTE_SUCCESS', () => { - it('should handle', () => { - const state = ImmutableMap({ - items: ImmutableOrderedMap([ - ['10744', ImmutableMap({ - id: '10744', - type: 'pleroma:emoji_reaction', - account: '9vMAje101ngtjlMj7w', - target: null, - created_at: '2020-06-10T02:54:39.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: '😢', - chat_message: null, - })], - ['10743', ImmutableMap({ - id: '10743', - type: 'favourite', - account: '9v5c6xSEgAi3Zu1Lv6', - target: null, - created_at: '2020-06-10T02:51:05.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - })], - ['10741', ImmutableMap({ - id: '10741', - type: 'favourite', - account: '9v5cKMOPGqPcgfcWp6', - target: null, - created_at: '2020-06-10T02:05:06.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - })], - ]), - }); - const action = { - type: ACCOUNT_MUTE_SUCCESS, - relationship: relationship, - }; - expect(reducer(state, action)).toEqual(ImmutableMap({ - items: ImmutableOrderedMap([ - ['10743', ImmutableMap({ - id: '10743', - type: 'favourite', - account: '9v5c6xSEgAi3Zu1Lv6', - target: null, - created_at: '2020-06-10T02:51:05.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - })], - ['10741', ImmutableMap({ - id: '10741', - type: 'favourite', - account: '9v5cKMOPGqPcgfcWp6', - target: null, - created_at: '2020-06-10T02:05:06.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - })], - ]), - })); - }); - }); - - describe('NOTIFICATIONS_CLEAR', () => { - it('clears the items', () => { - const state = initialState.set('items', ImmutableOrderedMap([['1', {}], ['2', {}]])); - const action = { type: NOTIFICATIONS_CLEAR }; - const result = reducer(state, action); - - expect(result.items.isEmpty()).toBe(true); - }); - }); - - describe('NOTIFICATIONS_MARK_READ_REQUEST', () => { - it('sets lastRead to the one in the action', () => { - const action = { type: NOTIFICATIONS_MARK_READ_REQUEST, lastRead: '1234' }; - const result = reducer(undefined, action); - - expect(result.lastRead).toEqual('1234'); - }); - }); - - describe('TIMELINE_DELETE', () => { - it('deletes notifications corresponding to the status ID', () => { - const actions = [{ - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications: [ - { id: '1', type: 'mention', status: { id: '4' }, account: { id: '7' } }, - { id: '2', type: 'mention', status: { id: '5' }, account: { id: '8' } }, - { id: '3', type: 'mention', status: { id: '6' }, account: { id: '9' } }, - { id: '4', type: 'mention', status: { id: '5' }, account: { id: '7' } }, - ], - next: null, - skipLoading: true, - }, { - type: TIMELINE_DELETE, - id: '5', - }]; - - // Setup by expanding, then calling `NOTIFICATIONS_FILTER_SET` - const result = applyActions(initialState, actions, reducer); - - expect(result.items.size).toEqual(2); - expect(result.items.get('5')).toBe(undefined); - }); - }); - - describe('MARKER_FETCH_SUCCESS', () => { - it('sets lastRead', () => { - const action = { - type: MARKER_FETCH_SUCCESS, - timeline: ['notifications'], - marker: { - notifications: { - last_read_id: '1234', - }, - }, - }; - - expect(reducer(undefined, action).get('lastRead')).toEqual('1234'); - }); - - it('updates the unread count', () => { - const action = { - type: MARKER_FETCH_SUCCESS, - timeline: ['notifications'], - marker: { - notifications: { - last_read_id: '5678', - }, - }, - }; - - const state = ImmutableMap({ - items: ImmutableOrderedMap({ - '9012': ImmutableMap({ id: '9012' }), - '5678': ImmutableMap({ id: '5678' }), - '1234': ImmutableMap({ id: '1234' }), - }), - unread: 3, - }); - - expect(reducer(state, action).get('unread')).toEqual(1); - }); - }); - - describe('MARKER_SAVE_REQUEST', () => { - it('sets lastRead', () => { - const action = { - type: MARKER_SAVE_REQUEST, - timeline: ['notifications'], - marker: { - notifications: { - last_read_id: '1234', - }, - }, - }; - - expect(reducer(undefined, action).get('lastRead')).toEqual('1234'); - }); - - it('updates the unread count', () => { - const action = { - type: MARKER_SAVE_REQUEST, - timeline: ['notifications'], - marker: { - notifications: { - last_read_id: '5678', - }, - }, - }; - - const state = ImmutableMap({ - items: ImmutableOrderedMap({ - '9012': ImmutableMap({ id: '9012' }), - '5678': ImmutableMap({ id: '5678' }), - '1234': ImmutableMap({ id: '1234' }), - }), - unread: 3, - }); - - expect(reducer(state, action).get('unread')).toEqual(1); - }); - }); - - describe('MARKER_SAVE_SUCCESS', () => { - it('sets lastRead', () => { - const action = { - type: MARKER_SAVE_SUCCESS, - timeline: ['notifications'], - marker: { - notifications: { - last_read_id: '5678', - }, - }, - }; - - expect(reducer(undefined, action).get('lastRead')).toEqual('5678'); - }); - - it('updates the unread count', () => { - const action = { - type: MARKER_SAVE_SUCCESS, - timeline: ['notifications'], - marker: { - notifications: { - last_read_id: '9012', - }, - }, - }; - - const state = ImmutableMap({ - items: ImmutableOrderedMap({ - '9012': ImmutableMap({ id: '9012' }), - '5678': ImmutableMap({ id: '5678' }), - '1234': ImmutableMap({ id: '1234' }), - }), - unread: 3, - }); - - expect(reducer(state, action).get('unread')).toEqual(0); - }); - }); -}); diff --git a/app/soapbox/reducers/__tests__/search-test.js b/app/soapbox/reducers/__tests__/search-test.js deleted file mode 100644 index 497f5b08c..000000000 --- a/app/soapbox/reducers/__tests__/search-test.js +++ /dev/null @@ -1,131 +0,0 @@ -import { Map as ImmutableMap, List as ImmutableList, Record as ImmutableRecord } from 'immutable'; - -import { - SEARCH_CHANGE, - SEARCH_CLEAR, - SEARCH_EXPAND_SUCCESS, -} from 'soapbox/actions/search'; - -import reducer from '../search'; - -describe('search reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {}).toJS()).toEqual({ - value: '', - submitted: false, - submittedValue: '', - hidden: false, - results: { - accounts: [], - statuses: [], - hashtags: [], - accountsHasMore: false, - statusesHasMore: false, - hashtagsHasMore: false, - accountsLoaded: false, - statusesLoaded: false, - hashtagsLoaded: false, - }, - filter: 'accounts', - accountId: null, - }); - }); - - describe('SEARCH_CHANGE', () => { - it('sets the value', () => { - const state = ImmutableMap({ value: 'hell' }); - const action = { type: SEARCH_CHANGE, value: 'hello' }; - expect(reducer(state, action).get('value')).toEqual('hello'); - }); - }); - - describe('SEARCH_CLEAR', () => { - it('resets the state', () => { - const state = ImmutableRecord({ - value: 'hello world', - submitted: true, - submittedValue: 'hello world', - hidden: false, - results: ImmutableRecord({})(), - filter: 'statuses', - })(); - - const action = { type: SEARCH_CLEAR }; - - const expected = { - value: '', - submitted: false, - submittedValue: '', - hidden: false, - results: { - accounts: [], - statuses: [], - hashtags: [], - accountsHasMore: false, - statusesHasMore: false, - hashtagsHasMore: false, - accountsLoaded: false, - statusesLoaded: false, - hashtagsLoaded: false, - }, - filter: 'accounts', - accountId: null, - }; - - expect(reducer(state, action).toJS()).toEqual(expected); - }); - }); - - describe(SEARCH_EXPAND_SUCCESS, () => { - it('imports hashtags as maps', () => { - const state = ImmutableRecord({ - value: 'artist', - submitted: true, - submittedValue: 'artist', - hidden: false, - results: ImmutableRecord({ - hashtags: ImmutableList(), - hashtagsHasMore: false, - hashtagsLoaded: true, - })(), - filter: 'hashtags', - })(); - - const action = { - type: SEARCH_EXPAND_SUCCESS, - results: { - accounts: [], - statuses: [], - hashtags: [{ - name: 'artist', - url: 'https://gleasonator.com/tags/artist', - history: [], - }], - }, - searchTerm: 'artist', - searchType: 'hashtags', - }; - - const expected = { - value: 'artist', - submitted: true, - submittedValue: 'artist', - hidden: false, - results: { - hashtags: [ - { - name: 'artist', - url: 'https://gleasonator.com/tags/artist', - history: [], - }, - ], - hashtagsHasMore: false, - hashtagsLoaded: true, - }, - filter: 'hashtags', - }; - - expect(reducer(state, action).toJS()).toEqual(expected); - }); - }); -}); diff --git a/app/soapbox/reducers/__tests__/user_lists.test.ts b/app/soapbox/reducers/__tests__/user_lists.test.ts index 5fbd5f1ea..fa23f845f 100644 --- a/app/soapbox/reducers/__tests__/user_lists.test.ts +++ b/app/soapbox/reducers/__tests__/user_lists.test.ts @@ -14,8 +14,6 @@ describe('user_lists reducer', () => { blocks: { next: null, items: ImmutableOrderedSet(), isLoading: false }, mutes: { next: null, items: ImmutableOrderedSet(), isLoading: false }, directory: { next: null, items: ImmutableOrderedSet(), isLoading: true }, - groups: {}, - groups_removed_accounts: {}, pinned: {}, birthday_reminders: {}, familiar_followers: {}, diff --git a/app/soapbox/reducers/group_editor.js b/app/soapbox/reducers/group_editor.js deleted file mode 100644 index 109182b1f..000000000 --- a/app/soapbox/reducers/group_editor.js +++ /dev/null @@ -1,58 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; - -import { - GROUP_CREATE_REQUEST, - GROUP_CREATE_FAIL, - GROUP_CREATE_SUCCESS, - GROUP_UPDATE_REQUEST, - GROUP_UPDATE_FAIL, - GROUP_UPDATE_SUCCESS, - GROUP_EDITOR_RESET, - GROUP_EDITOR_SETUP, - GROUP_EDITOR_VALUE_CHANGE, -} from '../actions/group_editor'; - -const initialState = ImmutableMap({ - groupId: null, - isSubmitting: false, - isChanged: false, - title: '', - description: '', - coverImage: null, -}); - -export default function groupEditorReducer(state = initialState, action) { - switch (action.type) { - case GROUP_EDITOR_RESET: - return initialState; - case GROUP_EDITOR_SETUP: - return state.withMutations(map => { - map.set('groupId', action.group.get('id')); - map.set('title', action.group.get('title')); - map.set('description', action.group.get('description')); - map.set('isSubmitting', false); - }); - case GROUP_EDITOR_VALUE_CHANGE: - return state.withMutations(map => { - map.set(action.field, action.value); - map.set('isChanged', true); - }); - case GROUP_CREATE_REQUEST: - case GROUP_UPDATE_REQUEST: - return state.withMutations(map => { - map.set('isSubmitting', true); - map.set('isChanged', false); - }); - case GROUP_CREATE_FAIL: - case GROUP_UPDATE_FAIL: - return state.set('isSubmitting', false); - case GROUP_CREATE_SUCCESS: - case GROUP_UPDATE_SUCCESS: - return state.withMutations(map => { - map.set('isSubmitting', false); - map.set('groupId', action.group.id); - }); - default: - return state; - } -} diff --git a/app/soapbox/reducers/group_lists.js b/app/soapbox/reducers/group_lists.js deleted file mode 100644 index 25b62648c..000000000 --- a/app/soapbox/reducers/group_lists.js +++ /dev/null @@ -1,22 +0,0 @@ -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; - -import { GROUPS_FETCH_SUCCESS } from '../actions/groups'; - -const initialState = ImmutableMap({ - featured: ImmutableList(), - member: ImmutableList(), - admin: ImmutableList(), -}); - -const normalizeList = (state, type, id, groups) => { - return state.set(type, ImmutableList(groups.map(item => item.id))); -}; - -export default function groupLists(state = initialState, action) { - switch (action.type) { - case GROUPS_FETCH_SUCCESS: - return normalizeList(state, action.tab, action.id, action.groups); - default: - return state; - } -} diff --git a/app/soapbox/reducers/group_relationships.js b/app/soapbox/reducers/group_relationships.js deleted file mode 100644 index f74fdb02e..000000000 --- a/app/soapbox/reducers/group_relationships.js +++ /dev/null @@ -1,27 +0,0 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; - -import { GROUP_RELATIONSHIPS_FETCH_SUCCESS, GROUP_JOIN_SUCCESS, GROUP_LEAVE_SUCCESS } from '../actions/groups'; - -const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship)); - -const normalizeRelationships = (state, relationships) => { - relationships.forEach(relationship => { - state = normalizeRelationship(state, relationship); - }); - - return state; -}; - -const initialState = ImmutableMap(); - -export default function group_relationships(state = initialState, action) { - switch (action.type) { - case GROUP_JOIN_SUCCESS: - case GROUP_LEAVE_SUCCESS: - return normalizeRelationship(state, action.relationship); - case GROUP_RELATIONSHIPS_FETCH_SUCCESS: - return normalizeRelationships(state, action.relationships); - default: - return state; - } -} diff --git a/app/soapbox/reducers/groups.js b/app/soapbox/reducers/groups.js deleted file mode 100644 index 8e301e468..000000000 --- a/app/soapbox/reducers/groups.js +++ /dev/null @@ -1,34 +0,0 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; - -import { GROUP_UPDATE_SUCCESS } from '../actions/group_editor'; -import { - GROUP_FETCH_SUCCESS, - GROUP_FETCH_FAIL, - GROUPS_FETCH_SUCCESS, -} from '../actions/groups'; - -const initialState = ImmutableMap(); - -const normalizeGroup = (state, group) => state.set(group.id, fromJS(group)); - -const normalizeGroups = (state, groups) => { - groups.forEach(group => { - state = normalizeGroup(state, group); - }); - - return state; -}; - -export default function groups(state = initialState, action) { - switch (action.type) { - case GROUP_FETCH_SUCCESS: - case GROUP_UPDATE_SUCCESS: - return normalizeGroup(state, action.group); - case GROUPS_FETCH_SUCCESS: - return normalizeGroups(state, action.groups); - case GROUP_FETCH_FAIL: - return state.set(action.id, false); - default: - return state; - } -} diff --git a/app/soapbox/reducers/index.ts b/app/soapbox/reducers/index.ts index 87750381b..8c59c014f 100644 --- a/app/soapbox/reducers/index.ts +++ b/app/soapbox/reducers/index.ts @@ -25,10 +25,6 @@ import custom_emojis from './custom_emojis'; import domain_lists from './domain_lists'; import dropdown_menu from './dropdown_menu'; import filters from './filters'; -import group_editor from './group_editor'; -import group_lists from './group_lists'; -import group_relationships from './group_relationships'; -import groups from './groups'; import history from './history'; import instance from './instance'; import listAdder from './list_adder'; @@ -95,10 +91,6 @@ const reducers = { suggestions, polls, trends, - groups, - group_relationships, - group_lists, - group_editor, sidebar, patron, soapbox, diff --git a/app/soapbox/reducers/timelines.ts b/app/soapbox/reducers/timelines.ts index def257407..97975d658 100644 --- a/app/soapbox/reducers/timelines.ts +++ b/app/soapbox/reducers/timelines.ts @@ -12,7 +12,6 @@ import { ACCOUNT_MUTE_SUCCESS, ACCOUNT_UNFOLLOW_SUCCESS, } from '../actions/accounts'; -import { GROUP_REMOVE_STATUS_SUCCESS } from '../actions/groups'; import { STATUS_CREATE_REQUEST, STATUS_CREATE_SUCCESS, @@ -210,10 +209,6 @@ const filterTimelines = (state: State, relationship: APIEntity, statuses: Immuta }); }; -const removeStatusFromGroup = (state: State, groupId: string, statusId: string) => { - return state.updateIn([`group:${groupId}`, 'items'], ImmutableOrderedSet(), ids => (ids as ImmutableOrderedSet).delete(statusId)); -}; - const timelineDequeue = (state: State, timelineId: string) => { const top = state.getIn([timelineId, 'top']); @@ -348,8 +343,6 @@ export default function timelines(state: State = initialState, action: AnyAction return timelineConnect(state, action.timeline); case TIMELINE_DISCONNECT: return timelineDisconnect(state, action.timeline); - case GROUP_REMOVE_STATUS_SUCCESS: - return removeStatusFromGroup(state, action.groupId, action.id); case TIMELINE_REPLACE: return state .update('home', TimelineRecord(), timeline => timeline.withMutations(timeline => { diff --git a/app/soapbox/reducers/user_lists.ts b/app/soapbox/reducers/user_lists.ts index 38017f0bb..4c84f1836 100644 --- a/app/soapbox/reducers/user_lists.ts +++ b/app/soapbox/reducers/user_lists.ts @@ -32,13 +32,6 @@ import { import { FAMILIAR_FOLLOWERS_FETCH_SUCCESS, } from '../actions/familiar_followers'; -import { - GROUP_MEMBERS_FETCH_SUCCESS, - GROUP_MEMBERS_EXPAND_SUCCESS, - GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS, - GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS, - GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS, -} from '../actions/groups'; import { REBLOGS_FETCH_SUCCESS, FAVOURITES_FETCH_SUCCESS, @@ -82,8 +75,6 @@ export const ReducerRecord = ImmutableRecord({ blocks: ListRecord(), mutes: ListRecord(), directory: ListRecord({ isLoading: true }), - groups: ImmutableMap(), - groups_removed_accounts: ImmutableMap(), pinned: ImmutableMap(), birthday_reminders: ImmutableMap(), familiar_followers: ImmutableMap(), @@ -94,7 +85,7 @@ export type List = ReturnType; type Reaction = ReturnType; type ReactionList = ReturnType; type Items = ImmutableOrderedSet; -type NestedListPath = ['followers' | 'following' | 'reblogged_by' | 'favourited_by' | 'reactions' | 'groups' | 'groups_removed_accounts' | 'pinned' | 'birthday_reminders' | 'familiar_followers', string]; +type NestedListPath = ['followers' | 'following' | 'reblogged_by' | 'favourited_by' | 'reactions' | 'pinned' | 'birthday_reminders' | 'familiar_followers', string]; type ListPath = ['follow_requests' | 'blocks' | 'mutes' | 'directory']; const normalizeList = (state: State, path: NestedListPath | ListPath, accounts: APIEntity[], next?: string | null) => { @@ -170,16 +161,6 @@ export default function userLists(state = ReducerRecord(), action: AnyAction) { case DIRECTORY_FETCH_FAIL: case DIRECTORY_EXPAND_FAIL: return state.setIn(['directory', 'isLoading'], false); - case GROUP_MEMBERS_FETCH_SUCCESS: - return normalizeList(state, ['groups', action.id], action.accounts, action.next); - case GROUP_MEMBERS_EXPAND_SUCCESS: - return appendToList(state, ['groups', action.id], action.accounts, action.next); - case GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS: - return normalizeList(state, ['groups_removed_accounts', action.id], action.accounts, action.next); - case GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS: - return appendToList(state, ['groups_removed_accounts', action.id], action.accounts, action.next); - case GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS: - return removeFromList(state, ['groups_removed_accounts', action.groupId], action.id); case PINNED_ACCOUNTS_FETCH_SUCCESS: return normalizeList(state, ['pinned', action.id], action.accounts, action.next); case BIRTHDAY_REMINDERS_FETCH_SUCCESS: diff --git a/app/soapbox/utils/sw.ts b/app/soapbox/utils/sw.ts new file mode 100644 index 000000000..910d7189a --- /dev/null +++ b/app/soapbox/utils/sw.ts @@ -0,0 +1,15 @@ +/** Unregister the ServiceWorker */ +// https://stackoverflow.com/a/49771828/8811886 +const unregisterSw = async(): Promise => { + if (navigator.serviceWorker) { + // FIXME: this only works if using a single tab. + // Send a message to sw.js instead to refresh all tabs. + const registrations = await navigator.serviceWorker.getRegistrations(); + const unregisterAll = registrations.map(r => r.unregister()); + await Promise.all(unregisterAll); + } +}; + +export { + unregisterSw, +}; \ No newline at end of file diff --git a/app/styles/components/media-gallery.scss b/app/styles/components/media-gallery.scss index 311af3b64..e9bb14425 100644 --- a/app/styles/components/media-gallery.scss +++ b/app/styles/components/media-gallery.scss @@ -1,6 +1,5 @@ .media-gallery { box-sizing: border-box; - margin-top: 8px; overflow: hidden; border-radius: 10px; position: relative; diff --git a/app/styles/components/reply-mentions.scss b/app/styles/components/reply-mentions.scss index 39b70a585..e767e5a03 100644 --- a/app/styles/components/reply-mentions.scss +++ b/app/styles/components/reply-mentions.scss @@ -10,7 +10,6 @@ .detailed-status { .reply-mentions { display: block; - margin: 4px 0 0 0; span { cursor: pointer; diff --git a/app/styles/components/status.scss b/app/styles/components/status.scss index a8c8e4561..4a2c57d3e 100644 --- a/app/styles/components/status.scss +++ b/app/styles/components/status.scss @@ -14,11 +14,6 @@ opacity: 1; animation: fade 150ms linear; - .video-player, - .audio-player { - margin-top: 8px; - } - &.light { .display-name { strong { diff --git a/app/styles/components/video-player.scss b/app/styles/components/video-player.scss index ada5b26f5..b4e87e9b6 100644 --- a/app/styles/components/video-player.scss +++ b/app/styles/components/video-player.scss @@ -7,7 +7,6 @@ flex-direction: column; height: 100%; justify-content: center; - margin-top: 8px; position: relative; text-align: center; z-index: 100; diff --git a/package.json b/package.json index 5c4491321..e4813bf50 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@sentry/browser": "^7.11.1", "@sentry/react": "^7.11.1", "@sentry/tracing": "^7.11.1", - "@tabler/icons": "^1.73.0", + "@tabler/icons": "^1.109.0", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/typography": "^0.5.7", "@tanstack/react-query": "^4.0.10", @@ -166,6 +166,7 @@ "react-inlinesvg": "^3.0.0", "react-intl": "^5.0.0", "react-motion": "^0.5.2", + "react-notification": "^6.8.5", "react-otp-input": "^2.4.0", "react-overlays": "^0.9.0", "react-popper": "^2.3.0", diff --git a/yarn.lock b/yarn.lock index 2b4a3a0cf..9c2729f93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2271,10 +2271,10 @@ remark "^13.0.0" unist-util-find-all-after "^3.0.2" -"@tabler/icons@^1.73.0": - version "1.73.0" - resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-1.73.0.tgz#26d81858baf41be939504e1f9b4b32835eda6fdb" - integrity sha512-MhAHFzVj79ZWlAIRD++7Mk55PZsdlEdkfkjO3DD257mqj8iJZQRAQtkx2UFJXVs2mMrcOUu1qtj4rlVC8BfnKA== +"@tabler/icons@^1.109.0": + version "1.109.0" + resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-1.109.0.tgz#11626c3fc097f2f70c4c197e4b9909fb05380752" + integrity sha512-B0YetE4pB6HY2Wa57v/LJ3NgkJzKYPze4U0DurIqPoKSptatKv2ga76FZSkO6EUpkYfHMtGPM6QjpJljfuCmAQ== "@tailwindcss/forms@^0.5.3": version "0.5.3" @@ -9922,6 +9922,13 @@ react-motion@^0.5.2: prop-types "^15.5.8" raf "^3.1.0" +react-notification@^6.8.5: + version "6.8.5" + resolved "https://registry.yarnpkg.com/react-notification/-/react-notification-6.8.5.tgz#7ea90a633bb2a280d899e30c93cf372265cce4f0" + integrity sha512-3pJPhSsWNYizpyeMeWuC+jVthqE9WKqQ6rHq2naiiP4fLGN4irwL2Xp2Q8Qn7agW/e4BIDxarab6fJOUp1cKUw== + dependencies: + prop-types "^15.6.2" + react-onclickoutside@^6.12.0: version "6.12.1" resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.12.1.tgz#92dddd28f55e483a1838c5c2930e051168c1e96b"