From 7d48c40b895418fa573fbc1b70e316ec878e9952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 17 Dec 2022 14:31:23 +0100 Subject: [PATCH] Placeholder group cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/groups.ts | 2 +- app/soapbox/components/group-card.tsx | 2 +- app/soapbox/features/groups/index.tsx | 26 ++++++++------- .../components/placeholder-group-card.tsx | 32 +++++++++++++++++++ app/soapbox/reducers/groups.ts | 29 ++++++++++------- app/soapbox/selectors/index.ts | 4 +-- 6 files changed, 68 insertions(+), 27 deletions(-) create mode 100644 app/soapbox/features/placeholder/components/placeholder-group-card.tsx diff --git a/app/soapbox/actions/groups.ts b/app/soapbox/actions/groups.ts index 271b03b63..ef29f48aa 100644 --- a/app/soapbox/actions/groups.ts +++ b/app/soapbox/actions/groups.ts @@ -310,7 +310,7 @@ const fetchGroupRelationshipsFail = (error: AxiosError) => ({ const joinGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { - const locked = (getState().groups.get(id) as any).locked || false; + const locked = (getState().groups.items.get(id) as any).locked || false; dispatch(joinGroupRequest(id, locked)); diff --git a/app/soapbox/components/group-card.tsx b/app/soapbox/components/group-card.tsx index a3bead4f2..b45ea29e7 100644 --- a/app/soapbox/components/group-card.tsx +++ b/app/soapbox/components/group-card.tsx @@ -21,7 +21,7 @@ const GroupCard: React.FC = ({ group }) => {
{group.header && {intl.formatMessage(messages.groupHeader)}} -
+
diff --git a/app/soapbox/features/groups/index.tsx b/app/soapbox/features/groups/index.tsx index 872104f50..c29ac39d4 100644 --- a/app/soapbox/features/groups/index.tsx +++ b/app/soapbox/features/groups/index.tsx @@ -10,30 +10,28 @@ import ScrollableList from 'soapbox/components/scrollable-list'; import { Column, Spinner, Stack, Text } from 'soapbox/components/ui'; import { useAppSelector } from 'soapbox/hooks'; +import PlaceholderGroupCard from '../placeholder/components/placeholder-group-card'; + import type { List as ImmutableList } from 'immutable'; import type { RootState } from 'soapbox/store'; import type { Group as GroupEntity } from 'soapbox/types/entities'; const getOrderedGroups = createSelector([ - (state: RootState) => state.groups, + (state: RootState) => state.groups.items, + (state: RootState) => state.groups.isLoading, (state: RootState) => state.group_relationships, -], (groups, group_relationships) => { - if (!groups) { - return groups; - } - - return (groups - .toList() - .filter((item: GroupEntity | false) => !!item) as ImmutableList) +], (groups, isLoading, group_relationships) => ({ + groups: (groups.toList().filter((item: GroupEntity | false) => !!item) as ImmutableList) .map((item) => item.set('relationship', group_relationships.get(item.id) || null)) .filter((item) => item.relationship?.member) - .sort((a, b) => a.display_name.localeCompare(b.display_name)); -}); + .sort((a, b) => a.display_name.localeCompare(b.display_name)), + isLoading, +})); const Groups: React.FC = () => { const dispatch = useDispatch(); - const groups = useAppSelector((state) => getOrderedGroups(state)); + const { groups, isLoading } = useAppSelector((state) => getOrderedGroups(state)); useEffect(() => { dispatch(fetchGroups()); @@ -72,6 +70,10 @@ const Groups: React.FC = () => { scrollKey='groups' emptyMessage={emptyMessage} itemClassName='py-3 last:pb-0' + isLoading + showLoading + placeholderComponent={PlaceholderGroupCard} + placeholderCount={3} > {groups.map((group) => ( diff --git a/app/soapbox/features/placeholder/components/placeholder-group-card.tsx b/app/soapbox/features/placeholder/components/placeholder-group-card.tsx new file mode 100644 index 000000000..fe611a7d4 --- /dev/null +++ b/app/soapbox/features/placeholder/components/placeholder-group-card.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +import { HStack, Stack, Text } from 'soapbox/components/ui'; + +import { generateText, randomIntFromInterval } from '../utils'; + +const PlaceholderGroupCard = () => { + const groupNameLength = randomIntFromInterval(5, 25); + const roleLength = randomIntFromInterval(5, 15); + const privacyLength = randomIntFromInterval(5, 15); + + return ( +
+ +
+
+
+
+
+ + {generateText(groupNameLength)} + + {generateText(roleLength)} + {generateText(privacyLength)} + + + +
+ ); +}; + +export default PlaceholderGroupCard; diff --git a/app/soapbox/reducers/groups.ts b/app/soapbox/reducers/groups.ts index c48572632..c7b6a1d28 100644 --- a/app/soapbox/reducers/groups.ts +++ b/app/soapbox/reducers/groups.ts @@ -1,6 +1,6 @@ -import { Map as ImmutableMap } from 'immutable'; +import { Map as ImmutableMap, Record as ImmutableRecord } from 'immutable'; -import { GROUP_FETCH_FAIL, GROUP_DELETE_SUCCESS } from 'soapbox/actions/groups'; +import { GROUP_FETCH_FAIL, GROUP_DELETE_SUCCESS, GROUP_FETCH_REQUEST } from 'soapbox/actions/groups'; import { GROUPS_IMPORT } from 'soapbox/actions/importer'; import { normalizeGroup } from 'soapbox/normalizers'; @@ -10,23 +10,30 @@ import type { APIEntity } from 'soapbox/types/entities'; type GroupRecord = ReturnType; type APIEntities = Array; -type State = ImmutableMap; +const ReducerRecord = ImmutableRecord({ + isLoading: true, + items: ImmutableMap({}), +}); -const normalizeGroups = (state: State, relationships: APIEntities) => { - relationships.forEach(relationship => { - state = state.set(relationship.id, normalizeGroup(relationship)); - }); +type State = ReturnType; - return state; -}; +const normalizeGroups = (state: State, groups: APIEntities) => + state.update('items', items => + groups.reduce((items: ImmutableMap, group) => + items.set(group.id, normalizeGroup(group)), items), + ).set('isLoading', false); -export default function groups(state: State = ImmutableMap(), action: AnyAction) { +export default function groups(state: State = ReducerRecord(), action: AnyAction) { switch (action.type) { case GROUPS_IMPORT: return normalizeGroups(state, action.groups); + case GROUP_FETCH_REQUEST: + return state.set('isLoading', true); case GROUP_DELETE_SUCCESS: case GROUP_FETCH_FAIL: - return state.set(action.id, false); + return state + .setIn(['items', action.id], false) + .set('isLoading', false); default: return state; } diff --git a/app/soapbox/selectors/index.ts b/app/soapbox/selectors/index.ts index abfb051a8..4dc7bf03e 100644 --- a/app/soapbox/selectors/index.ts +++ b/app/soapbox/selectors/index.ts @@ -144,7 +144,7 @@ export const makeGetStatus = () => { (state: RootState, { id }: APIStatus) => state.statuses.get(state.statuses.get(id)?.reblog || ''), (state: RootState, { id }: APIStatus) => state.accounts.get(state.statuses.get(id)?.account || ''), (state: RootState, { id }: APIStatus) => state.accounts.get(state.statuses.get(state.statuses.get(id)?.reblog || '')?.account || ''), - (state: RootState, { id }: APIStatus) => state.groups.get(state.statuses.get(id)?.group || ''), + (state: RootState, { id }: APIStatus) => state.groups.items.get(state.statuses.get(id)?.group || ''), (_state: RootState, { username }: APIStatus) => username, getFilters, (state: RootState) => state.me, @@ -356,7 +356,7 @@ export const makeGetStatusIds = () => createSelector([ export const makeGetGroup = () => { return createSelector([ - (state: RootState, id: string) => state.groups.get(id), + (state: RootState, id: string) => state.groups.items.get(id), (state: RootState, id: string) => state.group_relationships.get(id), ], (base, relationship) => { if (!base) return null;