diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9516ec2d6..97892e166 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: node:18 +image: node:20 variables: NODE_ENV: test diff --git a/.tool-versions b/.tool-versions index ab43e6ab2..6de89a83a 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -nodejs 18.14.0 +nodejs 20.0.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index b27bae103..f83896efc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Posts: Support posts filtering on recent Mastodon versions - Reactions: Support custom emoji reactions - Compatbility: Support Mastodon v2 timeline filters. +- Compatbility: Preliminary support for Ditto backend. - Posts: Support dislikes on Friendica. - UI: added a character counter to some textareas. @@ -30,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 18n: fixed Chinese language being detected from the browser. - Conversations: fixed pagination (Mastodon). - Compatibility: fix version parsing for Friendica. +- UI: fixed various overflow issues related to long usernames. +- UI: fixed display of Markdown code blocks in the reply indicator. ## [3.2.0] - 2023-02-15 diff --git a/Dockerfile b/Dockerfile index bfb7c2e48..b02bf86e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18 as build +FROM node:20 as build WORKDIR /app COPY package.json . COPY yarn.lock . diff --git a/Dockerfile.dev b/Dockerfile.dev index 8d1655db0..1e6056945 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM node:18 +FROM node:20 RUN apt-get update &&\ apt-get install -y inotify-tools &&\ diff --git a/app/soapbox/actions/__tests__/account-notes.test.ts b/app/soapbox/actions/__tests__/account-notes.test.ts index 8b85eecc5..a00a9d877 100644 --- a/app/soapbox/actions/__tests__/account-notes.test.ts +++ b/app/soapbox/actions/__tests__/account-notes.test.ts @@ -1,10 +1,11 @@ import { Map as ImmutableMap } from 'immutable'; import { __stub } from 'soapbox/api'; +import { buildRelationship } from 'soapbox/jest/factory'; import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { ReducerRecord, EditRecord } from 'soapbox/reducers/account-notes'; -import { normalizeAccount, normalizeRelationship } from '../../normalizers'; +import { normalizeAccount } from '../../normalizers'; import { changeAccountNoteComment, initAccountNoteModal, submitAccountNote } from '../account-notes'; import type { Account } from 'soapbox/types/entities'; @@ -66,7 +67,7 @@ describe('initAccountNoteModal()', () => { beforeEach(() => { const state = rootState - .set('relationships', ImmutableMap({ '1': normalizeRelationship({ note: 'hello' }) })); + .set('relationships', ImmutableMap({ '1': buildRelationship({ note: 'hello' }) })); store = mockStore(state); }); diff --git a/app/soapbox/actions/__tests__/accounts.test.ts b/app/soapbox/actions/__tests__/accounts.test.ts index d9faa0213..c13f8ef90 100644 --- a/app/soapbox/actions/__tests__/accounts.test.ts +++ b/app/soapbox/actions/__tests__/accounts.test.ts @@ -1,10 +1,11 @@ import { Map as ImmutableMap } from 'immutable'; import { __stub } from 'soapbox/api'; +import { buildRelationship } from 'soapbox/jest/factory'; import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { ListRecord, ReducerRecord } from 'soapbox/reducers/user-lists'; -import { normalizeAccount, normalizeInstance, normalizeRelationship } from '../../normalizers'; +import { normalizeAccount, normalizeInstance } from '../../normalizers'; import { authorizeFollowRequest, blockAccount, @@ -1340,7 +1341,7 @@ describe('fetchRelationships()', () => { describe('without newAccountIds', () => { beforeEach(() => { const state = rootState - .set('relationships', ImmutableMap({ [id]: normalizeRelationship({}) })) + .set('relationships', ImmutableMap({ [id]: buildRelationship() })) .set('me', '123'); store = mockStore(state); }); diff --git a/app/soapbox/actions/groups.ts b/app/soapbox/actions/groups.ts index 690b74540..ad760d916 100644 --- a/app/soapbox/actions/groups.ts +++ b/app/soapbox/actions/groups.ts @@ -4,7 +4,6 @@ import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; import { importFetchedGroups, importFetchedAccounts } from './importer'; -import { deleteFromTimelines } from './timelines'; import type { AxiosError } from 'axios'; import type { GroupRole } from 'soapbox/reducers/group-memberships'; @@ -35,10 +34,6 @@ const GROUP_RELATIONSHIPS_FETCH_REQUEST = 'GROUP_RELATIONSHIPS_FETCH_REQUEST'; const GROUP_RELATIONSHIPS_FETCH_SUCCESS = 'GROUP_RELATIONSHIPS_FETCH_SUCCESS'; const GROUP_RELATIONSHIPS_FETCH_FAIL = 'GROUP_RELATIONSHIPS_FETCH_FAIL'; -const GROUP_DELETE_STATUS_REQUEST = 'GROUP_DELETE_STATUS_REQUEST'; -const GROUP_DELETE_STATUS_SUCCESS = 'GROUP_DELETE_STATUS_SUCCESS'; -const GROUP_DELETE_STATUS_FAIL = 'GROUP_DELETE_STATUS_FAIL'; - const GROUP_KICK_REQUEST = 'GROUP_KICK_REQUEST'; const GROUP_KICK_SUCCESS = 'GROUP_KICK_SUCCESS'; const GROUP_KICK_FAIL = 'GROUP_KICK_FAIL'; @@ -206,36 +201,6 @@ const fetchGroupRelationshipsFail = (error: AxiosError) => ({ skipNotFound: true, }); -const groupDeleteStatus = (groupId: string, statusId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(groupDeleteStatusRequest(groupId, statusId)); - - return api(getState).delete(`/api/v1/groups/${groupId}/statuses/${statusId}`) - .then(() => { - dispatch(deleteFromTimelines(statusId)); - dispatch(groupDeleteStatusSuccess(groupId, statusId)); - }).catch(err => dispatch(groupDeleteStatusFail(groupId, statusId, err))); - }; - -const groupDeleteStatusRequest = (groupId: string, statusId: string) => ({ - type: GROUP_DELETE_STATUS_REQUEST, - groupId, - statusId, -}); - -const groupDeleteStatusSuccess = (groupId: string, statusId: string) => ({ - type: GROUP_DELETE_STATUS_SUCCESS, - groupId, - statusId, -}); - -const groupDeleteStatusFail = (groupId: string, statusId: string, error: AxiosError) => ({ - type: GROUP_DELETE_STATUS_SUCCESS, - groupId, - statusId, - error, -}); - const groupKick = (groupId: string, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(groupKickRequest(groupId, accountId)); @@ -677,9 +642,6 @@ export { GROUP_RELATIONSHIPS_FETCH_REQUEST, GROUP_RELATIONSHIPS_FETCH_SUCCESS, GROUP_RELATIONSHIPS_FETCH_FAIL, - GROUP_DELETE_STATUS_REQUEST, - GROUP_DELETE_STATUS_SUCCESS, - GROUP_DELETE_STATUS_FAIL, GROUP_KICK_REQUEST, GROUP_KICK_SUCCESS, GROUP_KICK_FAIL, @@ -735,10 +697,6 @@ export { fetchGroupRelationshipsRequest, fetchGroupRelationshipsSuccess, fetchGroupRelationshipsFail, - groupDeleteStatus, - groupDeleteStatusRequest, - groupDeleteStatusSuccess, - groupDeleteStatusFail, groupKick, groupKickRequest, groupKickSuccess, diff --git a/app/soapbox/actions/importer/index.ts b/app/soapbox/actions/importer/index.ts index ec0ec3121..fc9ad63bd 100644 --- a/app/soapbox/actions/importer/index.ts +++ b/app/soapbox/actions/importer/index.ts @@ -74,7 +74,7 @@ const importFetchedGroup = (group: APIEntity) => importFetchedGroups([group]); const importFetchedGroups = (groups: APIEntity[]) => { - const entities = filteredArray(groupSchema).catch([]).parse(groups); + const entities = filteredArray(groupSchema).parse(groups); return importGroups(entities); }; diff --git a/app/soapbox/actions/moderation.tsx b/app/soapbox/actions/moderation.tsx index c236a2986..cd08fcd2f 100644 --- a/app/soapbox/actions/moderation.tsx +++ b/app/soapbox/actions/moderation.tsx @@ -142,7 +142,7 @@ const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = () dispatch(openModal('CONFIRM', { icon: require('@tabler/icons/trash.svg'), heading: intl.formatMessage(messages.deleteStatusHeading), - message: intl.formatMessage(messages.deleteStatusPrompt, { acct }), + message: intl.formatMessage(messages.deleteStatusPrompt, { acct: {acct} }), confirm: intl.formatMessage(messages.deleteStatusConfirm), onConfirm: () => { dispatch(deleteStatus(statusId)).then(() => { diff --git a/app/soapbox/actions/search.ts b/app/soapbox/actions/search.ts index 6d64b6534..a2f165ac0 100644 --- a/app/soapbox/actions/search.ts +++ b/app/soapbox/actions/search.ts @@ -1,7 +1,7 @@ import api from '../api'; import { fetchRelationships } from './accounts'; -import { importFetchedAccounts, importFetchedGroups, importFetchedStatuses } from './importer'; +import { importFetchedAccounts, importFetchedStatuses } from './importer'; import type { AxiosError } from 'axios'; import type { SearchFilter } from 'soapbox/reducers/search'; @@ -83,10 +83,6 @@ const submitSearch = (filter?: SearchFilter) => dispatch(importFetchedStatuses(response.data.statuses)); } - if (response.data.groups) { - dispatch(importFetchedGroups(response.data.groups)); - } - dispatch(fetchSearchSuccess(response.data, value, type)); dispatch(fetchRelationships(response.data.accounts.map((item: APIEntity) => item.id))); }).catch(error => { @@ -143,10 +139,6 @@ const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: ( dispatch(importFetchedStatuses(data.statuses)); } - if (data.groups) { - dispatch(importFetchedGroups(data.groups)); - } - dispatch(expandSearchSuccess(data, value, type)); dispatch(fetchRelationships(data.accounts.map((item: APIEntity) => item.id))); }).catch(error => { diff --git a/app/soapbox/hooks/api/useAccount.ts b/app/soapbox/api/hooks/accounts/useAccount.ts similarity index 91% rename from app/soapbox/hooks/api/useAccount.ts rename to app/soapbox/api/hooks/accounts/useAccount.ts index a222d8de4..2442ad642 100644 --- a/app/soapbox/hooks/api/useAccount.ts +++ b/app/soapbox/api/hooks/accounts/useAccount.ts @@ -1,8 +1,8 @@ import { Entities } from 'soapbox/entity-store/entities'; import { useEntity } from 'soapbox/entity-store/hooks'; +import { useApi } from 'soapbox/hooks/useApi'; import { type Account, accountSchema } from 'soapbox/schemas'; -import { useApi } from '../useApi'; import { useRelationships } from './useRelationships'; diff --git a/app/soapbox/hooks/api/useRelationships.ts b/app/soapbox/api/hooks/accounts/useRelationships.ts similarity index 88% rename from app/soapbox/hooks/api/useRelationships.ts rename to app/soapbox/api/hooks/accounts/useRelationships.ts index fde5f1017..2103e2438 100644 --- a/app/soapbox/hooks/api/useRelationships.ts +++ b/app/soapbox/api/hooks/accounts/useRelationships.ts @@ -1,9 +1,8 @@ import { Entities } from 'soapbox/entity-store/entities'; import { useEntities } from 'soapbox/entity-store/hooks'; +import { useApi } from 'soapbox/hooks/useApi'; import { type Relationship, relationshipSchema } from 'soapbox/schemas'; -import { useApi } from '../useApi'; - function useRelationships(ids: string[]) { const api = useApi(); diff --git a/app/soapbox/hooks/api/groups/useBlockGroupMember.ts b/app/soapbox/api/hooks/groups/useBlockGroupMember.ts similarity index 100% rename from app/soapbox/hooks/api/groups/useBlockGroupMember.ts rename to app/soapbox/api/hooks/groups/useBlockGroupMember.ts diff --git a/app/soapbox/hooks/api/groups/useCancelMembershipRequest.ts b/app/soapbox/api/hooks/groups/useCancelMembershipRequest.ts similarity index 100% rename from app/soapbox/hooks/api/groups/useCancelMembershipRequest.ts rename to app/soapbox/api/hooks/groups/useCancelMembershipRequest.ts diff --git a/app/soapbox/hooks/api/groups/useCreateGroup.ts b/app/soapbox/api/hooks/groups/useCreateGroup.ts similarity index 100% rename from app/soapbox/hooks/api/groups/useCreateGroup.ts rename to app/soapbox/api/hooks/groups/useCreateGroup.ts diff --git a/app/soapbox/hooks/api/groups/useDeleteGroup.ts b/app/soapbox/api/hooks/groups/useDeleteGroup.ts similarity index 100% rename from app/soapbox/hooks/api/groups/useDeleteGroup.ts rename to app/soapbox/api/hooks/groups/useDeleteGroup.ts diff --git a/app/soapbox/api/hooks/groups/useDeleteGroupStatus.ts b/app/soapbox/api/hooks/groups/useDeleteGroupStatus.ts new file mode 100644 index 000000000..55a6f9459 --- /dev/null +++ b/app/soapbox/api/hooks/groups/useDeleteGroupStatus.ts @@ -0,0 +1,20 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useDeleteEntity } from 'soapbox/entity-store/hooks'; +import { useApi } from 'soapbox/hooks'; + +import type { Group } from 'soapbox/schemas'; + +function useDeleteGroupStatus(group: Group, statusId: string) { + const api = useApi(); + const { deleteEntity, isSubmitting } = useDeleteEntity( + Entities.STATUSES, + () => api.delete(`/api/v1/groups/${group.id}/statuses/${statusId}`), + ); + + return { + mutate: deleteEntity, + isSubmitting, + }; +} + +export { useDeleteGroupStatus }; \ No newline at end of file diff --git a/app/soapbox/hooks/api/groups/useDemoteGroupMember.ts b/app/soapbox/api/hooks/groups/useDemoteGroupMember.ts similarity index 100% rename from app/soapbox/hooks/api/groups/useDemoteGroupMember.ts rename to app/soapbox/api/hooks/groups/useDemoteGroupMember.ts diff --git a/app/soapbox/api/hooks/groups/useGroup.ts b/app/soapbox/api/hooks/groups/useGroup.ts new file mode 100644 index 000000000..b66c0fee7 --- /dev/null +++ b/app/soapbox/api/hooks/groups/useGroup.ts @@ -0,0 +1,24 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntity } from 'soapbox/entity-store/hooks'; +import { useApi } from 'soapbox/hooks'; +import { type Group, groupSchema } from 'soapbox/schemas'; + +import { useGroupRelationship } from './useGroupRelationship'; + +function useGroup(groupId: string, refetch = true) { + const api = useApi(); + + const { entity: group, ...result } = useEntity( + [Entities.GROUPS, groupId], + () => api.get(`/api/v1/groups/${groupId}`), + { schema: groupSchema, refetch }, + ); + const { entity: relationship } = useGroupRelationship(groupId); + + return { + ...result, + group: group ? { ...group, relationship: relationship || null } : undefined, + }; +} + +export { useGroup }; \ No newline at end of file diff --git a/app/soapbox/hooks/api/groups/useGroupLookup.ts b/app/soapbox/api/hooks/groups/useGroupLookup.ts similarity index 100% rename from app/soapbox/hooks/api/groups/useGroupLookup.ts rename to app/soapbox/api/hooks/groups/useGroupLookup.ts diff --git a/app/soapbox/hooks/api/groups/useGroupMedia.ts b/app/soapbox/api/hooks/groups/useGroupMedia.ts similarity index 100% rename from app/soapbox/hooks/api/groups/useGroupMedia.ts rename to app/soapbox/api/hooks/groups/useGroupMedia.ts diff --git a/app/soapbox/hooks/api/useGroupMembers.ts b/app/soapbox/api/hooks/groups/useGroupMembers.ts similarity index 89% rename from app/soapbox/hooks/api/useGroupMembers.ts rename to app/soapbox/api/hooks/groups/useGroupMembers.ts index 3a1c99af5..a9b03e7f2 100644 --- a/app/soapbox/hooks/api/useGroupMembers.ts +++ b/app/soapbox/api/hooks/groups/useGroupMembers.ts @@ -3,7 +3,7 @@ import { useEntities } from 'soapbox/entity-store/hooks'; import { GroupMember, groupMemberSchema } from 'soapbox/schemas'; import { GroupRoles } from 'soapbox/schemas/group-member'; -import { useApi } from '../useApi'; +import { useApi } from '../../../hooks/useApi'; function useGroupMembers(groupId: string, role: GroupRoles) { const api = useApi(); diff --git a/app/soapbox/hooks/api/groups/useGroupMembershipRequests.ts b/app/soapbox/api/hooks/groups/useGroupMembershipRequests.ts similarity index 71% rename from app/soapbox/hooks/api/groups/useGroupMembershipRequests.ts rename to app/soapbox/api/hooks/groups/useGroupMembershipRequests.ts index 78a59ced6..a6e068091 100644 --- a/app/soapbox/hooks/api/groups/useGroupMembershipRequests.ts +++ b/app/soapbox/api/hooks/groups/useGroupMembershipRequests.ts @@ -1,10 +1,10 @@ import { Entities } from 'soapbox/entity-store/entities'; -import { useEntities, useIncrementEntity } from 'soapbox/entity-store/hooks'; +import { useDismissEntity, useEntities } from 'soapbox/entity-store/hooks'; import { useApi } from 'soapbox/hooks/useApi'; import { accountSchema } from 'soapbox/schemas'; import { GroupRoles } from 'soapbox/schemas/group-member'; -import { useGroupRelationship } from './useGroups'; +import { useGroupRelationship } from './useGroupRelationship'; import type { ExpandedEntitiesPath } from 'soapbox/entity-store/hooks/types'; @@ -14,7 +14,7 @@ function useGroupMembershipRequests(groupId: string) { const { entity: relationship } = useGroupRelationship(groupId); - const { entities, invalidate, ...rest } = useEntities( + const { entities, invalidate, fetchEntities, ...rest } = useEntities( path, () => api.get(`/api/v1/groups/${groupId}/membership_requests`), { @@ -23,13 +23,13 @@ function useGroupMembershipRequests(groupId: string) { }, ); - const { incrementEntity: authorize } = useIncrementEntity(path, -1, async (accountId: string) => { + const { dismissEntity: authorize } = useDismissEntity(path, async (accountId: string) => { const response = await api.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/authorize`); invalidate(); return response; }); - const { incrementEntity: reject } = useIncrementEntity(path, -1, async (accountId: string) => { + const { dismissEntity: reject } = useDismissEntity(path, async (accountId: string) => { const response = await api.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/reject`); invalidate(); return response; @@ -37,6 +37,7 @@ function useGroupMembershipRequests(groupId: string) { return { accounts: entities, + refetch: fetchEntities, authorize, reject, ...rest, diff --git a/app/soapbox/api/hooks/groups/useGroupRelationship.ts b/app/soapbox/api/hooks/groups/useGroupRelationship.ts new file mode 100644 index 000000000..6b24c463c --- /dev/null +++ b/app/soapbox/api/hooks/groups/useGroupRelationship.ts @@ -0,0 +1,32 @@ +import { useEffect } from 'react'; +import { z } from 'zod'; + +import { fetchGroupRelationshipsSuccess } from 'soapbox/actions/groups'; +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntity } from 'soapbox/entity-store/hooks'; +import { useApi, useAppDispatch } from 'soapbox/hooks'; +import { type GroupRelationship, groupRelationshipSchema } from 'soapbox/schemas'; + +function useGroupRelationship(groupId: string) { + const api = useApi(); + const dispatch = useAppDispatch(); + + const { entity: groupRelationship, ...result } = useEntity( + [Entities.GROUP_RELATIONSHIPS, groupId], + () => api.get(`/api/v1/groups/relationships?id[]=${groupId}`), + { schema: z.array(groupRelationshipSchema).transform(arr => arr[0]) }, + ); + + useEffect(() => { + if (groupRelationship?.id) { + dispatch(fetchGroupRelationshipsSuccess([groupRelationship])); + } + }, [groupRelationship?.id]); + + return { + entity: groupRelationship, + ...result, + }; +} + +export { useGroupRelationship }; \ No newline at end of file diff --git a/app/soapbox/api/hooks/groups/useGroupRelationships.ts b/app/soapbox/api/hooks/groups/useGroupRelationships.ts new file mode 100644 index 000000000..c4106adda --- /dev/null +++ b/app/soapbox/api/hooks/groups/useGroupRelationships.ts @@ -0,0 +1,27 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntities } from 'soapbox/entity-store/hooks'; +import { useApi } from 'soapbox/hooks'; +import { type GroupRelationship, groupRelationshipSchema } from 'soapbox/schemas'; + +function useGroupRelationships(groupIds: string[]) { + const api = useApi(); + const q = groupIds.map(id => `id[]=${id}`).join('&'); + + const { entities, ...result } = useEntities( + [Entities.GROUP_RELATIONSHIPS, ...groupIds], + () => api.get(`/api/v1/groups/relationships?${q}`), + { schema: groupRelationshipSchema, enabled: groupIds.length > 0 }, + ); + + const relationships = entities.reduce>((map, relationship) => { + map[relationship.id] = relationship; + return map; + }, {}); + + return { + ...result, + relationships, + }; +} + +export { useGroupRelationships }; \ No newline at end of file diff --git a/app/soapbox/hooks/api/groups/useGroupSearch.ts b/app/soapbox/api/hooks/groups/useGroupSearch.ts similarity index 83% rename from app/soapbox/hooks/api/groups/useGroupSearch.ts rename to app/soapbox/api/hooks/groups/useGroupSearch.ts index 17c10e90f..d1d8acf9c 100644 --- a/app/soapbox/hooks/api/groups/useGroupSearch.ts +++ b/app/soapbox/api/hooks/groups/useGroupSearch.ts @@ -1,11 +1,9 @@ import { Entities } from 'soapbox/entity-store/entities'; import { useEntities } from 'soapbox/entity-store/hooks'; +import { useApi, useFeatures } from 'soapbox/hooks'; import { groupSchema } from 'soapbox/schemas'; -import { useApi } from '../../useApi'; -import { useFeatures } from '../../useFeatures'; - -import { useGroupRelationships } from './useGroups'; +import { useGroupRelationships } from './useGroupRelationships'; import type { Group } from 'soapbox/schemas'; diff --git a/app/soapbox/hooks/api/groups/useGroupTag.ts b/app/soapbox/api/hooks/groups/useGroupTag.ts similarity index 100% rename from app/soapbox/hooks/api/groups/useGroupTag.ts rename to app/soapbox/api/hooks/groups/useGroupTag.ts diff --git a/app/soapbox/hooks/api/groups/useGroupTags.ts b/app/soapbox/api/hooks/groups/useGroupTags.ts similarity index 100% rename from app/soapbox/hooks/api/groups/useGroupTags.ts rename to app/soapbox/api/hooks/groups/useGroupTags.ts diff --git a/app/soapbox/hooks/api/groups/useGroupValidation.ts b/app/soapbox/api/hooks/groups/useGroupValidation.ts similarity index 100% rename from app/soapbox/hooks/api/groups/useGroupValidation.ts rename to app/soapbox/api/hooks/groups/useGroupValidation.ts diff --git a/app/soapbox/api/hooks/groups/useGroups.ts b/app/soapbox/api/hooks/groups/useGroups.ts new file mode 100644 index 000000000..13ca45713 --- /dev/null +++ b/app/soapbox/api/hooks/groups/useGroups.ts @@ -0,0 +1,31 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntities } from 'soapbox/entity-store/hooks'; +import { useApi } from 'soapbox/hooks'; +import { useFeatures } from 'soapbox/hooks/useFeatures'; +import { groupSchema, type Group } from 'soapbox/schemas/group'; + +import { useGroupRelationships } from './useGroupRelationships'; + +function useGroups(q: string = '') { + const api = useApi(); + const features = useFeatures(); + + const { entities, ...result } = useEntities( + [Entities.GROUPS, 'search', q], + () => api.get('/api/v1/groups', { params: { q } }), + { enabled: features.groups, schema: groupSchema }, + ); + const { relationships } = useGroupRelationships(entities.map(entity => entity.id)); + + const groups = entities.map((group) => ({ + ...group, + relationship: relationships[group.id] || null, + })); + + return { + ...result, + groups, + }; +} + +export { useGroups }; diff --git a/app/soapbox/hooks/api/groups/useGroupsFromTag.ts b/app/soapbox/api/hooks/groups/useGroupsFromTag.ts similarity index 82% rename from app/soapbox/hooks/api/groups/useGroupsFromTag.ts rename to app/soapbox/api/hooks/groups/useGroupsFromTag.ts index a6b8540dc..2c7e5a94f 100644 --- a/app/soapbox/hooks/api/groups/useGroupsFromTag.ts +++ b/app/soapbox/api/hooks/groups/useGroupsFromTag.ts @@ -1,11 +1,9 @@ import { Entities } from 'soapbox/entity-store/entities'; import { useEntities } from 'soapbox/entity-store/hooks'; +import { useApi, useFeatures } from 'soapbox/hooks'; import { groupSchema } from 'soapbox/schemas'; -import { useApi } from '../../useApi'; -import { useFeatures } from '../../useFeatures'; - -import { useGroupRelationships } from './useGroups'; +import { useGroupRelationships } from './useGroupRelationships'; import type { Group } from 'soapbox/schemas'; diff --git a/app/soapbox/hooks/api/groups/useJoinGroup.ts b/app/soapbox/api/hooks/groups/useJoinGroup.ts similarity index 100% rename from app/soapbox/hooks/api/groups/useJoinGroup.ts rename to app/soapbox/api/hooks/groups/useJoinGroup.ts diff --git a/app/soapbox/hooks/api/groups/useLeaveGroup.ts b/app/soapbox/api/hooks/groups/useLeaveGroup.ts similarity index 100% rename from app/soapbox/hooks/api/groups/useLeaveGroup.ts rename to app/soapbox/api/hooks/groups/useLeaveGroup.ts diff --git a/app/soapbox/hooks/api/usePopularGroups.ts b/app/soapbox/api/hooks/groups/usePopularGroups.ts similarity index 78% rename from app/soapbox/hooks/api/usePopularGroups.ts rename to app/soapbox/api/hooks/groups/usePopularGroups.ts index 385322500..b5959a335 100644 --- a/app/soapbox/hooks/api/usePopularGroups.ts +++ b/app/soapbox/api/hooks/groups/usePopularGroups.ts @@ -2,9 +2,10 @@ import { Entities } from 'soapbox/entity-store/entities'; import { useEntities } from 'soapbox/entity-store/hooks'; import { Group, groupSchema } from 'soapbox/schemas'; -import { useGroupRelationships } from '../api/groups/useGroups'; -import { useApi } from '../useApi'; -import { useFeatures } from '../useFeatures'; +import { useApi } from '../../../hooks/useApi'; +import { useFeatures } from '../../../hooks/useFeatures'; + +import { useGroupRelationships } from './useGroupRelationships'; function usePopularGroups() { const api = useApi(); diff --git a/app/soapbox/hooks/api/groups/usePopularTags.ts b/app/soapbox/api/hooks/groups/usePopularTags.ts similarity index 73% rename from app/soapbox/hooks/api/groups/usePopularTags.ts rename to app/soapbox/api/hooks/groups/usePopularTags.ts index 0bd272a2d..e0ec2c550 100644 --- a/app/soapbox/hooks/api/groups/usePopularTags.ts +++ b/app/soapbox/api/hooks/groups/usePopularTags.ts @@ -1,9 +1,7 @@ import { Entities } from 'soapbox/entity-store/entities'; import { useEntities } from 'soapbox/entity-store/hooks'; -import { GroupTag, groupTagSchema } from 'soapbox/schemas'; - -import { useApi } from '../../useApi'; -import { useFeatures } from '../../useFeatures'; +import { useApi, useFeatures } from 'soapbox/hooks'; +import { type GroupTag, groupTagSchema } from 'soapbox/schemas'; function usePopularTags() { const api = useApi(); diff --git a/app/soapbox/hooks/api/groups/usePromoteGroupMember.ts b/app/soapbox/api/hooks/groups/usePromoteGroupMember.ts similarity index 100% rename from app/soapbox/hooks/api/groups/usePromoteGroupMember.ts rename to app/soapbox/api/hooks/groups/usePromoteGroupMember.ts diff --git a/app/soapbox/hooks/api/useSuggestedGroups.ts b/app/soapbox/api/hooks/groups/useSuggestedGroups.ts similarity index 75% rename from app/soapbox/hooks/api/useSuggestedGroups.ts rename to app/soapbox/api/hooks/groups/useSuggestedGroups.ts index 49f60c2b1..be9b5a78e 100644 --- a/app/soapbox/hooks/api/useSuggestedGroups.ts +++ b/app/soapbox/api/hooks/groups/useSuggestedGroups.ts @@ -1,10 +1,9 @@ import { Entities } from 'soapbox/entity-store/entities'; import { useEntities } from 'soapbox/entity-store/hooks'; -import { Group, groupSchema } from 'soapbox/schemas'; +import { useApi, useFeatures } from 'soapbox/hooks'; +import { type Group, groupSchema } from 'soapbox/schemas'; -import { useGroupRelationships } from '../api/groups/useGroups'; -import { useApi } from '../useApi'; -import { useFeatures } from '../useFeatures'; +import { useGroupRelationships } from './useGroupRelationships'; function useSuggestedGroups() { const api = useApi(); diff --git a/app/soapbox/hooks/api/groups/useUpdateGroup.ts b/app/soapbox/api/hooks/groups/useUpdateGroup.ts similarity index 100% rename from app/soapbox/hooks/api/groups/useUpdateGroup.ts rename to app/soapbox/api/hooks/groups/useUpdateGroup.ts diff --git a/app/soapbox/hooks/api/groups/useUpdateGroupTag.ts b/app/soapbox/api/hooks/groups/useUpdateGroupTag.ts similarity index 100% rename from app/soapbox/hooks/api/groups/useUpdateGroupTag.ts rename to app/soapbox/api/hooks/groups/useUpdateGroupTag.ts diff --git a/app/soapbox/hooks/api/index.ts b/app/soapbox/api/hooks/index.ts similarity index 67% rename from app/soapbox/hooks/api/index.ts rename to app/soapbox/api/hooks/index.ts index c8e1f67c3..ade03f799 100644 --- a/app/soapbox/hooks/api/index.ts +++ b/app/soapbox/api/hooks/index.ts @@ -1,7 +1,8 @@ + /** * Accounts */ -export { useAccount } from './useAccount'; +export { useAccount } from './accounts/useAccount'; /** * Groups @@ -11,22 +12,29 @@ export { useCancelMembershipRequest } from './groups/useCancelMembershipRequest' export { useCreateGroup, type CreateGroupParams } from './groups/useCreateGroup'; export { useDeleteGroup } from './groups/useDeleteGroup'; export { useDemoteGroupMember } from './groups/useDemoteGroupMember'; -export { useGroup, useGroups } from './groups/useGroups'; +export { useGroup } from './groups/useGroup'; +export { useGroupLookup } from './groups/useGroupLookup'; export { useGroupMedia } from './groups/useGroupMedia'; +export { useGroupMembers } from './groups/useGroupMembers'; export { useGroupMembershipRequests } from './groups/useGroupMembershipRequests'; +export { useGroupRelationship } from './groups/useGroupRelationship'; +export { useGroupRelationships } from './groups/useGroupRelationships'; export { useGroupSearch } from './groups/useGroupSearch'; export { useGroupTag } from './groups/useGroupTag'; export { useGroupTags } from './groups/useGroupTags'; export { useGroupValidation } from './groups/useGroupValidation'; +export { useGroups } from './groups/useGroups'; export { useGroupsFromTag } from './groups/useGroupsFromTag'; export { useJoinGroup } from './groups/useJoinGroup'; export { useLeaveGroup } from './groups/useLeaveGroup'; +export { usePopularGroups } from './groups/usePopularGroups'; export { usePopularTags } from './groups/usePopularTags'; export { usePromoteGroupMember } from './groups/usePromoteGroupMember'; +export { useSuggestedGroups } from './groups/useSuggestedGroups'; export { useUpdateGroup } from './groups/useUpdateGroup'; export { useUpdateGroupTag } from './groups/useUpdateGroupTag'; /** * Relationships */ -export { useRelationships } from './useRelationships'; +export { useRelationships } from './accounts/useRelationships'; \ No newline at end of file diff --git a/app/soapbox/components/authorize-reject-buttons.tsx b/app/soapbox/components/authorize-reject-buttons.tsx index a5fddc61a..5dfb37a31 100644 --- a/app/soapbox/components/authorize-reject-buttons.tsx +++ b/app/soapbox/components/authorize-reject-buttons.tsx @@ -51,7 +51,7 @@ const AuthorizeRejectButtons: React.FC = ({ onAuthorize await action(); setState(past); } catch (e) { - console.error(e); + if (e) console.error(e); } }; if (typeof countdown === 'number') { diff --git a/app/soapbox/components/birthday-panel.tsx b/app/soapbox/components/birthday-panel.tsx index 059b8678b..bcfe5d073 100644 --- a/app/soapbox/components/birthday-panel.tsx +++ b/app/soapbox/components/birthday-panel.tsx @@ -58,6 +58,7 @@ const BirthdayPanel = ({ limit }: IBirthdayPanel) => { key={accountId} // @ts-ignore: TS thinks `id` is passed to , but it isn't id={accountId} + withRelationship={false} /> ))} diff --git a/app/soapbox/components/dropdown-menu/dropdown-menu-item.tsx b/app/soapbox/components/dropdown-menu/dropdown-menu-item.tsx index 97c2ff045..6ee5a3aee 100644 --- a/app/soapbox/components/dropdown-menu/dropdown-menu-item.tsx +++ b/app/soapbox/components/dropdown-menu/dropdown-menu-item.tsx @@ -93,7 +93,7 @@ const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => { > {item.icon && } - {item.text} + {item.text} {item.count ? ( diff --git a/app/soapbox/components/hoc/group-lookup-hoc.tsx b/app/soapbox/components/hoc/group-lookup-hoc.tsx index a7c76eed6..07c2d475b 100644 --- a/app/soapbox/components/hoc/group-lookup-hoc.tsx +++ b/app/soapbox/components/hoc/group-lookup-hoc.tsx @@ -1,7 +1,7 @@ import React from 'react'; +import { useGroupLookup } from 'soapbox/api/hooks'; import ColumnLoading from 'soapbox/features/ui/components/column-loading'; -import { useGroupLookup } from 'soapbox/hooks/api/groups/useGroupLookup'; import { Layout } from '../ui'; diff --git a/app/soapbox/components/list.tsx b/app/soapbox/components/list.tsx index e1b8cdcd9..b56e0e6a7 100644 --- a/app/soapbox/components/list.tsx +++ b/app/soapbox/components/list.tsx @@ -56,8 +56,7 @@ const ListItem: React.FC = ({ label, hint, children, onClick, onSelec return ( = ({ label, hint, children, onClick, onSelec {onClick ? ( - + {children} diff --git a/app/soapbox/components/polls/__tests__/poll-footer.test.tsx b/app/soapbox/components/polls/__tests__/poll-footer.test.tsx index 29c841a0a..a9e709399 100644 --- a/app/soapbox/components/polls/__tests__/poll-footer.test.tsx +++ b/app/soapbox/components/polls/__tests__/poll-footer.test.tsx @@ -4,14 +4,22 @@ import { IntlProvider } from 'react-intl'; import { Provider } from 'react-redux'; import { __stub } from 'soapbox/api'; -import { normalizePoll } from 'soapbox/normalizers/poll'; +import { mockStore, render, screen, rootState } from 'soapbox/jest/test-helpers'; +import { type Poll } from 'soapbox/schemas'; -import { mockStore, render, screen, rootState } from '../../../jest/test-helpers'; import PollFooter from '../poll-footer'; -let poll = normalizePoll({ - id: 1, - options: [{ title: 'Apples', votes_count: 0 }], +let poll: Poll = { + id: '1', + options: [{ + title: 'Apples', + votes_count: 0, + title_emojified: 'Apples', + }, { + title: 'Oranges', + votes_count: 0, + title_emojified: 'Oranges', + }], emojis: [], expired: false, expires_at: '2020-03-24T19:33:06.000Z', @@ -20,7 +28,7 @@ let poll = normalizePoll({ votes_count: 0, own_votes: null, voted: false, -}); +}; describe('', () => { describe('with "showResults" enabled', () => { @@ -62,10 +70,10 @@ describe('', () => { describe('when the Poll has not expired', () => { beforeEach(() => { - poll = normalizePoll({ - ...poll.toJS(), + poll = { + ...poll, expired: false, - }); + }; }); it('renders time remaining', () => { @@ -77,10 +85,10 @@ describe('', () => { describe('when the Poll has expired', () => { beforeEach(() => { - poll = normalizePoll({ - ...poll.toJS(), + poll = { + ...poll, expired: true, - }); + }; }); it('renders closed', () => { @@ -100,10 +108,10 @@ describe('', () => { describe('when the Poll is multiple', () => { beforeEach(() => { - poll = normalizePoll({ - ...poll.toJS(), + poll = { + ...poll, multiple: true, - }); + }; }); it('renders the Vote button', () => { @@ -115,10 +123,10 @@ describe('', () => { describe('when the Poll is not multiple', () => { beforeEach(() => { - poll = normalizePoll({ - ...poll.toJS(), + poll = { + ...poll, multiple: false, - }); + }; }); it('does not render the Vote button', () => { diff --git a/app/soapbox/components/polls/poll-footer.tsx b/app/soapbox/components/polls/poll-footer.tsx index c62cc9522..1994c1e76 100644 --- a/app/soapbox/components/polls/poll-footer.tsx +++ b/app/soapbox/components/polls/poll-footer.tsx @@ -40,21 +40,21 @@ const PollFooter: React.FC = ({ poll, showResults, selected }): JSX let votesCount = null; if (poll.voters_count !== null && poll.voters_count !== undefined) { - votesCount = ; + votesCount = ; } else { - votesCount = ; + votesCount = ; } return ( - {(!showResults && poll?.multiple) && ( + {(!showResults && poll.multiple) && ( )} - {poll.pleroma.get('non_anonymous') && ( + {poll.pleroma?.non_anonymous && ( <> diff --git a/app/soapbox/components/polls/poll-option.tsx b/app/soapbox/components/polls/poll-option.tsx index 792a3a066..b4c37e11d 100644 --- a/app/soapbox/components/polls/poll-option.tsx +++ b/app/soapbox/components/polls/poll-option.tsx @@ -112,10 +112,13 @@ const PollOption: React.FC = (props): JSX.Element | null => { const pollVotesCount = poll.voters_count || poll.votes_count; const percent = pollVotesCount === 0 ? 0 : (option.votes_count / pollVotesCount) * 100; - const leading = poll.options.filterNot(other => other.title === option.title).every(other => option.votes_count >= other.votes_count); const voted = poll.own_votes?.includes(index); const message = intl.formatMessage(messages.votes, { votes: option.votes_count }); + const leading = poll.options + .filter(other => other.title !== option.title) + .every(other => option.votes_count >= other.votes_count); + return (
{showResults ? ( diff --git a/app/soapbox/components/status-action-bar.tsx b/app/soapbox/components/status-action-bar.tsx index 585cc32d9..13e1a4a3c 100644 --- a/app/soapbox/components/status-action-bar.tsx +++ b/app/soapbox/components/status-action-bar.tsx @@ -7,18 +7,20 @@ import { blockAccount } from 'soapbox/actions/accounts'; import { launchChat } from 'soapbox/actions/chats'; import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'soapbox/actions/compose'; import { editEvent } from 'soapbox/actions/events'; -import { groupBlock, groupDeleteStatus, groupKick } from 'soapbox/actions/groups'; import { toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog } from 'soapbox/actions/interactions'; import { openModal } from 'soapbox/actions/modals'; import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation'; import { initMuteModal } from 'soapbox/actions/mutes'; import { initReport, ReportableEntities } from 'soapbox/actions/reports'; import { deleteStatus, editStatus, toggleMuteStatus } from 'soapbox/actions/statuses'; +import { deleteFromTimelines } from 'soapbox/actions/timelines'; +import { useDeleteGroupStatus } from 'soapbox/api/hooks/groups/useDeleteGroupStatus'; import DropdownMenu from 'soapbox/components/dropdown-menu'; import StatusActionButton from 'soapbox/components/status-action-button'; import StatusReactionWrapper from 'soapbox/components/status-reaction-wrapper'; import { HStack } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks'; +import { GroupRoles } from 'soapbox/schemas/group-member'; import toast from 'soapbox/toast'; import { isLocal, isRemote } from 'soapbox/utils/accounts'; import copy from 'soapbox/utils/copy'; @@ -87,16 +89,7 @@ const messages = defineMessages({ blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' }, replies_disabled_group: { id: 'status.disabled_replies.group_membership', defaultMessage: 'Only group members can reply' }, groupModDelete: { id: 'status.group_mod_delete', defaultMessage: 'Delete post from group' }, - groupModKick: { id: 'status.group_mod_kick', defaultMessage: 'Kick @{name} from group' }, - groupModBlock: { id: 'status.group_mod_block', defaultMessage: 'Block @{name} from group' }, - deleteFromGroupHeading: { id: 'confirmations.delete_from_group.heading', defaultMessage: 'Delete from group' }, deleteFromGroupMessage: { id: 'confirmations.delete_from_group.message', defaultMessage: 'Are you sure you want to delete @{name}\'s post?' }, - kickFromGroupHeading: { id: 'confirmations.kick_from_group.heading', defaultMessage: 'Kick group member' }, - kickFromGroupMessage: { id: 'confirmations.kick_from_group.message', defaultMessage: 'Are you sure you want to kick @{name} from this group?' }, - kickFromGroupConfirm: { id: 'confirmations.kick_from_group.confirm', defaultMessage: 'Kick' }, - blockFromGroupHeading: { id: 'confirmations.block_from_group.heading', defaultMessage: 'Block group member' }, - blockFromGroupMessage: { id: 'confirmations.block_from_group.message', defaultMessage: 'Are you sure you want to block @{name} from interacting with this group?' }, - blockFromGroupConfirm: { id: 'confirmations.block_from_group.confirm', defaultMessage: 'Block' }, }); interface IStatusActionBar { @@ -121,6 +114,7 @@ const StatusActionBar: React.FC = ({ const features = useFeatures(); const settings = useSettings(); const soapboxConfig = useSoapboxConfig(); + const deleteGroupStatus = useDeleteGroupStatus(status?.group as Group, status.id); const { allowedEmoji } = soapboxConfig; @@ -258,8 +252,8 @@ const StatusActionBar: React.FC = ({ dispatch(openModal('CONFIRM', { icon: require('@tabler/icons/ban.svg'), - heading: , - message: @{account.get('acct')} }} />, + heading: , + message: @{account.acct} }} />, confirm: intl.formatMessage(messages.blockConfirm), onConfirm: () => dispatch(blockAccount(account.id)), secondary: intl.formatMessage(messages.blockAndReport), @@ -313,31 +307,15 @@ const StatusActionBar: React.FC = ({ dispatch(openModal('CONFIRM', { heading: intl.formatMessage(messages.deleteHeading), - message: intl.formatMessage(messages.deleteFromGroupMessage, { name: account.username }), + message: intl.formatMessage(messages.deleteFromGroupMessage, { name: {account.username} }), confirm: intl.formatMessage(messages.deleteConfirm), - onConfirm: () => dispatch(groupDeleteStatus((status.group as Group).id, status.id)), - })); - }; - - const handleKickFromGroup: React.EventHandler = () => { - const account = status.account as Account; - - dispatch(openModal('CONFIRM', { - heading: intl.formatMessage(messages.kickFromGroupHeading), - message: intl.formatMessage(messages.kickFromGroupMessage, { name: account.username }), - confirm: intl.formatMessage(messages.kickFromGroupConfirm), - onConfirm: () => dispatch(groupKick((status.group as Group).id, account.id)), - })); - }; - - const handleBlockFromGroup: React.EventHandler = () => { - const account = status.account as Account; - - dispatch(openModal('CONFIRM', { - heading: intl.formatMessage(messages.blockFromGroupHeading), - message: intl.formatMessage(messages.blockFromGroupMessage, { name: account.username }), - confirm: intl.formatMessage(messages.blockFromGroupConfirm), - onConfirm: () => dispatch(groupBlock((status.group as Group).id, account.id)), + onConfirm: () => { + deleteGroupStatus.mutate(status.id, { + onSuccess() { + dispatch(deleteFromTimelines(status.id)); + }, + }); + }, })); }; @@ -362,7 +340,7 @@ const StatusActionBar: React.FC = ({ menu.push({ text: intl.formatMessage(messages.copy), action: handleCopy, - icon: require('@tabler/icons/link.svg'), + icon: require('@tabler/icons/clipboard-copy.svg'), }); if (features.embeds && isLocal(account)) { @@ -466,7 +444,7 @@ const StatusActionBar: React.FC = ({ menu.push({ text: intl.formatMessage(messages.mute, { name: username }), action: handleMuteClick, - icon: require('@tabler/icons/circle-x.svg'), + icon: require('@tabler/icons/volume-3.svg'), }); menu.push({ text: intl.formatMessage(messages.block, { name: username }), @@ -480,23 +458,17 @@ const StatusActionBar: React.FC = ({ }); } - if (status.group && groupRelationship?.role && ['admin', 'moderator'].includes(groupRelationship.role)) { + if (status.group && + groupRelationship?.role && + [GroupRoles.OWNER].includes(groupRelationship.role) && + !ownAccount + ) { menu.push(null); menu.push({ text: intl.formatMessage(messages.groupModDelete), action: handleDeleteFromGroup, icon: require('@tabler/icons/trash.svg'), - }); - // TODO: figure out when an account is not in the group anymore - menu.push({ - text: intl.formatMessage(messages.groupModKick, { name: account.get('username') }), - action: handleKickFromGroup, - icon: require('@tabler/icons/user-minus.svg'), - }); - menu.push({ - text: intl.formatMessage(messages.groupModBlock, { name: account.get('username') }), - action: handleBlockFromGroup, - icon: require('@tabler/icons/ban.svg'), + destructive: true, }); } diff --git a/app/soapbox/components/status-reply-mentions.tsx b/app/soapbox/components/status-reply-mentions.tsx index 61f2f2969..e03d0c7f7 100644 --- a/app/soapbox/components/status-reply-mentions.tsx +++ b/app/soapbox/components/status-reply-mentions.tsx @@ -54,7 +54,7 @@ const StatusReplyMentions: React.FC = ({ status, hoverable e.stopPropagation()} > @{isPubkey(account.username) ? account.username.slice(0, 8) : account.username} diff --git a/app/soapbox/components/ui/modal/modal.tsx b/app/soapbox/components/ui/modal/modal.tsx index 98b001f4c..d7f29303f 100644 --- a/app/soapbox/components/ui/modal/modal.tsx +++ b/app/soapbox/components/ui/modal/modal.tsx @@ -102,7 +102,7 @@ const Modal = React.forwardRef(({ 'flex-row-reverse': closePosition === 'left', })} > -

+

{title}

diff --git a/app/soapbox/containers/group-container.tsx b/app/soapbox/containers/group-container.tsx deleted file mode 100644 index f1254b2ca..000000000 --- a/app/soapbox/containers/group-container.tsx +++ /dev/null @@ -1,24 +0,0 @@ -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/account/components/header.tsx b/app/soapbox/features/account/components/header.tsx index 4a589a328..fbdfc3181 100644 --- a/app/soapbox/features/account/components/header.tsx +++ b/app/soapbox/features/account/components/header.tsx @@ -130,7 +130,7 @@ const Header: React.FC = ({ account }) => { dispatch(openModal('CONFIRM', { icon: require('@tabler/icons/ban.svg'), heading: , - message: @{account.acct} }} />, + message: @{account.acct} }} />, confirm: intl.formatMessage(messages.blockConfirm), onConfirm: () => dispatch(blockAccount(account.id)), secondary: intl.formatMessage(messages.blockAndReport), @@ -215,7 +215,7 @@ const Header: React.FC = ({ account }) => { const unfollowModal = getSettings(getState()).get('unfollowModal'); if (unfollowModal) { dispatch(openModal('CONFIRM', { - message: @{account.acct} }} />, + message: @{account.acct} }} />, confirm: intl.formatMessage(messages.removeFromFollowersConfirm), onConfirm: () => dispatch(removeFromFollowers(account.id)), })); diff --git a/app/soapbox/features/ads/providers/index.ts b/app/soapbox/features/ads/providers/index.ts index 8ff7d5219..63067b81d 100644 --- a/app/soapbox/features/ads/providers/index.ts +++ b/app/soapbox/features/ads/providers/index.ts @@ -6,7 +6,6 @@ import type { Card } from 'soapbox/types/entities'; /** Map of available provider modules. */ const PROVIDERS: Record Promise> = { soapbox: async() => (await import(/* webpackChunkName: "features/ads/soapbox" */'./soapbox-config')).default, - rumble: async() => (await import(/* webpackChunkName: "features/ads/rumble" */'./rumble')).default, truth: async() => (await import(/* webpackChunkName: "features/ads/truth" */'./truth')).default, }; diff --git a/app/soapbox/features/ads/providers/rumble.ts b/app/soapbox/features/ads/providers/rumble.ts deleted file mode 100644 index 21dc6e7f3..000000000 --- a/app/soapbox/features/ads/providers/rumble.ts +++ /dev/null @@ -1,58 +0,0 @@ -import axios from 'axios'; - -import { getSettings } from 'soapbox/actions/settings'; -import { getSoapboxConfig } from 'soapbox/actions/soapbox'; -import { normalizeAd, normalizeCard } from 'soapbox/normalizers'; - -import type { AdProvider } from '.'; - -/** Rumble ad API entity. */ -interface RumbleAd { - type: number - impression: string - click: string - asset: string - expires: number -} - -/** Response from Rumble ad server. */ -interface RumbleApiResponse { - count: number - ads: RumbleAd[] -} - -/** Provides ads from Soapbox Config. */ -const RumbleAdProvider: AdProvider = { - getAds: async(getState) => { - const state = getState(); - const settings = getSettings(state); - const soapboxConfig = getSoapboxConfig(state); - const endpoint = soapboxConfig.extensions.getIn(['ads', 'endpoint']) as string | undefined; - - if (endpoint) { - try { - const { data } = await axios.get(endpoint, { - headers: { - 'Accept-Language': settings.get('locale', '*') as string, - }, - }); - - return data.ads.map(item => normalizeAd({ - impression: item.impression, - card: normalizeCard({ - type: item.type === 1 ? 'link' : 'rich', - image: item.asset, - url: item.click, - }), - expires_at: new Date(item.expires * 1000), - })); - } catch (e) { - // do nothing - } - } - - return []; - }, -}; - -export default RumbleAdProvider; diff --git a/app/soapbox/features/ads/providers/truth.ts b/app/soapbox/features/ads/providers/truth.ts index 9207db522..5582bd3cf 100644 --- a/app/soapbox/features/ads/providers/truth.ts +++ b/app/soapbox/features/ads/providers/truth.ts @@ -1,18 +1,19 @@ import axios from 'axios'; +import { z } from 'zod'; import { getSettings } from 'soapbox/actions/settings'; -import { normalizeCard } from 'soapbox/normalizers'; +import { cardSchema } from 'soapbox/schemas/card'; +import { filteredArray } from 'soapbox/schemas/utils'; import type { AdProvider } from '.'; -import type { Card } from 'soapbox/types/entities'; /** TruthSocial ad API entity. */ -interface TruthAd { - impression: string - card: Card - expires_at: string - reason: string -} +const truthAdSchema = z.object({ + impression: z.string(), + card: cardSchema, + expires_at: z.string(), + reason: z.string().catch(''), +}); /** Provides ads from the TruthSocial API. */ const TruthAdProvider: AdProvider = { @@ -21,16 +22,13 @@ const TruthAdProvider: AdProvider = { const settings = getSettings(state); try { - const { data } = await axios.get('/api/v2/truth/ads?device=desktop', { + const { data } = await axios.get('/api/v2/truth/ads?device=desktop', { headers: { - 'Accept-Language': settings.get('locale', '*') as string, + 'Accept-Language': z.string().catch('*').parse(settings.get('locale')), }, }); - return data.map(item => ({ - ...item, - card: normalizeCard(item.card), - })); + return filteredArray(truthAdSchema).parse(data); } catch (e) { // do nothing } diff --git a/app/soapbox/features/chats/components/__tests__/chat-message-reaction.test.tsx b/app/soapbox/features/chats/components/__tests__/chat-message-reaction.test.tsx index 45fac5383..6ab22d4d5 100644 --- a/app/soapbox/features/chats/components/__tests__/chat-message-reaction.test.tsx +++ b/app/soapbox/features/chats/components/__tests__/chat-message-reaction.test.tsx @@ -1,12 +1,10 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; -import { normalizeEmojiReaction } from 'soapbox/normalizers/emoji-reaction'; - import { render, screen } from '../../../../jest/test-helpers'; import ChatMessageReaction from '../chat-message-reaction'; -const emojiReaction = normalizeEmojiReaction({ +const emojiReaction = ({ name: '👍', count: 1, me: false, @@ -56,7 +54,7 @@ describe('', () => { render( { - {(chatMessage.emoji_reactions?.size) ? ( + {(chatMessage.emoji_reactions?.length) ? (
= ({ className, status, hideActi hideActions={hideActions} /> - { const intl = useIntl(); const dispatch = useAppDispatch(); - const features = useFeatures(); const value = useAppSelector((state) => state.search.submittedValue); const results = useAppSelector((state) => state.search.results); @@ -66,14 +62,6 @@ const SearchResults = () => { }, ); - if (features.groups) items.push( - { - text: intl.formatMessage(messages.groups), - action: () => selectFilter('groups'), - name: 'groups', - }, - ); - items.push( { text: intl.formatMessage(messages.hashtags), @@ -186,31 +174,6 @@ 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 = null; - } else if (loaded) { - noResultsMessage = ( -
- -
- ); - } - } - if (selectedFilter === 'hashtags') { hasMore = results.hashtagsHasMore; loaded = results.hashtagsLoaded; @@ -238,11 +201,11 @@ const SearchResults = () => { {filterByAccount ? ( - + {account} }} /> diff --git a/app/soapbox/features/edit-profile/index.tsx b/app/soapbox/features/edit-profile/index.tsx index 8483a171e..f45d75afa 100644 --- a/app/soapbox/features/edit-profile/index.tsx +++ b/app/soapbox/features/edit-profile/index.tsx @@ -124,7 +124,7 @@ const accountToCredentials = (account: Account): AccountCredentials => { discoverable: account.discoverable, bot: account.bot, display_name: account.display_name, - note: account.source.get('note'), + note: account.source.get('note', ''), locked: account.locked, fields_attributes: [...account.source.get>('fields', ImmutableList()).toJS()], stranger_notifications: account.getIn(['pleroma', 'notification_settings', 'block_from_strangers']) === true, diff --git a/app/soapbox/features/feed-filtering/feed-carousel.tsx b/app/soapbox/features/feed-filtering/feed-carousel.tsx index 9d62538ce..f437aad0c 100644 --- a/app/soapbox/features/feed-filtering/feed-carousel.tsx +++ b/app/soapbox/features/feed-filtering/feed-carousel.tsx @@ -207,7 +207,7 @@ const FeedCarousel = () => { style={{ width: widthPerAvatar || 'auto' }} key={idx} > - +
)) ) : ( diff --git a/app/soapbox/features/group/components/__tests__/group-action-button.test.tsx b/app/soapbox/features/group/components/__tests__/group-action-button.test.tsx index 6809ea009..a0df6affe 100644 --- a/app/soapbox/features/group/components/__tests__/group-action-button.test.tsx +++ b/app/soapbox/features/group/components/__tests__/group-action-button.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { buildGroup, buildGroupRelationship } from 'soapbox/jest/factory'; import { render, screen } from 'soapbox/jest/test-helpers'; +import { GroupRoles } from 'soapbox/schemas/group-member'; import { Group } from 'soapbox/types/entities'; import GroupActionButton from '../group-action-button'; @@ -45,7 +46,7 @@ describe('', () => { beforeEach(() => { group = buildGroup({ relationship: buildGroupRelationship({ - member: null, + member: false, }), }); }); @@ -98,7 +99,7 @@ describe('', () => { relationship: buildGroupRelationship({ requested: false, member: true, - role: 'owner', + role: GroupRoles.OWNER, }), }); }); @@ -116,7 +117,7 @@ describe('', () => { relationship: buildGroupRelationship({ requested: false, member: true, - role: 'user', + role: GroupRoles.USER, }), }); }); diff --git a/app/soapbox/features/group/components/__tests__/group-header.test.tsx b/app/soapbox/features/group/components/__tests__/group-header.test.tsx new file mode 100644 index 000000000..03f171e14 --- /dev/null +++ b/app/soapbox/features/group/components/__tests__/group-header.test.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +import { buildGroup } from 'soapbox/jest/factory'; +import { render, screen } from 'soapbox/jest/test-helpers'; +import { Group } from 'soapbox/types/entities'; + +import GroupHeader from '../group-header'; + +let group: Group; + +describe('', () => { + describe('without a group', () => { + it('should render the blankslate', () => { + render(); + expect(screen.getByTestId('group-header-missing')).toBeInTheDocument(); + }); + }); + + describe('when the Group has been deleted', () => { + it('only shows name, header, and avatar', () => { + group = buildGroup({ display_name: 'my group', deleted_at: new Date().toISOString() }); + render(); + + expect(screen.queryAllByTestId('group-header-missing')).toHaveLength(0); + expect(screen.queryAllByTestId('group-actions')).toHaveLength(0); + expect(screen.queryAllByTestId('group-meta')).toHaveLength(0); + expect(screen.getByTestId('group-header-image')).toBeInTheDocument(); + expect(screen.getByTestId('group-avatar')).toBeInTheDocument(); + expect(screen.getByTestId('group-name')).toBeInTheDocument(); + }); + }); + + describe('with a valid Group', () => { + it('only shows all fields', () => { + group = buildGroup({ display_name: 'my group', deleted_at: null }); + render(); + + expect(screen.queryAllByTestId('group-header-missing')).toHaveLength(0); + expect(screen.getByTestId('group-actions')).toBeInTheDocument(); + expect(screen.getByTestId('group-meta')).toBeInTheDocument(); + expect(screen.getByTestId('group-header-image')).toBeInTheDocument(); + expect(screen.getByTestId('group-avatar')).toBeInTheDocument(); + expect(screen.getByTestId('group-name')).toBeInTheDocument(); + }); + }); +}); \ No newline at end of file diff --git a/app/soapbox/features/group/components/__tests__/group-member-list-item.test.tsx b/app/soapbox/features/group/components/__tests__/group-member-list-item.test.tsx new file mode 100644 index 000000000..abecc3287 --- /dev/null +++ b/app/soapbox/features/group/components/__tests__/group-member-list-item.test.tsx @@ -0,0 +1,320 @@ +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { __stub } from 'soapbox/api'; +import { buildGroup, buildGroupMember, buildGroupRelationship } from 'soapbox/jest/factory'; +import { render, screen, waitFor } from 'soapbox/jest/test-helpers'; +import { GroupRoles } from 'soapbox/schemas/group-member'; + +import GroupMemberListItem from '../group-member-list-item'; + +describe('', () => { + describe('account rendering', () => { + const accountId = '4'; + const groupMember = buildGroupMember({}, { + id: accountId, + display_name: 'tiger woods', + }); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + it('should render the users avatar', async () => { + const group = buildGroup({ + relationship: buildGroupRelationship(), + }); + + render(); + + await waitFor(() => { + expect(screen.getByTestId('group-member-list-item')).toHaveTextContent(groupMember.account.display_name); + }); + }); + }); + + describe('role badge', () => { + const accountId = '4'; + const group = buildGroup(); + + describe('when the user is an Owner', () => { + const groupMember = buildGroupMember({ role: GroupRoles.OWNER }, { + id: accountId, + display_name: 'tiger woods', + }); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + it('should render the correct badge', async () => { + render(); + + await waitFor(() => { + expect(screen.getByTestId('role-badge')).toHaveTextContent('owner'); + }); + }); + }); + + describe('when the user is an Admin', () => { + const groupMember = buildGroupMember({ role: GroupRoles.ADMIN }, { + id: accountId, + display_name: 'tiger woods', + }); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + it('should render the correct badge', async () => { + render(); + + await waitFor(() => { + expect(screen.getByTestId('role-badge')).toHaveTextContent('admin'); + }); + }); + }); + + describe('when the user is an User', () => { + const groupMember = buildGroupMember({ role: GroupRoles.USER }, { + id: accountId, + display_name: 'tiger woods', + }); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + it('should render no correct badge', async () => { + render(); + + await waitFor(() => { + expect(screen.queryAllByTestId('role-badge')).toHaveLength(0); + }); + }); + }); + }); + + describe('as a Group owner', () => { + const group = buildGroup({ + relationship: buildGroupRelationship({ + role: GroupRoles.OWNER, + member: true, + }), + }); + + describe('when the user has role of "user"', () => { + const accountId = '4'; + const groupMember = buildGroupMember({}, { + id: accountId, + display_name: 'tiger woods', + username: 'tiger', + }); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + describe('when "canPromoteToAdmin is true', () => { + it('should render dropdown with correct Owner actions', async () => { + const user = userEvent.setup(); + + render(); + + await waitFor(async() => { + await user.click(screen.getByTestId('icon-button')); + }); + + const dropdownMenu = screen.getByTestId('dropdown-menu'); + expect(dropdownMenu).toHaveTextContent('Assign admin role'); + expect(dropdownMenu).toHaveTextContent('Kick @tiger from group'); + expect(dropdownMenu).toHaveTextContent('Ban from group'); + }); + }); + + describe('when "canPromoteToAdmin is false', () => { + it('should prevent promoting user to Admin', async () => { + const user = userEvent.setup(); + + render(); + + await waitFor(async() => { + await user.click(screen.getByTestId('icon-button')); + await user.click(screen.getByTitle('Assign admin role')); + }); + + expect(screen.getByTestId('toast')).toHaveTextContent('Admin limit reached'); + }); + }); + }); + + describe('when the user has role of "admin"', () => { + const accountId = '4'; + const groupMember = buildGroupMember( + { + role: GroupRoles.ADMIN, + }, + { + id: accountId, + display_name: 'tiger woods', + username: 'tiger', + }, + ); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + it('should render dropdown with correct Owner actions', async () => { + const user = userEvent.setup(); + + render(); + + await waitFor(async() => { + await user.click(screen.getByTestId('icon-button')); + }); + + const dropdownMenu = screen.getByTestId('dropdown-menu'); + expect(dropdownMenu).toHaveTextContent('Remove admin role'); + expect(dropdownMenu).toHaveTextContent('Kick @tiger from group'); + expect(dropdownMenu).toHaveTextContent('Ban from group'); + }); + }); + }); + + describe('as a Group admin', () => { + const group = buildGroup({ + relationship: buildGroupRelationship({ + role: GroupRoles.ADMIN, + member: true, + }), + }); + + describe('when the user has role of "user"', () => { + const accountId = '4'; + const groupMember = buildGroupMember({}, { + id: accountId, + display_name: 'tiger woods', + username: 'tiger', + }); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + it('should render dropdown with correct Admin actions', async () => { + const user = userEvent.setup(); + + render(); + + await waitFor(async() => { + await user.click(screen.getByTestId('icon-button')); + }); + + const dropdownMenu = screen.getByTestId('dropdown-menu'); + expect(dropdownMenu).not.toHaveTextContent('Assign admin role'); + expect(dropdownMenu).toHaveTextContent('Kick @tiger from group'); + expect(dropdownMenu).toHaveTextContent('Ban from group'); + }); + }); + + describe('when the user has role of "admin"', () => { + const accountId = '4'; + const groupMember = buildGroupMember( + { + role: GroupRoles.ADMIN, + }, + { + id: accountId, + display_name: 'tiger woods', + username: 'tiger', + }, + ); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + it('should not render the dropdown', async () => { + render(); + + await waitFor(async() => { + expect(screen.queryAllByTestId('icon-button')).toHaveLength(0); + }); + }); + }); + + describe('when the user has role of "owner"', () => { + const accountId = '4'; + const groupMember = buildGroupMember( + { + role: GroupRoles.OWNER, + }, + { + id: accountId, + display_name: 'tiger woods', + username: 'tiger', + }, + ); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + it('should not render the dropdown', async () => { + render(); + + await waitFor(async() => { + expect(screen.queryAllByTestId('icon-button')).toHaveLength(0); + }); + }); + }); + }); + + describe('as a Group user', () => { + const group = buildGroup({ + relationship: buildGroupRelationship({ + role: GroupRoles.USER, + member: true, + }), + }); + const accountId = '4'; + const groupMember = buildGroupMember({}, { + id: accountId, + display_name: 'tiger woods', + username: 'tiger', + }); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + it('should not render the dropdown', async () => { + render(); + + await waitFor(async() => { + expect(screen.queryAllByTestId('icon-button')).toHaveLength(0); + }); + }); + }); +}); \ No newline at end of file diff --git a/app/soapbox/features/group/components/__tests__/group-options-button.test.tsx b/app/soapbox/features/group/components/__tests__/group-options-button.test.tsx index 8704b9351..e3171bb81 100644 --- a/app/soapbox/features/group/components/__tests__/group-options-button.test.tsx +++ b/app/soapbox/features/group/components/__tests__/group-options-button.test.tsx @@ -17,7 +17,7 @@ describe('', () => { requested: false, member: true, blocked_by: true, - role: 'user', + role: GroupRoles.USER, }), }); }); diff --git a/app/soapbox/features/group/components/__tests__/group-tag-list-item.test.tsx b/app/soapbox/features/group/components/__tests__/group-tag-list-item.test.tsx new file mode 100644 index 000000000..4418fff86 --- /dev/null +++ b/app/soapbox/features/group/components/__tests__/group-tag-list-item.test.tsx @@ -0,0 +1,123 @@ +import React from 'react'; + +import { buildGroup, buildGroupTag, buildGroupRelationship } from 'soapbox/jest/factory'; +import { render, screen } from 'soapbox/jest/test-helpers'; +import { GroupRoles } from 'soapbox/schemas/group-member'; + +import GroupTagListItem from '../group-tag-list-item'; + +describe('', () => { + describe('tag name', () => { + const name = 'hello'; + + it('should render the tag name', () => { + const group = buildGroup(); + const tag = buildGroupTag({ name }); + render(); + + expect(screen.getByTestId('group-tag-list-item')).toHaveTextContent(`#${name}`); + }); + + describe('when the tag is "visible"', () => { + const group = buildGroup(); + const tag = buildGroupTag({ name, visible: true }); + + it('renders the default name', () => { + render(); + expect(screen.getByTestId('group-tag-name')).toHaveClass('text-gray-900'); + }); + }); + + describe('when the tag is not "visible" and user is Owner', () => { + const group = buildGroup({ + relationship: buildGroupRelationship({ + role: GroupRoles.OWNER, + member: true, + }), + }); + const tag = buildGroupTag({ + name, + visible: false, + }); + + it('renders the subtle name', () => { + render(); + expect(screen.getByTestId('group-tag-name')).toHaveClass('text-gray-400'); + }); + }); + + describe('when the tag is not "visible" and user is Admin or User', () => { + const group = buildGroup({ + relationship: buildGroupRelationship({ + role: GroupRoles.ADMIN, + member: true, + }), + }); + const tag = buildGroupTag({ + name, + visible: false, + }); + + it('renders the subtle name', () => { + render(); + expect(screen.getByTestId('group-tag-name')).toHaveClass('text-gray-900'); + }); + }); + }); + + describe('pinning', () => { + describe('as an owner', () => { + const group = buildGroup({ + relationship: buildGroupRelationship({ + role: GroupRoles.OWNER, + member: true, + }), + }); + + describe('when the tag is visible', () => { + const tag = buildGroupTag({ visible: true }); + + it('renders the pin icon', () => { + render(); + expect(screen.getByTestId('pin-icon')).toBeInTheDocument(); + }); + }); + + describe('when the tag is not visible', () => { + const tag = buildGroupTag({ visible: false }); + + it('does not render the pin icon', () => { + render(); + expect(screen.queryAllByTestId('pin-icon')).toHaveLength(0); + }); + }); + + describe('as a non-owner', () => { + const group = buildGroup({ + relationship: buildGroupRelationship({ + role: GroupRoles.ADMIN, + member: true, + }), + }); + + describe('when the tag is visible', () => { + const tag = buildGroupTag({ visible: true }); + + it('does not render the pin icon', () => { + render(); + expect(screen.queryAllByTestId('pin-icon')).toHaveLength(0); + }); + }); + + describe('when the tag is not visible', () => { + const tag = buildGroupTag({ visible: false }); + + it('does not render the pin icon', () => { + render(); + expect(screen.queryAllByTestId('pin-icon')).toHaveLength(0); + }); + }); + }); + }); + }); +}); \ No newline at end of file diff --git a/app/soapbox/features/group/components/group-action-button.tsx b/app/soapbox/features/group/components/group-action-button.tsx index e9a47a962..005b9e245 100644 --- a/app/soapbox/features/group/components/group-action-button.tsx +++ b/app/soapbox/features/group/components/group-action-button.tsx @@ -3,11 +3,11 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { fetchGroupRelationshipsSuccess } from 'soapbox/actions/groups'; import { openModal } from 'soapbox/actions/modals'; +import { useCancelMembershipRequest, useJoinGroup, useLeaveGroup } from 'soapbox/api/hooks'; import { Button } from 'soapbox/components/ui'; import { importEntities } from 'soapbox/entity-store/actions'; import { Entities } from 'soapbox/entity-store/entities'; import { useAppDispatch, useOwnAccount } from 'soapbox/hooks'; -import { useCancelMembershipRequest, useJoinGroup, useLeaveGroup } from 'soapbox/hooks/api'; import { queryClient } from 'soapbox/queries/client'; import { GroupKeys } from 'soapbox/queries/groups'; import { GroupRoles } from 'soapbox/schemas/group-member'; diff --git a/app/soapbox/features/group/components/group-header.tsx b/app/soapbox/features/group/components/group-header.tsx index 9626906d5..ad22e1e13 100644 --- a/app/soapbox/features/group/components/group-header.tsx +++ b/app/soapbox/features/group/components/group-header.tsx @@ -34,7 +34,7 @@ const GroupHeader: React.FC = ({ group }) => { if (!group) { return ( -
+
@@ -107,7 +107,10 @@ const GroupHeader: React.FC = ({ group }) => { } return ( -
+
{isHeaderMissing ? ( ) : header} @@ -120,7 +123,7 @@ const GroupHeader: React.FC = ({ group }) => {
{renderHeader()} -
+
= ({ group }) => { size='xl' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} + data-testid='group-name' /> {!isDeleted && ( <> - + @@ -154,7 +158,7 @@ const GroupHeader: React.FC = ({ group }) => { /> - + diff --git a/app/soapbox/features/group/components/group-member-list-item.tsx b/app/soapbox/features/group/components/group-member-list-item.tsx index c88f4e58d..f9b18735d 100644 --- a/app/soapbox/features/group/components/group-member-list-item.tsx +++ b/app/soapbox/features/group/components/group-member-list-item.tsx @@ -4,6 +4,7 @@ import { defineMessages, useIntl } from 'react-intl'; import { groupKick } from 'soapbox/actions/groups'; import { openModal } from 'soapbox/actions/modals'; +import { useAccount, useBlockGroupMember, useDemoteGroupMember, usePromoteGroupMember } from 'soapbox/api/hooks'; import Account from 'soapbox/components/account'; import DropdownMenu from 'soapbox/components/dropdown-menu/dropdown-menu'; import { HStack } from 'soapbox/components/ui'; @@ -11,7 +12,6 @@ import { deleteEntities } from 'soapbox/entity-store/actions'; import { Entities } from 'soapbox/entity-store/entities'; import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder-account'; import { useAppDispatch, useFeatures } from 'soapbox/hooks'; -import { useAccount, useBlockGroupMember, useDemoteGroupMember, usePromoteGroupMember } from 'soapbox/hooks/api'; import { GroupRoles } from 'soapbox/schemas/group-member'; import toast from 'soapbox/toast'; @@ -180,7 +180,11 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => { } return ( - +
@@ -188,6 +192,7 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => { {(isMemberOwner || isMemberAdmin) ? ( { require('@tabler/icons/pin.svg') } iconClassName='h-5 w-5 text-primary-500 dark:text-accent-blue' + data-testid='pin-icon' /> ); @@ -123,13 +124,18 @@ const GroupTagListItem = (props: IGroupMemberListItem) => { }; return ( - + #{tag.name} @@ -137,7 +143,7 @@ const GroupTagListItem = (props: IGroupMemberListItem) => { {intl.formatMessage(messages.total)}: {' '} - {shortNumberFormat(tag.groups)} + {shortNumberFormat(tag.uses)} diff --git a/app/soapbox/features/group/edit-group.tsx b/app/soapbox/features/group/edit-group.tsx index 184df76d5..c0e972604 100644 --- a/app/soapbox/features/group/edit-group.tsx +++ b/app/soapbox/features/group/edit-group.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { useGroup, useUpdateGroup } from 'soapbox/api/hooks'; import { Button, Column, Form, FormActions, FormGroup, Icon, Input, Spinner, Textarea } from 'soapbox/components/ui'; import { useAppSelector, useInstance } from 'soapbox/hooks'; -import { useGroup, useUpdateGroup } from 'soapbox/hooks/api'; import { useImageField, useTextField } from 'soapbox/hooks/forms'; import toast from 'soapbox/toast'; import { isDefaultAvatar, isDefaultHeader } from 'soapbox/utils/accounts'; diff --git a/app/soapbox/features/group/group-blocked-members.tsx b/app/soapbox/features/group/group-blocked-members.tsx index 988b90b1b..f45e8259f 100644 --- a/app/soapbox/features/group/group-blocked-members.tsx +++ b/app/soapbox/features/group/group-blocked-members.tsx @@ -2,11 +2,11 @@ import React, { useCallback, useEffect } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { fetchGroupBlocks, groupUnblock } from 'soapbox/actions/groups'; +import { useGroup } from 'soapbox/api/hooks'; 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 { useGroup } from 'soapbox/hooks/api'; import { makeGetAccount } from 'soapbox/selectors'; import toast from 'soapbox/toast'; diff --git a/app/soapbox/features/group/group-gallery.tsx b/app/soapbox/features/group/group-gallery.tsx index 70a77680d..f0b2b95c7 100644 --- a/app/soapbox/features/group/group-gallery.tsx +++ b/app/soapbox/features/group/group-gallery.tsx @@ -2,11 +2,11 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import { openModal } from 'soapbox/actions/modals'; +import { useGroup, useGroupMedia } from 'soapbox/api/hooks'; import LoadMore from 'soapbox/components/load-more'; import MissingIndicator from 'soapbox/components/missing-indicator'; import { Column, Spinner } from 'soapbox/components/ui'; import { useAppDispatch } from 'soapbox/hooks'; -import { useGroup, useGroupMedia } from 'soapbox/hooks/api'; import MediaItem from '../account-gallery/components/media-item'; diff --git a/app/soapbox/features/group/group-members.tsx b/app/soapbox/features/group/group-members.tsx index 36ccd05b7..1c2a892f3 100644 --- a/app/soapbox/features/group/group-members.tsx +++ b/app/soapbox/features/group/group-members.tsx @@ -1,12 +1,10 @@ import clsx from 'clsx'; import React, { useMemo } from 'react'; +import { useGroup, useGroupMembers, useGroupMembershipRequests } from 'soapbox/api/hooks'; import { PendingItemsRow } from 'soapbox/components/pending-items-row'; import ScrollableList from 'soapbox/components/scrollable-list'; import { useFeatures } from 'soapbox/hooks'; -import { useGroup } from 'soapbox/hooks/api'; -import { useGroupMembershipRequests } from 'soapbox/hooks/api/groups/useGroupMembershipRequests'; -import { useGroupMembers } from 'soapbox/hooks/api/useGroupMembers'; import { GroupRoles } from 'soapbox/schemas/group-member'; import PlaceholderAccount from '../placeholder/components/placeholder-account'; @@ -15,6 +13,7 @@ import GroupMemberListItem from './components/group-member-list-item'; import type { Group } from 'soapbox/types/entities'; + interface IGroupMembers { params: { groupId: string } } diff --git a/app/soapbox/features/group/group-membership-requests.tsx b/app/soapbox/features/group/group-membership-requests.tsx index c83d45b06..79700e1a7 100644 --- a/app/soapbox/features/group/group-membership-requests.tsx +++ b/app/soapbox/features/group/group-membership-requests.tsx @@ -1,12 +1,12 @@ +import { AxiosError } from 'axios'; import React, { useEffect } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; +import { useGroup, useGroupMembers, useGroupMembershipRequests } from 'soapbox/api/hooks'; import Account from 'soapbox/components/account'; import { AuthorizeRejectButtons } from 'soapbox/components/authorize-reject-buttons'; import ScrollableList from 'soapbox/components/scrollable-list'; import { Column, HStack, Spinner } from 'soapbox/components/ui'; -import { useGroup, useGroupMembershipRequests } from 'soapbox/hooks/api'; -import { useGroupMembers } from 'soapbox/hooks/api/useGroupMembers'; import { GroupRoles } from 'soapbox/schemas/group-member'; import toast from 'soapbox/toast'; @@ -59,7 +59,7 @@ const GroupMembershipRequests: React.FC = ({ params }) const { group } = useGroup(id); - const { accounts, authorize, reject, isLoading } = useGroupMembershipRequests(id); + const { accounts, authorize, reject, refetch, isLoading } = useGroupMembershipRequests(id); const { invalidate } = useGroupMembers(id, GroupRoles.USER); useEffect(() => { @@ -81,19 +81,35 @@ const GroupMembershipRequests: React.FC = ({ params }) } async function handleAuthorize(account: AccountEntity) { - try { - await authorize(account.id); - } catch (_e) { - toast.error(intl.formatMessage(messages.authorizeFail, { name: account.username })); - } + return authorize(account.id) + .then(() => Promise.resolve()) + .catch((error: AxiosError) => { + refetch(); + + let message = intl.formatMessage(messages.authorizeFail, { name: account.username }); + if (error.response?.status === 409) { + message = (error.response?.data as any).error; + } + toast.error(message); + + return Promise.reject(); + }); } async function handleReject(account: AccountEntity) { - try { - await reject(account.id); - } catch (_e) { - toast.error(intl.formatMessage(messages.rejectFail, { name: account.username })); - } + return reject(account.id) + .then(() => Promise.resolve()) + .catch((error: AxiosError) => { + refetch(); + + let message = intl.formatMessage(messages.rejectFail, { name: account.username }); + if (error.response?.status === 409) { + message = (error.response?.data as any).error; + } + toast.error(message); + + return Promise.reject(); + }); } return ( diff --git a/app/soapbox/features/group/group-tag-timeline.tsx b/app/soapbox/features/group/group-tag-timeline.tsx index f2ddee382..e79092e7a 100644 --- a/app/soapbox/features/group/group-tag-timeline.tsx +++ b/app/soapbox/features/group/group-tag-timeline.tsx @@ -2,9 +2,9 @@ import React, { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; import { expandGroupTimelineFromTag } from 'soapbox/actions/timelines'; +import { useGroup, useGroupTag } from 'soapbox/api/hooks'; import { Column, Icon, Stack, Text } from 'soapbox/components/ui'; import { useAppDispatch } from 'soapbox/hooks'; -import { useGroup, useGroupTag } from 'soapbox/hooks/api'; import Timeline from '../ui/components/timeline'; diff --git a/app/soapbox/features/group/group-tags.tsx b/app/soapbox/features/group/group-tags.tsx index 516fb94df..710a4fdb5 100644 --- a/app/soapbox/features/group/group-tags.tsx +++ b/app/soapbox/features/group/group-tags.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; +import { useGroupTags } from 'soapbox/api/hooks'; import ScrollableList from 'soapbox/components/scrollable-list'; import { Icon, Stack, Text } from 'soapbox/components/ui'; -import { useGroupTags } from 'soapbox/hooks/api'; import { useGroup } from 'soapbox/queries/groups'; import PlaceholderAccount from '../placeholder/components/placeholder-account'; diff --git a/app/soapbox/features/group/group-timeline.tsx b/app/soapbox/features/group/group-timeline.tsx index d189fdb68..a920a862c 100644 --- a/app/soapbox/features/group/group-timeline.tsx +++ b/app/soapbox/features/group/group-timeline.tsx @@ -6,10 +6,10 @@ import { Link } from 'react-router-dom'; import { groupCompose, setGroupTimelineVisible, uploadCompose } from 'soapbox/actions/compose'; import { connectGroupStream } from 'soapbox/actions/streaming'; import { expandGroupTimeline } from 'soapbox/actions/timelines'; +import { useGroup } from 'soapbox/api/hooks'; import { Avatar, HStack, Icon, Stack, Text, Toggle } from 'soapbox/components/ui'; import ComposeForm from 'soapbox/features/compose/components/compose-form'; import { useAppDispatch, useAppSelector, useDraggedFiles, useOwnAccount } from 'soapbox/hooks'; -import { useGroup } from 'soapbox/hooks/api'; import Timeline from '../ui/components/timeline'; diff --git a/app/soapbox/features/group/manage-group.tsx b/app/soapbox/features/group/manage-group.tsx index 5c3ee5c24..d57b8792a 100644 --- a/app/soapbox/features/group/manage-group.tsx +++ b/app/soapbox/features/group/manage-group.tsx @@ -3,10 +3,10 @@ import { defineMessages, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; import { openModal } from 'soapbox/actions/modals'; +import { useDeleteGroup, useGroup } from 'soapbox/api/hooks'; import List, { ListItem } from 'soapbox/components/list'; import { CardBody, CardHeader, CardTitle, Column, Spinner, Text } from 'soapbox/components/ui'; import { useAppDispatch, useBackend, useGroupsPath } from 'soapbox/hooks'; -import { useDeleteGroup, useGroup } from 'soapbox/hooks/api'; import { GroupRoles } from 'soapbox/schemas/group-member'; import toast from 'soapbox/toast'; import { TRUTHSOCIAL } from 'soapbox/utils/features'; @@ -16,16 +16,16 @@ import ColumnForbidden from '../ui/components/column-forbidden'; type RouteParams = { groupId: string }; const messages = defineMessages({ - heading: { id: 'column.manage_group', defaultMessage: 'Manage group' }, - editGroup: { id: 'manage_group.edit_group', defaultMessage: 'Edit group' }, + heading: { id: 'column.manage_group', defaultMessage: 'Manage Group' }, + editGroup: { id: 'manage_group.edit_group', defaultMessage: 'Edit Group' }, pendingRequests: { id: 'manage_group.pending_requests', defaultMessage: 'Pending Requests' }, blockedMembers: { id: 'manage_group.blocked_members', defaultMessage: 'Banned Members' }, - deleteGroup: { id: 'manage_group.delete_group', defaultMessage: 'Delete group' }, + deleteGroup: { id: 'manage_group.delete_group', defaultMessage: 'Delete Group' }, deleteConfirm: { id: 'confirmations.delete_group.confirm', defaultMessage: 'Delete' }, - deleteHeading: { id: 'confirmations.delete_group.heading', defaultMessage: 'Delete group' }, + deleteHeading: { id: 'confirmations.delete_group.heading', defaultMessage: 'Delete Group' }, deleteMessage: { id: 'confirmations.delete_group.message', defaultMessage: 'Are you sure you want to delete this group? This is a permanent action that cannot be undone.' }, members: { id: 'group.tabs.members', defaultMessage: 'Members' }, - other: { id: 'settings.other', defaultMessage: 'Other options' }, + other: { id: 'settings.other', defaultMessage: 'Other Options' }, deleteSuccess: { id: 'group.delete.success', defaultMessage: 'Group successfully deleted' }, }); diff --git a/app/soapbox/features/groups/components/discover/popular-groups.tsx b/app/soapbox/features/groups/components/discover/popular-groups.tsx index 894542eb8..83426f553 100644 --- a/app/soapbox/features/groups/components/discover/popular-groups.tsx +++ b/app/soapbox/features/groups/components/discover/popular-groups.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; +import { usePopularGroups } from 'soapbox/api/hooks'; import Link from 'soapbox/components/link'; import { Carousel, HStack, Stack, Text } from 'soapbox/components/ui'; import PlaceholderGroupDiscover from 'soapbox/features/placeholder/components/placeholder-group-discover'; -import { usePopularGroups } from 'soapbox/hooks/api/usePopularGroups'; import GroupGridItem from './group-grid-item'; diff --git a/app/soapbox/features/groups/components/discover/popular-tags.tsx b/app/soapbox/features/groups/components/discover/popular-tags.tsx index 4cfed0bee..75ff36628 100644 --- a/app/soapbox/features/groups/components/discover/popular-tags.tsx +++ b/app/soapbox/features/groups/components/discover/popular-tags.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; +import { usePopularTags } from 'soapbox/api/hooks'; import Link from 'soapbox/components/link'; import { HStack, Stack, Text } from 'soapbox/components/ui'; -import { usePopularTags } from 'soapbox/hooks/api'; import TagListItem from './tag-list-item'; diff --git a/app/soapbox/features/groups/components/discover/search/results.tsx b/app/soapbox/features/groups/components/discover/search/results.tsx index 2543f6ebc..3ae6b2179 100644 --- a/app/soapbox/features/groups/components/discover/search/results.tsx +++ b/app/soapbox/features/groups/components/discover/search/results.tsx @@ -3,8 +3,8 @@ import React, { useCallback, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso'; +import { useGroupSearch } from 'soapbox/api/hooks'; import { HStack, Stack, Text } from 'soapbox/components/ui'; -import { useGroupSearch } from 'soapbox/hooks/api'; import GroupGridItem from '../group-grid-item'; import GroupListItem from '../group-list-item'; diff --git a/app/soapbox/features/groups/components/discover/search/search.tsx b/app/soapbox/features/groups/components/discover/search/search.tsx index 647f10fb5..0e2d7f00e 100644 --- a/app/soapbox/features/groups/components/discover/search/search.tsx +++ b/app/soapbox/features/groups/components/discover/search/search.tsx @@ -1,10 +1,10 @@ import React, { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; +import { useGroupSearch } from 'soapbox/api/hooks'; import { Stack } from 'soapbox/components/ui'; import PlaceholderGroupSearch from 'soapbox/features/placeholder/components/placeholder-group-search'; import { useDebounce, useOwnAccount } from 'soapbox/hooks'; -import { useGroupSearch } from 'soapbox/hooks/api'; import { saveGroupSearch } from 'soapbox/utils/groups'; import Blankslate from './blankslate'; diff --git a/app/soapbox/features/groups/components/discover/suggested-groups.tsx b/app/soapbox/features/groups/components/discover/suggested-groups.tsx index e0e26e874..5d73cc3f0 100644 --- a/app/soapbox/features/groups/components/discover/suggested-groups.tsx +++ b/app/soapbox/features/groups/components/discover/suggested-groups.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; +import { useSuggestedGroups } from 'soapbox/api/hooks'; import Link from 'soapbox/components/link'; import { Carousel, HStack, Stack, Text } from 'soapbox/components/ui'; import PlaceholderGroupDiscover from 'soapbox/features/placeholder/components/placeholder-group-discover'; -import { useSuggestedGroups } from 'soapbox/hooks/api/useSuggestedGroups'; import GroupGridItem from './group-grid-item'; diff --git a/app/soapbox/features/groups/index.tsx b/app/soapbox/features/groups/index.tsx index 7528989f6..7b1c51c55 100644 --- a/app/soapbox/features/groups/index.tsx +++ b/app/soapbox/features/groups/index.tsx @@ -3,11 +3,11 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { Link } from 'react-router-dom'; import { openModal } from 'soapbox/actions/modals'; +import { useGroups } from 'soapbox/api/hooks'; import GroupCard from 'soapbox/components/group-card'; import ScrollableList from 'soapbox/components/scrollable-list'; import { Button, Input, Stack, Text } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector, useDebounce, useFeatures } from 'soapbox/hooks'; -import { useGroups } from 'soapbox/hooks/api'; import { PERMISSION_CREATE_GROUPS, hasPermission } from 'soapbox/utils/permissions'; import PlaceholderGroupCard from '../placeholder/components/placeholder-group-card'; diff --git a/app/soapbox/features/groups/popular.tsx b/app/soapbox/features/groups/popular.tsx index c2c11148c..2f417dd8f 100644 --- a/app/soapbox/features/groups/popular.tsx +++ b/app/soapbox/features/groups/popular.tsx @@ -3,8 +3,8 @@ import React, { useCallback, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso'; +import { usePopularGroups } from 'soapbox/api/hooks'; import { Column } from 'soapbox/components/ui'; -import { usePopularGroups } from 'soapbox/hooks/api/usePopularGroups'; import GroupGridItem from './components/discover/group-grid-item'; import GroupListItem from './components/discover/group-list-item'; diff --git a/app/soapbox/features/groups/suggested.tsx b/app/soapbox/features/groups/suggested.tsx index 4d1843598..89833a9a8 100644 --- a/app/soapbox/features/groups/suggested.tsx +++ b/app/soapbox/features/groups/suggested.tsx @@ -3,8 +3,8 @@ import React, { useCallback, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso'; +import { useSuggestedGroups } from 'soapbox/api/hooks'; import { Column } from 'soapbox/components/ui'; -import { useSuggestedGroups } from 'soapbox/hooks/api/useSuggestedGroups'; import GroupGridItem from './components/discover/group-grid-item'; import GroupListItem from './components/discover/group-list-item'; diff --git a/app/soapbox/features/groups/tag.tsx b/app/soapbox/features/groups/tag.tsx index 8b18053d3..ccc54bbb3 100644 --- a/app/soapbox/features/groups/tag.tsx +++ b/app/soapbox/features/groups/tag.tsx @@ -2,8 +2,8 @@ import clsx from 'clsx'; import React, { useCallback, useState } from 'react'; import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso'; +import { useGroupTag, useGroupsFromTag } from 'soapbox/api/hooks'; import { Column, HStack, Icon } from 'soapbox/components/ui'; -import { useGroupTag, useGroupsFromTag } from 'soapbox/hooks/api'; import GroupGridItem from './components/discover/group-grid-item'; import GroupListItem from './components/discover/group-list-item'; diff --git a/app/soapbox/features/groups/tags.tsx b/app/soapbox/features/groups/tags.tsx index 1484665e4..aa37a514b 100644 --- a/app/soapbox/features/groups/tags.tsx +++ b/app/soapbox/features/groups/tags.tsx @@ -3,8 +3,8 @@ import React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { Virtuoso } from 'react-virtuoso'; +import { usePopularTags } from 'soapbox/api/hooks'; import { Column, Text } from 'soapbox/components/ui'; -import { usePopularTags } from 'soapbox/hooks/api'; import TagListItem from './components/discover/tag-list-item'; diff --git a/app/soapbox/features/placeholder/components/placeholder-avatar.tsx b/app/soapbox/features/placeholder/components/placeholder-avatar.tsx index 6eb479f13..058904001 100644 --- a/app/soapbox/features/placeholder/components/placeholder-avatar.tsx +++ b/app/soapbox/features/placeholder/components/placeholder-avatar.tsx @@ -1,3 +1,4 @@ +import clsx from 'clsx'; import React from 'react'; import { Stack } from 'soapbox/components/ui'; @@ -5,10 +6,11 @@ import { Stack } from 'soapbox/components/ui'; interface IPlaceholderAvatar { size: number withText?: boolean + className?: string } /** Fake avatar to display while data is loading. */ -const PlaceholderAvatar: React.FC = ({ size, withText = false }) => { +const PlaceholderAvatar: React.FC = ({ size, withText = false, className }) => { const style = React.useMemo(() => { if (!size) { return {}; @@ -21,7 +23,10 @@ const PlaceholderAvatar: React.FC = ({ size, withText = fals }, [size]); return ( - +
= ({ variant = 'rounded' }) => ( +const PlaceholderStatus: React.FC = ({ variant }) => (
{ - {displayName} + {displayName} diff --git a/app/soapbox/features/status/components/detailed-status.tsx b/app/soapbox/features/status/components/detailed-status.tsx index 2959d285d..15f2e86e8 100644 --- a/app/soapbox/features/status/components/detailed-status.tsx +++ b/app/soapbox/features/status/components/detailed-status.tsx @@ -125,7 +125,6 @@ const DetailedStatus: React.FC = ({ = ({ count, onClick, chi } > - + {shortNumberFormat(count)} diff --git a/app/soapbox/features/status/components/thread-status.tsx b/app/soapbox/features/status/components/thread-status.tsx index c3ff7bc65..8f3e5b249 100644 --- a/app/soapbox/features/status/components/thread-status.tsx +++ b/app/soapbox/features/status/components/thread-status.tsx @@ -46,7 +46,7 @@ const ThreadStatus: React.FC = (props): JSX.Element => { // @ts-ignore FIXME ) : ( - + )}
); diff --git a/app/soapbox/features/ui/components/__tests__/subscribe-button.test.tsx b/app/soapbox/features/ui/components/__tests__/subscribe-button.test.tsx index d0ec92f96..5edc9636b 100644 --- a/app/soapbox/features/ui/components/__tests__/subscribe-button.test.tsx +++ b/app/soapbox/features/ui/components/__tests__/subscribe-button.test.tsx @@ -1,8 +1,9 @@ -// import { Map as ImmutableMap } from 'immutable'; import React from 'react'; -import { render, screen } from '../../../../jest/test-helpers'; -import { normalizeAccount, normalizeRelationship } from '../../../../normalizers'; +import { buildRelationship } from 'soapbox/jest/factory'; +import { render, screen } from 'soapbox/jest/test-helpers'; +import { normalizeAccount } from 'soapbox/normalizers'; + import SubscribeButton from '../subscription-button'; import type { ReducerAccount } from 'soapbox/reducers/accounts'; @@ -19,162 +20,10 @@ describe('', () => { describe('with "accountNotifies" disabled', () => { it('renders nothing', () => { - const account = normalizeAccount({ ...justin, relationship: normalizeRelationship({ following: true }) }) as ReducerAccount; + const account = normalizeAccount({ ...justin, relationship: buildRelationship({ following: true }) }) as ReducerAccount; render(, undefined, store); expect(screen.queryAllByTestId('icon-button')).toHaveLength(0); }); }); - - // describe('with "accountNotifies" enabled', () => { - // beforeEach(() => { - // store = { - // ...store, - // instance: normalizeInstance({ - // version: '3.4.1 (compatible; TruthSocial 1.0.0)', - // software: 'TRUTHSOCIAL', - // pleroma: ImmutableMap({}), - // }), - // }; - // }); - - // describe('when the relationship is requested', () => { - // beforeEach(() => { - // account = normalizeAccount({ ...account, relationship: normalizeRelationship({ requested: true }) }); - - // store = { - // ...store, - // accounts: ImmutableMap({ - // '1': account, - // }), - // }; - // }); - - // it('renders the button', () => { - // render(, null, store); - // expect(screen.getByTestId('icon-button')).toBeInTheDocument(); - // }); - - // describe('when the user "isSubscribed"', () => { - // beforeEach(() => { - // account = normalizeAccount({ - // ...account, - // relationship: normalizeRelationship({ requested: true, notifying: true }), - // }); - - // store = { - // ...store, - // accounts: ImmutableMap({ - // '1': account, - // }), - // }; - // }); - - // it('renders the unsubscribe button', () => { - // render(, null, store); - // expect(screen.getByTestId('icon-button').title).toEqual(`Unsubscribe to notifications from @${account.acct}`); - // }); - // }); - - // describe('when the user is not "isSubscribed"', () => { - // beforeEach(() => { - // account = normalizeAccount({ - // ...account, - // relationship: normalizeRelationship({ requested: true, notifying: false }), - // }); - - // store = { - // ...store, - // accounts: ImmutableMap({ - // '1': account, - // }), - // }; - // }); - - // it('renders the unsubscribe button', () => { - // render(, null, store); - // expect(screen.getByTestId('icon-button').title).toEqual(`Subscribe to notifications from @${account.acct}`); - // }); - // }); - // }); - - // describe('when the user is not following the account', () => { - // beforeEach(() => { - // account = normalizeAccount({ ...account, relationship: normalizeRelationship({ following: false }) }); - - // store = { - // ...store, - // accounts: ImmutableMap({ - // '1': account, - // }), - // }; - // }); - - // it('renders nothing', () => { - // render(, null, store); - // expect(screen.queryAllByTestId('icon-button')).toHaveLength(0); - // }); - // }); - - // describe('when the user is following the account', () => { - // beforeEach(() => { - // account = normalizeAccount({ ...account, relationship: normalizeRelationship({ following: true }) }); - - // store = { - // ...store, - // accounts: ImmutableMap({ - // '1': account, - // }), - // }; - // }); - - // it('renders the button', () => { - // render(, null, store); - // expect(screen.getByTestId('icon-button')).toBeInTheDocument(); - // }); - - // describe('when the user "isSubscribed"', () => { - // beforeEach(() => { - // account = normalizeAccount({ - // ...account, - // relationship: normalizeRelationship({ requested: true, notifying: true }), - // }); - - // store = { - // ...store, - // accounts: ImmutableMap({ - // '1': account, - // }), - // }; - // }); - - // it('renders the unsubscribe button', () => { - // render(, null, store); - // expect(screen.getByTestId('icon-button').title).toEqual(`Unsubscribe to notifications from @${account.acct}`); - // }); - // }); - - // describe('when the user is not "isSubscribed"', () => { - // beforeEach(() => { - // account = normalizeAccount({ - // ...account, - // relationship: normalizeRelationship({ requested: true, notifying: false }), - // }); - - // store = { - // ...store, - // accounts: ImmutableMap({ - // '1': account, - // }), - // }; - // }); - - // it('renders the unsubscribe button', () => { - // render(, null, store); - // expect(screen.getByTestId('icon-button').title).toEqual(`Subscribe to notifications from @${account.acct}`); - // }); - // }); - // }); - // }); - }); diff --git a/app/soapbox/features/ui/components/compose-button.tsx b/app/soapbox/features/ui/components/compose-button.tsx index a2944f699..a21909bb1 100644 --- a/app/soapbox/features/ui/components/compose-button.tsx +++ b/app/soapbox/features/ui/components/compose-button.tsx @@ -4,9 +4,9 @@ import { useLocation, useRouteMatch } from 'react-router-dom'; import { groupComposeModal } from 'soapbox/actions/compose'; import { openModal } from 'soapbox/actions/modals'; +import { useGroupLookup } from 'soapbox/api/hooks'; import { Avatar, Button, HStack } from 'soapbox/components/ui'; import { useAppDispatch } from 'soapbox/hooks'; -import { useGroupLookup } from 'soapbox/hooks/api/groups/useGroupLookup'; const ComposeButton = () => { const location = useLocation(); @@ -25,7 +25,6 @@ const HomeComposeButton = () => { return (