diff --git a/app/soapbox/actions/groups.ts b/app/soapbox/actions/groups.ts index ca023c4ec..3c115b425 100644 --- a/app/soapbox/actions/groups.ts +++ b/app/soapbox/actions/groups.ts @@ -131,7 +131,7 @@ const createGroup = (params: Record, shouldReset?: boolean) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(createGroupRequest()); - api(getState).post('/api/v1/groups', params, { + return api(getState).post('/api/v1/groups', params, { headers: { 'Content-Type': 'multipart/form-data', }, @@ -166,7 +166,7 @@ const updateGroup = (id: string, params: Record, shouldReset?: bool (dispatch: AppDispatch, getState: () => RootState) => { dispatch(updateGroupRequest()); - api(getState).put(`/api/v1/groups/${id}`, params) + return api(getState).put(`/api/v1/groups/${id}`, params) .then(({ data }) => { dispatch(importFetchedGroups([data])); dispatch(updateGroupSuccess(data)); @@ -196,7 +196,7 @@ const updateGroupFail = (error: AxiosError) => ({ const deleteGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(deleteGroupRequest(id)); - api(getState).delete(`/api/v1/groups/${id}`) + return api(getState).delete(`/api/v1/groups/${id}`) .then(() => dispatch(deleteGroupSuccess(id))) .catch(err => dispatch(deleteGroupFail(id, err))); }; @@ -221,7 +221,7 @@ const fetchGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootS dispatch(fetchGroupRelationships([id])); dispatch(fetchGroupRequest(id)); - api(getState).get(`/api/v1/groups/${id}`) + return api(getState).get(`/api/v1/groups/${id}`) .then(({ data }) => { dispatch(importFetchedGroups([data])); dispatch(fetchGroupSuccess(data)); @@ -248,7 +248,7 @@ const fetchGroupFail = (id: string, error: AxiosError) => ({ const fetchGroups = () => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchGroupsRequest()); - api(getState).get('/api/v1/groups') + return api(getState).get('/api/v1/groups') .then(({ data }) => { dispatch(importFetchedGroups(data)); dispatch(fetchGroupsSuccess(data)); @@ -282,7 +282,7 @@ const fetchGroupRelationships = (groupIds: string[]) => dispatch(fetchGroupRelationshipsRequest(newGroupIds)); - api(getState).get(`/api/v1/groups/relationships?${newGroupIds.map(id => `id[]=${id}`).join('&')}`).then(response => { + return api(getState).get(`/api/v1/groups/relationships?${newGroupIds.map(id => `id[]=${id}`).join('&')}`).then(response => { dispatch(fetchGroupRelationshipsSuccess(response.data)); }).catch(error => { dispatch(fetchGroupRelationshipsFail(error)); @@ -314,7 +314,7 @@ const joinGroup = (id: string) => dispatch(joinGroupRequest(id, locked)); - api(getState).post(`/api/v1/groups/${id}/join`).then(response => { + return api(getState).post(`/api/v1/groups/${id}/join`).then(response => { dispatch(joinGroupSuccess(response.data)); dispatch(snackbar.success(locked ? messages.joinRequestSuccess : messages.joinSuccess)); }).catch(error => { @@ -326,7 +326,7 @@ const leaveGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(leaveGroupRequest(id)); - api(getState).post(`/api/v1/groups/${id}/leave`).then(response => { + return api(getState).post(`/api/v1/groups/${id}/leave`).then(response => { dispatch(leaveGroupSuccess(response.data)); dispatch(snackbar.success(messages.leaveSuccess)); }).catch(error => { @@ -376,7 +376,7 @@ const groupDeleteStatus = (groupId: string, statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(groupDeleteStatusRequest(groupId, statusId)); - api(getState).delete(`/api/v1/groups/${groupId}/statuses/${statusId}`) + return api(getState).delete(`/api/v1/groups/${groupId}/statuses/${statusId}`) .then(() => { dispatch(deleteFromTimelines(statusId)); dispatch(groupDeleteStatusSuccess(groupId, statusId)); @@ -434,7 +434,7 @@ const fetchGroupBlocks = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchGroupBlocksRequest(id)); - api(getState).get(`/api/v1/groups/${id}/blocks`).then(response => { + return api(getState).get(`/api/v1/groups/${id}/blocks`).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); @@ -473,7 +473,7 @@ const expandGroupBlocks = (id: string) => dispatch(expandGroupBlocksRequest(id)); - api(getState).get(url).then(response => { + return api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); @@ -620,7 +620,7 @@ const fetchGroupMemberships = (id: string, role: GroupRole) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchGroupMembershipsRequest(id, role)); - api(getState).get(`/api/v1/groups/${id}/memberships`, { params: { role } }).then(response => { + return api(getState).get(`/api/v1/groups/${id}/memberships`, { params: { role } }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data.map((membership: APIEntity) => membership.account))); @@ -662,7 +662,7 @@ const expandGroupMemberships = (id: string, role: GroupRole) => dispatch(expandGroupMembershipsRequest(id, role)); - api(getState).get(url).then(response => { + return api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data.map((membership: APIEntity) => membership.account))); @@ -698,7 +698,7 @@ const fetchGroupMembershipRequests = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchGroupMembershipRequestsRequest(id)); - api(getState).get(`/api/v1/groups/${id}/membership_requests`).then(response => { + return api(getState).get(`/api/v1/groups/${id}/membership_requests`).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); @@ -737,7 +737,7 @@ const expandGroupMembershipRequests = (id: string) => dispatch(expandGroupMembershipRequestsRequest(id)); - api(getState).get(url).then(response => { + return api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); @@ -770,7 +770,7 @@ const authorizeGroupMembershipRequest = (groupId: string, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(authorizeGroupMembershipRequestRequest(groupId, accountId)); - api(getState) + return api(getState) .post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/authorize`) .then(() => dispatch(authorizeGroupMembershipRequestSuccess(groupId, accountId))) .catch(error => dispatch(authorizeGroupMembershipRequestFail(groupId, accountId, error))); @@ -799,7 +799,7 @@ const rejectGroupMembershipRequest = (groupId: string, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(rejectGroupMembershipRequestRequest(groupId, accountId)); - api(getState) + return api(getState) .post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/reject`) .then(() => dispatch(rejectGroupMembershipRequestSuccess(groupId, accountId))) .catch(error => dispatch(rejectGroupMembershipRequestFail(groupId, accountId, error))); diff --git a/app/soapbox/components/account.tsx b/app/soapbox/components/account.tsx index 5618b8cf8..0fd843d6b 100644 --- a/app/soapbox/components/account.tsx +++ b/app/soapbox/components/account.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { FormattedMessage } from 'react-intl'; import { Link, useHistory } from 'react-router-dom'; import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper'; @@ -11,6 +12,7 @@ import { displayFqn } from 'soapbox/utils/state'; import RelativeTimestamp from './relative-timestamp'; import { Avatar, Emoji, HStack, Icon, IconButton, Stack, Text } from './ui'; +import type { StatusApprovalStatus } from 'soapbox/normalizers/status'; import type { Account as AccountEntity } from 'soapbox/types/entities'; interface IInstanceFavicon { @@ -68,6 +70,7 @@ interface IAccount { withLinkToProfile?: boolean, withRelationship?: boolean, showEdit?: boolean, + approvalStatus?: StatusApprovalStatus, emoji?: string, note?: string, } @@ -92,6 +95,7 @@ const Account = ({ withLinkToProfile = true, withRelationship = true, showEdit = false, + approvalStatus, emoji, note, }: IAccount) => { @@ -236,6 +240,18 @@ const Account = ({ ) : null} + {approvalStatus && ['pending', 'rejected'].includes(approvalStatus) && ( + <> + · + + + {approvalStatus === 'pending' + ? + : } + + + )} + {showEdit ? ( <> · diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index b9e513fa2..86c8489da 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -388,6 +388,7 @@ const Status: React.FC = (props) => { showEdit={!!actualStatus.edited_at} showProfileHoverCard={hoverable} withLinkToProfile={hoverable} + approvalStatus={actualStatus.approval_status} /> diff --git a/app/soapbox/containers/group-container.tsx b/app/soapbox/containers/group-container.tsx new file mode 100644 index 000000000..f1254b2ca --- /dev/null +++ b/app/soapbox/containers/group-container.tsx @@ -0,0 +1,24 @@ +import React, { useCallback } from 'react'; + +import GroupCard from 'soapbox/components/group-card'; +import { useAppSelector } from 'soapbox/hooks'; +import { makeGetGroup } from 'soapbox/selectors'; + +interface IGroupContainer { + id: string +} + +const GroupContainer: React.FC = (props) => { + const { id, ...rest } = props; + + const getGroup = useCallback(makeGetGroup(), []); + const group = useAppSelector(state => getGroup(state, id)); + + if (group) { + return ; + } else { + return null; + } +}; + +export default GroupContainer; diff --git a/app/soapbox/features/compose/components/search-results.tsx b/app/soapbox/features/compose/components/search-results.tsx index c1dedd455..97a941fa0 100644 --- a/app/soapbox/features/compose/components/search-results.tsx +++ b/app/soapbox/features/compose/components/search-results.tsx @@ -9,8 +9,10 @@ import IconButton from 'soapbox/components/icon-button'; import ScrollableList from 'soapbox/components/scrollable-list'; import { HStack, Tabs, Text } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account-container'; +import GroupContainer from 'soapbox/containers/group-container'; import StatusContainer from 'soapbox/containers/status-container'; import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder-account'; +import PlaceholderGroupCard from 'soapbox/features/placeholder/components/placeholder-group-card'; import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder-hashtag'; import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder-status'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; @@ -179,23 +181,15 @@ const SearchResults = () => { if (selectedFilter === 'groups') { hasMore = results.groupsHasMore; loaded = results.groupsLoaded; + placeholderComponent = PlaceholderGroupCard; if (results.groups && results.groups.size > 0) { searchResults = results.groups.map((groupId: string) => ( - <> + )); resultsIds = results.groups; } else if (!submitted && trendingStatuses && !trendingStatuses.isEmpty()) { - // searchResults = trendingStatuses.map((statusId: string) => ( - // // @ts-ignore - // - // )); - // resultsIds = trendingStatuses; + searchResults = null; } else if (loaded) { noResultsMessage = (
diff --git a/app/soapbox/features/directory/components/account-card.tsx b/app/soapbox/features/directory/components/account-card.tsx index 6c6d0a697..b5cb6fc17 100644 --- a/app/soapbox/features/directory/components/account-card.tsx +++ b/app/soapbox/features/directory/components/account-card.tsx @@ -3,10 +3,10 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import { getSettings } from 'soapbox/actions/settings'; +import Account from 'soapbox/components/account'; import Badge from 'soapbox/components/badge'; import RelativeTimestamp from 'soapbox/components/relative-timestamp'; import { Stack, Text } from 'soapbox/components/ui'; -import AccountContainer from 'soapbox/containers/account-container'; import ActionButton from 'soapbox/features/ui/components/action-button'; import { useAppSelector } from 'soapbox/hooks'; import { makeGetAccount } from 'soapbox/selectors'; @@ -51,8 +51,8 @@ const AccountCard: React.FC = ({ id }) => {
- diff --git a/app/soapbox/features/group/components/group-header.tsx b/app/soapbox/features/group/components/group-header.tsx index ef6e08324..fb9389c20 100644 --- a/app/soapbox/features/group/components/group-header.tsx +++ b/app/soapbox/features/group/components/group-header.tsx @@ -2,7 +2,7 @@ import { List as ImmutableList } from 'immutable'; import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { editGroup, joinGroup, leaveGroup } from 'soapbox/actions/groups'; +import { joinGroup, leaveGroup } from 'soapbox/actions/groups'; import { openModal } from 'soapbox/actions/modals'; import StillImage from 'soapbox/components/still-image'; import { Avatar, Button, HStack, Icon, Stack, Text } from 'soapbox/components/ui'; @@ -47,24 +47,15 @@ const GroupHeader: React.FC = ({ group }) => { ); } - const onJoinGroup = () => { - dispatch(joinGroup(group.id)); - }; + const onJoinGroup = () => dispatch(joinGroup(group.id)); - const onLeaveGroup = () => { + const onLeaveGroup = () => dispatch(openModal('CONFIRM', { heading: intl.formatMessage(messages.confirmationHeading), message: intl.formatMessage(messages.confirmationMessage), confirm: intl.formatMessage(messages.confirmationConfirm), - onConfirm: () => { - dispatch(leaveGroup(group.id)); - }, + onConfirm: () => dispatch(leaveGroup(group.id)), })); - }; - - const onEditGroup = () => { - dispatch(editGroup(group)); - }; const onAvatarClick = () => { const avatar = normalizeAttachment({ @@ -131,13 +122,24 @@ const GroupHeader: React.FC = ({ group }) => { ); } + if (group.relationship.requested) { + return ( + + ); + } + if (group.relationship?.role === 'admin') { return ( ); } diff --git a/app/soapbox/features/group/group-blocked-members.tsx b/app/soapbox/features/group/group-blocked-members.tsx new file mode 100644 index 000000000..293aef795 --- /dev/null +++ b/app/soapbox/features/group/group-blocked-members.tsx @@ -0,0 +1,104 @@ +import React, { useCallback, useEffect } from 'react'; +import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; + +import { fetchGroup, fetchGroupBlocks, groupUnblock } from 'soapbox/actions/groups'; +import snackbar from 'soapbox/actions/snackbar'; +import Account from 'soapbox/components/account'; +import ScrollableList from 'soapbox/components/scrollable-list'; +import { Button, Column, HStack, Spinner } from 'soapbox/components/ui'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { makeGetAccount, makeGetGroup } from 'soapbox/selectors'; + +import ColumnForbidden from '../ui/components/column-forbidden'; + +type RouteParams = { id: string }; + +const messages = defineMessages({ + heading: { id: 'column.group_blocked_members', defaultMessage: 'Blocked members' }, + unblock: { id: 'group.group_mod_unblock', defaultMessage: 'Unblock' }, + unblocked: { id: 'group.group_mod_unblock.success', defaultMessage: 'Unblocked @{name} from group' }, +}); + +interface IBlockedMember { + accountId: string + groupId: string +} + +const BlockedMember: React.FC = ({ accountId, groupId }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + const getAccount = useCallback(makeGetAccount(), []); + + const account = useAppSelector((state) => getAccount(state, accountId)); + + if (!account) return null; + + const handleUnblock = () => + dispatch(groupUnblock(groupId, accountId)).then(() => { + dispatch(snackbar.success(intl.formatMessage(messages.unblocked, { name: account.acct }))); + }); + + return ( + +
+ +
+ + + {groups.map((group) => ( + + + + ))} + +
); }; diff --git a/app/soapbox/features/notifications/components/notification.tsx b/app/soapbox/features/notifications/components/notification.tsx index bf330a6c1..1bc8b1760 100644 --- a/app/soapbox/features/notifications/components/notification.tsx +++ b/app/soapbox/features/notifications/components/notification.tsx @@ -8,16 +8,16 @@ import { reblog, favourite, unreblog, unfavourite } from 'soapbox/actions/intera import { openModal } from 'soapbox/actions/modals'; import { getSettings } from 'soapbox/actions/settings'; import { hideStatus, revealStatus } from 'soapbox/actions/statuses'; +import Account from 'soapbox/components/account'; import Icon from 'soapbox/components/icon'; import { HStack, Text, Emoji } from 'soapbox/components/ui'; -import AccountContainer from 'soapbox/containers/account-container'; import StatusContainer from 'soapbox/containers/status-container'; import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks'; import { makeGetNotification } from 'soapbox/selectors'; import { NotificationType, validType } from 'soapbox/utils/notification'; import type { ScrollPosition } from 'soapbox/components/status'; -import type { Account, Status as StatusEntity, Notification as NotificationEntity } from 'soapbox/types/entities'; +import type { Account as AccountEntity, Status as StatusEntity, Notification as NotificationEntity } from 'soapbox/types/entities'; const notificationForScreenReader = (intl: IntlShape, message: string, timestamp: Date) => { const output = [message]; @@ -27,7 +27,7 @@ const notificationForScreenReader = (intl: IntlShape, message: string, timestamp return output.join(', '); }; -const buildLink = (account: Account): JSX.Element => ( +const buildLink = (account: AccountEntity): JSX.Element => ( = defineMessages({ const buildMessage = ( intl: IntlShape, type: NotificationType, - account: Account, + account: AccountEntity, totalCount: number | null, targetName: string, instanceTitle: string, @@ -287,16 +287,16 @@ const Notification: React.FC = (props) => { case 'follow': case 'user_approved': return account && typeof account === 'object' ? ( -