Merge branch 'group-improvements' into 'develop'
Group improvements See merge request soapbox-pub/soapbox!2385
This commit is contained in:
commit
89ab02224f
|
@ -39,7 +39,7 @@ const useButtonStyles = ({
|
||||||
size,
|
size,
|
||||||
}: IButtonStyles) => {
|
}: IButtonStyles) => {
|
||||||
const buttonStyle = clsx({
|
const buttonStyle = clsx({
|
||||||
'inline-flex items-center border font-medium rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 appearance-none transition-all': true,
|
'inline-flex items-center place-content-center border font-medium rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 appearance-none transition-all': true,
|
||||||
'select-none disabled:opacity-75 disabled:cursor-default': disabled,
|
'select-none disabled:opacity-75 disabled:cursor-default': disabled,
|
||||||
[`${themes[theme]}`]: true,
|
[`${themes[theme]}`]: true,
|
||||||
[`${sizes[size]}`]: true,
|
[`${sizes[size]}`]: true,
|
||||||
|
|
|
@ -40,6 +40,8 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
|
||||||
|
|
||||||
const onJoinGroup = () => joinGroup.mutate({}, {
|
const onJoinGroup = () => joinGroup.mutate({}, {
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
|
joinGroup.invalidate();
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
group.locked
|
group.locked
|
||||||
? intl.formatMessage(messages.joinRequestSuccess)
|
? intl.formatMessage(messages.joinRequestSuccess)
|
||||||
|
@ -53,8 +55,9 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
|
||||||
heading: intl.formatMessage(messages.confirmationHeading),
|
heading: intl.formatMessage(messages.confirmationHeading),
|
||||||
message: intl.formatMessage(messages.confirmationMessage),
|
message: intl.formatMessage(messages.confirmationMessage),
|
||||||
confirm: intl.formatMessage(messages.confirmationConfirm),
|
confirm: intl.formatMessage(messages.confirmationConfirm),
|
||||||
onConfirm: () => leaveGroup.mutate({}, {
|
onConfirm: () => leaveGroup.mutate(group.relationship?.id as string, {
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
|
leaveGroup.invalidate();
|
||||||
toast.success(intl.formatMessage(messages.leaveSuccess));
|
toast.success(intl.formatMessage(messages.leaveSuccess));
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { HStack, Icon, Text } from 'soapbox/components/ui';
|
import { HStack, Icon, Text } from 'soapbox/components/ui';
|
||||||
|
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||||
import { Group } from 'soapbox/types/entities';
|
import { Group } from 'soapbox/types/entities';
|
||||||
|
|
||||||
interface IGroupRelationship {
|
interface IGroupRelationship {
|
||||||
|
@ -9,10 +10,10 @@ interface IGroupRelationship {
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupRelationship = ({ group }: IGroupRelationship) => {
|
const GroupRelationship = ({ group }: IGroupRelationship) => {
|
||||||
const isAdmin = group.relationship?.role === 'admin';
|
const isOwner = group.relationship?.role === GroupRoles.OWNER;
|
||||||
const isModerator = group.relationship?.role === 'moderator';
|
const isAdmin = group.relationship?.role === GroupRoles.ADMIN;
|
||||||
|
|
||||||
if (!isAdmin || !isModerator) {
|
if (!isOwner || !isAdmin) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,14 +22,14 @@ const GroupRelationship = ({ group }: IGroupRelationship) => {
|
||||||
<Icon
|
<Icon
|
||||||
className='h-4 w-4'
|
className='h-4 w-4'
|
||||||
src={
|
src={
|
||||||
isAdmin
|
isOwner
|
||||||
? require('@tabler/icons/users.svg')
|
? require('@tabler/icons/users.svg')
|
||||||
: require('@tabler/icons/gavel.svg')
|
: require('@tabler/icons/gavel.svg')
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text tag='span' weight='medium' size='sm' theme='inherit'>
|
<Text tag='span' weight='medium' size='sm' theme='inherit'>
|
||||||
{isAdmin
|
{isOwner
|
||||||
? <FormattedMessage id='group.role.admin' defaultMessage='Admin' />
|
? <FormattedMessage id='group.role.admin' defaultMessage='Admin' />
|
||||||
: <FormattedMessage id='group.role.moderator' defaultMessage='Moderator' />}
|
: <FormattedMessage id='group.role.moderator' defaultMessage='Moderator' />}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -1,26 +1,22 @@
|
||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import GroupAvatar from 'soapbox/components/groups/group-avatar';
|
import GroupAvatar from 'soapbox/components/groups/group-avatar';
|
||||||
import { Button, HStack, Stack, Text } from 'soapbox/components/ui';
|
import { HStack, Stack, Text } from 'soapbox/components/ui';
|
||||||
|
import GroupActionButton from 'soapbox/features/group/components/group-action-button';
|
||||||
import GroupMemberCount from 'soapbox/features/group/components/group-member-count';
|
import GroupMemberCount from 'soapbox/features/group/components/group-member-count';
|
||||||
import GroupPrivacy from 'soapbox/features/group/components/group-privacy';
|
import GroupPrivacy from 'soapbox/features/group/components/group-privacy';
|
||||||
import { useJoinGroup } from 'soapbox/hooks/api';
|
|
||||||
import { Group as GroupEntity } from 'soapbox/types/entities';
|
import type { Group } from 'soapbox/schemas';
|
||||||
|
|
||||||
interface IGroup {
|
interface IGroup {
|
||||||
group: GroupEntity
|
group: Group
|
||||||
width?: number
|
width?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupGridItem = forwardRef((props: IGroup, ref: React.ForwardedRef<HTMLDivElement>) => {
|
const GroupGridItem = forwardRef((props: IGroup, ref: React.ForwardedRef<HTMLDivElement>) => {
|
||||||
const { group, width = 'auto' } = props;
|
const { group, width = 'auto' } = props;
|
||||||
|
|
||||||
const joinGroup = useJoinGroup(group);
|
|
||||||
|
|
||||||
const onJoinGroup = () => joinGroup.mutate(group);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={group.id}
|
key={group.id}
|
||||||
|
@ -71,16 +67,7 @@ const GroupGridItem = forwardRef((props: IGroup, ref: React.ForwardedRef<HTMLDiv
|
||||||
</Stack>
|
</Stack>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Button
|
<GroupActionButton group={group} />
|
||||||
theme='primary'
|
|
||||||
block
|
|
||||||
onClick={onJoinGroup}
|
|
||||||
disabled={joinGroup.isLoading}
|
|
||||||
>
|
|
||||||
{group.locked
|
|
||||||
? <FormattedMessage id='group.join.private' defaultMessage='Request Access' />
|
|
||||||
: <FormattedMessage id='group.join.public' defaultMessage='Join Group' />}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { FormattedMessage } from 'react-intl';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import GroupAvatar from 'soapbox/components/groups/group-avatar';
|
import GroupAvatar from 'soapbox/components/groups/group-avatar';
|
||||||
import { Button, HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||||
import { useJoinGroup } from 'soapbox/hooks/api';
|
import GroupActionButton from 'soapbox/features/group/components/group-action-button';
|
||||||
import { Group as GroupEntity } from 'soapbox/types/entities';
|
import { Group as GroupEntity } from 'soapbox/types/entities';
|
||||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||||
|
|
||||||
|
@ -16,10 +16,6 @@ interface IGroup {
|
||||||
const GroupListItem = (props: IGroup) => {
|
const GroupListItem = (props: IGroup) => {
|
||||||
const { group, withJoinAction = true } = props;
|
const { group, withJoinAction = true } = props;
|
||||||
|
|
||||||
const joinGroup = useJoinGroup(group);
|
|
||||||
|
|
||||||
const onJoinGroup = () => joinGroup.mutate(group);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
alignItems='center'
|
alignItems='center'
|
||||||
|
@ -74,11 +70,7 @@ const GroupListItem = (props: IGroup) => {
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{withJoinAction && (
|
{withJoinAction && (
|
||||||
<Button theme='primary' onClick={onJoinGroup} disabled={joinGroup.isLoading}>
|
<GroupActionButton group={group} />
|
||||||
{group.locked
|
|
||||||
? <FormattedMessage id='group.join.private' defaultMessage='Request Access' />
|
|
||||||
: <FormattedMessage id='group.join.public' defaultMessage='Join Group' />}
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
|
||||||
import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso';
|
import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso';
|
||||||
|
|
||||||
import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||||
import { useGroupSearch } from 'soapbox/queries/groups/search';
|
import { useGroupSearch } from 'soapbox/hooks/api';
|
||||||
import { Group } from 'soapbox/types/entities';
|
import { Group } from 'soapbox/types/entities';
|
||||||
|
|
||||||
import GroupGridItem from '../group-grid-item';
|
import GroupGridItem from '../group-grid-item';
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
|
||||||
import { Stack } from 'soapbox/components/ui';
|
import { Stack } from 'soapbox/components/ui';
|
||||||
import PlaceholderGroupSearch from 'soapbox/features/placeholder/components/placeholder-group-search';
|
import PlaceholderGroupSearch from 'soapbox/features/placeholder/components/placeholder-group-search';
|
||||||
import { useDebounce, useOwnAccount } from 'soapbox/hooks';
|
import { useDebounce, useOwnAccount } from 'soapbox/hooks';
|
||||||
import { useGroupSearch } from 'soapbox/queries/groups/search';
|
import { useGroupSearch } from 'soapbox/hooks/api';
|
||||||
import { saveGroupSearch } from 'soapbox/utils/groups';
|
import { saveGroupSearch } from 'soapbox/utils/groups';
|
||||||
|
|
||||||
import Blankslate from './blankslate';
|
import Blankslate from './blankslate';
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||||
|
import { groupSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import { useApi } from '../../useApi';
|
||||||
|
import { useFeatures } from '../../useFeatures';
|
||||||
|
|
||||||
|
import { useGroupRelationships } from './useGroups';
|
||||||
|
|
||||||
|
import type { Group } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function useGroupSearch(search: string) {
|
||||||
|
const api = useApi();
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
|
const { entities, ...result } = useEntities<Group>(
|
||||||
|
[Entities.GROUPS, 'discover', 'search', search],
|
||||||
|
() => api.get('/api/v1/groups/search', {
|
||||||
|
params: {
|
||||||
|
q: search,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ enabled: features.groupsDiscovery && !!search, schema: groupSchema },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
|
||||||
|
|
||||||
|
const groups = entities.map((group) => ({
|
||||||
|
...group,
|
||||||
|
relationship: relationships[group.id] || null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
groups,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useGroupSearch };
|
|
@ -2,9 +2,13 @@ import { Entities } from 'soapbox/entity-store/entities';
|
||||||
import { useEntityActions } from 'soapbox/entity-store/hooks';
|
import { useEntityActions } from 'soapbox/entity-store/hooks';
|
||||||
import { groupRelationshipSchema } from 'soapbox/schemas';
|
import { groupRelationshipSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import { useGroups } from './useGroups';
|
||||||
|
|
||||||
import type { Group, GroupRelationship } from 'soapbox/schemas';
|
import type { Group, GroupRelationship } from 'soapbox/schemas';
|
||||||
|
|
||||||
function useJoinGroup(group: Group) {
|
function useJoinGroup(group: Group) {
|
||||||
|
const { invalidate } = useGroups();
|
||||||
|
|
||||||
const { createEntity, isLoading } = useEntityActions<GroupRelationship>(
|
const { createEntity, isLoading } = useEntityActions<GroupRelationship>(
|
||||||
[Entities.GROUP_RELATIONSHIPS, group.id],
|
[Entities.GROUP_RELATIONSHIPS, group.id],
|
||||||
{ post: `/api/v1/groups/${group.id}/join` },
|
{ post: `/api/v1/groups/${group.id}/join` },
|
||||||
|
@ -14,6 +18,7 @@ function useJoinGroup(group: Group) {
|
||||||
return {
|
return {
|
||||||
mutate: createEntity,
|
mutate: createEntity,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
invalidate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
import { Entities } from 'soapbox/entity-store/entities';
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
import { useEntityActions } from 'soapbox/entity-store/hooks';
|
import { useEntityActions } from 'soapbox/entity-store/hooks';
|
||||||
import { Group, GroupRelationship, groupRelationshipSchema } from 'soapbox/schemas';
|
import { groupRelationshipSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import { useGroups } from './useGroups';
|
||||||
|
|
||||||
|
import type { Group, GroupRelationship } from 'soapbox/schemas';
|
||||||
|
|
||||||
function useLeaveGroup(group: Group) {
|
function useLeaveGroup(group: Group) {
|
||||||
|
const { invalidate } = useGroups();
|
||||||
|
|
||||||
const { createEntity, isLoading } = useEntityActions<GroupRelationship>(
|
const { createEntity, isLoading } = useEntityActions<GroupRelationship>(
|
||||||
[Entities.GROUP_RELATIONSHIPS, group.id],
|
[Entities.GROUP_RELATIONSHIPS, group.id],
|
||||||
{ post: `/api/v1/groups/${group.id}/leave` },
|
{ post: `/api/v1/groups/${group.id}/leave` },
|
||||||
|
@ -12,6 +18,7 @@ function useLeaveGroup(group: Group) {
|
||||||
return {
|
return {
|
||||||
mutate: createEntity,
|
mutate: createEntity,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
invalidate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ export { useCancelMembershipRequest } from './groups/useCancelMembershipRequest'
|
||||||
export { useDeleteGroup } from './groups/useDeleteGroup';
|
export { useDeleteGroup } from './groups/useDeleteGroup';
|
||||||
export { useDemoteGroupMember } from './groups/useDemoteGroupMember';
|
export { useDemoteGroupMember } from './groups/useDemoteGroupMember';
|
||||||
export { useGroup, useGroups } from './groups/useGroups';
|
export { useGroup, useGroups } from './groups/useGroups';
|
||||||
|
export { useGroupSearch } from './groups/useGroupSearch';
|
||||||
export { useJoinGroup } from './groups/useJoinGroup';
|
export { useJoinGroup } from './groups/useJoinGroup';
|
||||||
export { useLeaveGroup } from './groups/useLeaveGroup';
|
export { useLeaveGroup } from './groups/useLeaveGroup';
|
||||||
export { usePromoteGroupMember } from './groups/usePromoteGroupMember';
|
export { usePromoteGroupMember } from './groups/usePromoteGroupMember';
|
||||||
|
|
|
@ -8,13 +8,15 @@ import {
|
||||||
fromJS,
|
fromJS,
|
||||||
} from 'immutable';
|
} from 'immutable';
|
||||||
|
|
||||||
|
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||||
|
|
||||||
export const GroupRelationshipRecord = ImmutableRecord({
|
export const GroupRelationshipRecord = ImmutableRecord({
|
||||||
id: '',
|
id: '',
|
||||||
blocked_by: false,
|
blocked_by: false,
|
||||||
member: false,
|
member: false,
|
||||||
notifying: null,
|
notifying: null,
|
||||||
requested: false,
|
requested: false,
|
||||||
role: null as 'admin' | 'moderator' | 'user' | null,
|
role: 'user' as GroupRoles,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const normalizeGroupRelationship = (relationship: Record<string, any>) => {
|
export const normalizeGroupRelationship = (relationship: Record<string, any>) => {
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { useApi } from 'soapbox/hooks';
|
|
||||||
import { normalizeAccount } from 'soapbox/normalizers';
|
|
||||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
|
||||||
|
|
||||||
const GroupMemberKeys = {
|
|
||||||
members: (id: string, role: string) => ['group', id, role] as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
const useGroupMembers = (groupId: string, role: GroupRoles) => {
|
|
||||||
const api = useApi();
|
|
||||||
|
|
||||||
const getQuery = async () => {
|
|
||||||
const { data } = await api.get(`/api/v1/groups/${groupId}/memberships`, {
|
|
||||||
params: {
|
|
||||||
role,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = data.map((member: any) => {
|
|
||||||
return {
|
|
||||||
...member,
|
|
||||||
account: normalizeAccount(member.account),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
return useQuery(
|
|
||||||
GroupMemberKeys.members(groupId, role),
|
|
||||||
getQuery,
|
|
||||||
{
|
|
||||||
placeholderData: [],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { useGroupMembers };
|
|
|
@ -1,10 +1,12 @@
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
|
||||||
|
import { GroupRoles } from './group-member';
|
||||||
|
|
||||||
const groupRelationshipSchema = z.object({
|
const groupRelationshipSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
member: z.boolean().catch(false),
|
member: z.boolean().catch(false),
|
||||||
requested: z.boolean().catch(false),
|
requested: z.boolean().catch(false),
|
||||||
role: z.string().nullish().catch(null),
|
role: z.nativeEnum(GroupRoles).catch(GroupRoles.USER),
|
||||||
blocked_by: z.boolean().catch(false),
|
blocked_by: z.boolean().catch(false),
|
||||||
notifying: z.boolean().nullable().catch(null),
|
notifying: z.boolean().nullable().catch(null),
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue