kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Improve Group Header with latest designs
rodzic
8fd3b99887
commit
3cc4f8b64b
|
@ -0,0 +1,130 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { render, screen } from 'soapbox/jest/test-helpers';
|
||||||
|
import { normalizeGroup, normalizeGroupRelationship } from 'soapbox/normalizers';
|
||||||
|
import { Group } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
import GroupActionButton from '../group-action-button';
|
||||||
|
|
||||||
|
let group: Group;
|
||||||
|
|
||||||
|
describe('<GroupActionButton />', () => {
|
||||||
|
describe('with no group relationship', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
group = normalizeGroup({
|
||||||
|
relationship: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with a private group', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
group = group.set('locked', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the Request Access button', () => {
|
||||||
|
render(<GroupActionButton group={group} />);
|
||||||
|
|
||||||
|
expect(screen.getByRole('button')).toHaveTextContent('Request Access');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with a public group', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
group = group.set('locked', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the Join Group button', () => {
|
||||||
|
render(<GroupActionButton group={group} />);
|
||||||
|
|
||||||
|
expect(screen.getByRole('button')).toHaveTextContent('Join Group');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with no group relationship member', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
group = normalizeGroup({
|
||||||
|
relationship: normalizeGroupRelationship({
|
||||||
|
member: null,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with a private group', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
group = group.set('locked', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the Request Access button', () => {
|
||||||
|
render(<GroupActionButton group={group} />);
|
||||||
|
|
||||||
|
expect(screen.getByRole('button')).toHaveTextContent('Request Access');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with a public group', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
group = group.set('locked', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the Join Group button', () => {
|
||||||
|
render(<GroupActionButton group={group} />);
|
||||||
|
|
||||||
|
expect(screen.getByRole('button')).toHaveTextContent('Join Group');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the user has requested to join', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
group = normalizeGroup({
|
||||||
|
relationship: normalizeGroupRelationship({
|
||||||
|
requested: true,
|
||||||
|
member: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the Cancel Request button', () => {
|
||||||
|
render(<GroupActionButton group={group} />);
|
||||||
|
|
||||||
|
expect(screen.getByRole('button')).toHaveTextContent('Cancel Request');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the user is an Admin', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
group = normalizeGroup({
|
||||||
|
relationship: normalizeGroupRelationship({
|
||||||
|
requested: false,
|
||||||
|
member: true,
|
||||||
|
role: 'admin',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the Manage Group button', () => {
|
||||||
|
render(<GroupActionButton group={group} />);
|
||||||
|
|
||||||
|
expect(screen.getByRole('button')).toHaveTextContent('Manage Group');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the user is just a member', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
group = normalizeGroup({
|
||||||
|
relationship: normalizeGroupRelationship({
|
||||||
|
requested: false,
|
||||||
|
member: true,
|
||||||
|
role: 'user',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the Leave Group button', () => {
|
||||||
|
render(<GroupActionButton group={group} />);
|
||||||
|
|
||||||
|
expect(screen.getByRole('button')).toHaveTextContent('Leave Group');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,69 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { render, screen } from 'soapbox/jest/test-helpers';
|
||||||
|
import { normalizeGroup } from 'soapbox/normalizers';
|
||||||
|
import { Group } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
import GroupMemberCount from '../group-member-count';
|
||||||
|
|
||||||
|
let group: Group;
|
||||||
|
|
||||||
|
describe('<GroupMemberCount />', () => {
|
||||||
|
describe('without support for "members_count"', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
group = normalizeGroup({
|
||||||
|
members_count: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null', () => {
|
||||||
|
render(<GroupMemberCount group={group} />);
|
||||||
|
|
||||||
|
expect(screen.queryAllByTestId('group-member-count')).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with support for "members_count"', () => {
|
||||||
|
describe('with 1 member', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
group = normalizeGroup({
|
||||||
|
members_count: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render correctly', () => {
|
||||||
|
render(<GroupMemberCount group={group} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('group-member-count').textContent).toEqual('1 member');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with 2 members', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
group = normalizeGroup({
|
||||||
|
members_count: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render correctly', () => {
|
||||||
|
render(<GroupMemberCount group={group} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('group-member-count').textContent).toEqual('2 members');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with 1000 members', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
group = normalizeGroup({
|
||||||
|
members_count: 1000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render correctly', () => {
|
||||||
|
render(<GroupMemberCount group={group} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('group-member-count').textContent).toEqual('1k members');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,39 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { render, screen } from 'soapbox/jest/test-helpers';
|
||||||
|
import { normalizeGroup } from 'soapbox/normalizers';
|
||||||
|
import { Group } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
import GroupPrivacy from '../group-privacy';
|
||||||
|
|
||||||
|
let group: Group;
|
||||||
|
|
||||||
|
describe('<GroupPrivacy />', () => {
|
||||||
|
describe('with a Private group', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
group = normalizeGroup({
|
||||||
|
locked: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the correct text', () => {
|
||||||
|
render(<GroupPrivacy group={group} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('group-privacy')).toHaveTextContent('Private');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with a Public group', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
group = normalizeGroup({
|
||||||
|
locked: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the correct text', () => {
|
||||||
|
render(<GroupPrivacy group={group} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('group-privacy')).toHaveTextContent('Public');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,83 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { joinGroup, leaveGroup } from 'soapbox/actions/groups';
|
||||||
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
|
import { Button } from 'soapbox/components/ui';
|
||||||
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
|
import { Group } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
interface IGroupActionButton {
|
||||||
|
group: Group
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
confirmationHeading: { id: 'confirmations.leave_group.heading', defaultMessage: 'Leave group' },
|
||||||
|
confirmationMessage: { id: 'confirmations.leave_group.message', defaultMessage: 'You are about to leave the group. Do you want to continue?' },
|
||||||
|
confirmationConfirm: { id: 'confirmations.leave_group.confirm', defaultMessage: 'Leave' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const GroupActionButton = ({ group }: IGroupActionButton) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const isNonMember = !group.relationship || !group.relationship.member;
|
||||||
|
const isRequested = group.relationship?.requested;
|
||||||
|
const isAdmin = group.relationship?.role === 'admin';
|
||||||
|
|
||||||
|
const onJoinGroup = () => dispatch(joinGroup(group.id));
|
||||||
|
|
||||||
|
const onLeaveGroup = () =>
|
||||||
|
dispatch(openModal('CONFIRM', {
|
||||||
|
heading: intl.formatMessage(messages.confirmationHeading),
|
||||||
|
message: intl.formatMessage(messages.confirmationMessage),
|
||||||
|
confirm: intl.formatMessage(messages.confirmationConfirm),
|
||||||
|
onConfirm: () => dispatch(leaveGroup(group.id)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (isNonMember) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
theme='primary'
|
||||||
|
onClick={onJoinGroup}
|
||||||
|
>
|
||||||
|
{group.locked
|
||||||
|
? <FormattedMessage id='group.join.private' defaultMessage='Request Access' />
|
||||||
|
: <FormattedMessage id='group.join.public' defaultMessage='Join Group' />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRequested) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
theme='secondary'
|
||||||
|
onClick={onLeaveGroup}
|
||||||
|
>
|
||||||
|
<FormattedMessage id='group.cancel_request' defaultMessage='Cancel Request' />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAdmin) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
theme='secondary'
|
||||||
|
to={`/groups/${group.id}/manage`}
|
||||||
|
>
|
||||||
|
<FormattedMessage id='group.manage' defaultMessage='Manage Group' />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
theme='secondary'
|
||||||
|
onClick={onLeaveGroup}
|
||||||
|
>
|
||||||
|
<FormattedMessage id='group.leave' defaultMessage='Leave Group' />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GroupActionButton;
|
|
@ -1,22 +1,23 @@
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { joinGroup, leaveGroup } from 'soapbox/actions/groups';
|
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import StillImage from 'soapbox/components/still-image';
|
import StillImage from 'soapbox/components/still-image';
|
||||||
import { Avatar, Button, HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
import { Avatar, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||||
import { useAppDispatch } from 'soapbox/hooks';
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
import { normalizeAttachment } from 'soapbox/normalizers';
|
import { normalizeAttachment } from 'soapbox/normalizers';
|
||||||
import { isDefaultHeader } from 'soapbox/utils/accounts';
|
import { isDefaultHeader } from 'soapbox/utils/accounts';
|
||||||
|
|
||||||
|
import GroupActionButton from './group-action-button';
|
||||||
|
import GroupMemberCount from './group-member-count';
|
||||||
|
import GroupPrivacy from './group-privacy';
|
||||||
|
import GroupRelationship from './group-relationship';
|
||||||
|
|
||||||
import type { Group } from 'soapbox/types/entities';
|
import type { Group } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
header: { id: 'group.header.alt', defaultMessage: 'Group header' },
|
header: { id: 'group.header.alt', defaultMessage: 'Group header' },
|
||||||
confirmationHeading: { id: 'confirmations.leave_group.heading', defaultMessage: 'Leave group' },
|
|
||||||
confirmationMessage: { id: 'confirmations.leave_group.message', defaultMessage: 'You are about to leave the group. Do you want to continue?' },
|
|
||||||
confirmationConfirm: { id: 'confirmations.leave_group.confirm', defaultMessage: 'Leave' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IGroupHeader {
|
interface IGroupHeader {
|
||||||
|
@ -47,16 +48,6 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onJoinGroup = () => dispatch(joinGroup(group.id));
|
|
||||||
|
|
||||||
const onLeaveGroup = () =>
|
|
||||||
dispatch(openModal('CONFIRM', {
|
|
||||||
heading: intl.formatMessage(messages.confirmationHeading),
|
|
||||||
message: intl.formatMessage(messages.confirmationMessage),
|
|
||||||
confirm: intl.formatMessage(messages.confirmationConfirm),
|
|
||||||
onConfirm: () => dispatch(leaveGroup(group.id)),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const onAvatarClick = () => {
|
const onAvatarClick = () => {
|
||||||
const avatar = normalizeAttachment({
|
const avatar = normalizeAttachment({
|
||||||
type: 'image',
|
type: 'image',
|
||||||
|
@ -95,6 +86,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
||||||
<StillImage
|
<StillImage
|
||||||
src={group.header}
|
src={group.header}
|
||||||
alt={intl.formatMessage(messages.header)}
|
alt={intl.formatMessage(messages.header)}
|
||||||
|
className='h-32 w-full bg-gray-200 object-center dark:bg-gray-900/50 md:rounded-t-xl lg:h-52'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -110,95 +102,40 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
||||||
return header;
|
return header;
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeActionButton = () => {
|
|
||||||
if (!group.relationship || !group.relationship.member) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
theme='primary'
|
|
||||||
onClick={onJoinGroup}
|
|
||||||
>
|
|
||||||
{group.locked
|
|
||||||
? <FormattedMessage id='group.join.private' defaultMessage='Request to Join' />
|
|
||||||
: <FormattedMessage id='group.join.public' defaultMessage='Join Group' />}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group.relationship.requested) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
theme='secondary'
|
|
||||||
onClick={onLeaveGroup}
|
|
||||||
>
|
|
||||||
<FormattedMessage id='group.cancel_request' defaultMessage='Cancel request' />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group.relationship?.role === 'admin') {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
theme='secondary'
|
|
||||||
to={`/groups/${group.id}/manage`}
|
|
||||||
>
|
|
||||||
<FormattedMessage id='group.manage' defaultMessage='Manage group' />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
theme='secondary'
|
|
||||||
onClick={onLeaveGroup}
|
|
||||||
>
|
|
||||||
<FormattedMessage id='group.leave' defaultMessage='Leave group' />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const actionButton = makeActionButton();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='-mx-4 -mt-4'>
|
<div className='-mx-4 -mt-4'>
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<div className='relative isolate flex h-32 w-full flex-col justify-center overflow-hidden bg-gray-200 dark:bg-gray-900/50 md:rounded-t-xl lg:h-[200px]'>
|
{renderHeader()}
|
||||||
{renderHeader()}
|
|
||||||
</div>
|
|
||||||
<div className='absolute left-1/2 bottom-0 -translate-x-1/2 translate-y-1/2'>
|
<div className='absolute left-1/2 bottom-0 -translate-x-1/2 translate-y-1/2'>
|
||||||
<a href={group.avatar} onClick={handleAvatarClick} target='_blank'>
|
<a href={group.avatar} onClick={handleAvatarClick} target='_blank'>
|
||||||
<Avatar className='ring-[3px] ring-white dark:ring-primary-900' src={group.avatar} size={72} />
|
<Avatar
|
||||||
|
className='ring-[3px] ring-white dark:ring-primary-900'
|
||||||
|
src={group.avatar}
|
||||||
|
size={80}
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Stack className='p-3 pt-12' alignItems='center' space={2}>
|
<Stack alignItems='center' space={3} className='mt-10 py-4'>
|
||||||
<Text className='mb-1' size='xl' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
<Text
|
||||||
<HStack className='text-gray-700 dark:text-gray-600' space={3} wrap>
|
size='xl'
|
||||||
{group.relationship?.role === 'admin' ? (
|
weight='bold'
|
||||||
<HStack space={1} alignItems='center'>
|
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
|
||||||
<Icon className='h-4 w-4' src={require('@tabler/icons/users.svg')} />
|
/>
|
||||||
<span><FormattedMessage id='group.role.admin' defaultMessage='Admin' /></span>
|
|
||||||
</HStack>
|
<Stack space={1}>
|
||||||
) : group.relationship?.role === 'moderator' && (
|
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
||||||
<HStack space={1} alignItems='center'>
|
<GroupRelationship group={group} />
|
||||||
<Icon className='h-4 w-4' src={require('@tabler/icons/gavel.svg')} />
|
<GroupPrivacy group={group} />
|
||||||
<span><FormattedMessage id='group.role.moderator' defaultMessage='Moderator' /></span>
|
<GroupMemberCount group={group} />
|
||||||
</HStack>
|
</HStack>
|
||||||
)}
|
|
||||||
{group.locked ? (
|
<Text theme='muted' dangerouslySetInnerHTML={{ __html: group.note_emojified }} />
|
||||||
<HStack space={1} alignItems='center'>
|
</Stack>
|
||||||
<Icon className='h-4 w-4' src={require('@tabler/icons/lock.svg')} />
|
|
||||||
<span><FormattedMessage id='group.privacy.locked' defaultMessage='Private' /></span>
|
<GroupActionButton group={group} />
|
||||||
</HStack>
|
|
||||||
) : (
|
|
||||||
<HStack space={1} alignItems='center'>
|
|
||||||
<Icon className='h-4 w-4' src={require('@tabler/icons/world.svg')} />
|
|
||||||
<span><FormattedMessage id='group.privacy.public' defaultMessage='Public' /></span>
|
|
||||||
</HStack>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
<Text theme='muted' dangerouslySetInnerHTML={{ __html: group.note_emojified }} />
|
|
||||||
{actionButton}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Text } from 'soapbox/components/ui';
|
||||||
|
import { Group } from 'soapbox/types/entities';
|
||||||
|
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||||
|
|
||||||
|
interface IGroupMemberCount {
|
||||||
|
group: Group
|
||||||
|
}
|
||||||
|
|
||||||
|
const GroupMemberCount = ({ group }: IGroupMemberCount) => {
|
||||||
|
if (typeof group.members_count === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text theme='inherit' tag='span' size='sm' weight='medium' data-testid='group-member-count'>
|
||||||
|
{shortNumberFormat(group.members_count)}
|
||||||
|
{' '}
|
||||||
|
<FormattedMessage
|
||||||
|
id='groups.discover.search.results.member_count'
|
||||||
|
defaultMessage='{members, plural, one {member} other {members}}'
|
||||||
|
values={{
|
||||||
|
members: group.members_count,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GroupMemberCount;
|
|
@ -0,0 +1,32 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { HStack, Icon, Text } from 'soapbox/components/ui';
|
||||||
|
import { Group } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
interface IGroupPolicy {
|
||||||
|
group: Group
|
||||||
|
}
|
||||||
|
|
||||||
|
const GroupPrivacy = ({ group }: IGroupPolicy) => (
|
||||||
|
<HStack space={1} alignItems='center' data-testid='group-privacy'>
|
||||||
|
<Icon
|
||||||
|
className='h-4 w-4'
|
||||||
|
src={
|
||||||
|
group.locked
|
||||||
|
? require('@tabler/icons/lock.svg')
|
||||||
|
: require('@tabler/icons/world.svg')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text theme='inherit' tag='span' size='sm' weight='medium'>
|
||||||
|
{group.locked ? (
|
||||||
|
<FormattedMessage id='group.privacy.locked' defaultMessage='Private' />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage id='group.privacy.public' defaultMessage='Public' />
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default GroupPrivacy;
|
|
@ -0,0 +1,39 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { HStack, Icon, Text } from 'soapbox/components/ui';
|
||||||
|
import { Group } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
interface IGroupRelationship {
|
||||||
|
group: Group
|
||||||
|
}
|
||||||
|
|
||||||
|
const GroupRelationship = ({ group }: IGroupRelationship) => {
|
||||||
|
const isAdmin = group.relationship?.role === 'admin';
|
||||||
|
const isModerator = group.relationship?.role === 'moderator';
|
||||||
|
|
||||||
|
if (!isAdmin || !isModerator) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack space={1} alignItems='center'>
|
||||||
|
<Icon
|
||||||
|
className='h-4 w-4'
|
||||||
|
src={
|
||||||
|
isAdmin
|
||||||
|
? require('@tabler/icons/users.svg')
|
||||||
|
: require('@tabler/icons/gavel.svg')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text tag='span' weight='medium' size='sm' theme='inherit'>
|
||||||
|
{isAdmin
|
||||||
|
? <FormattedMessage id='group.role.admin' defaultMessage='Admin' />
|
||||||
|
: <FormattedMessage id='group.role.moderator' defaultMessage='Moderator' />}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GroupRelationship;
|
|
@ -3,7 +3,6 @@ import { FormattedMessage } from 'react-intl';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { groupCompose } from 'soapbox/actions/compose';
|
import { groupCompose } from 'soapbox/actions/compose';
|
||||||
import { fetchGroup } from 'soapbox/actions/groups';
|
|
||||||
import { connectGroupStream } from 'soapbox/actions/streaming';
|
import { connectGroupStream } from 'soapbox/actions/streaming';
|
||||||
import { expandGroupTimeline } from 'soapbox/actions/timelines';
|
import { expandGroupTimeline } from 'soapbox/actions/timelines';
|
||||||
import { Avatar, HStack, Stack } from 'soapbox/components/ui';
|
import { Avatar, HStack, Stack } from 'soapbox/components/ui';
|
||||||
|
@ -31,7 +30,6 @@ const GroupTimeline: React.FC<IGroupTimeline> = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchGroup(groupId));
|
|
||||||
dispatch(expandGroupTimeline(groupId));
|
dispatch(expandGroupTimeline(groupId));
|
||||||
|
|
||||||
dispatch(groupCompose(`group:${groupId}`, groupId));
|
dispatch(groupCompose(`group:${groupId}`, groupId));
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { Avatar, Button, HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
import { Avatar, Button, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||||
|
import GroupMemberCount from 'soapbox/features/group/components/group-member-count';
|
||||||
|
import GroupPrivacy from 'soapbox/features/group/components/group-privacy';
|
||||||
import { Group as GroupEntity } from 'soapbox/types/entities';
|
import { Group as GroupEntity } from 'soapbox/types/entities';
|
||||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
|
||||||
|
|
||||||
interface IGroup {
|
interface IGroup {
|
||||||
group: GroupEntity
|
group: GroupEntity
|
||||||
|
@ -21,75 +23,56 @@ const Group = forwardRef((props: IGroup, ref: React.ForwardedRef<HTMLDivElement>
|
||||||
width,
|
width,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack
|
<Link to={`/groups/${group.id}`}>
|
||||||
className='aspect-w-10 aspect-h-7 h-full w-full overflow-hidden rounded-lg'
|
<Stack
|
||||||
ref={ref}
|
className='aspect-w-10 aspect-h-7 h-full w-full overflow-hidden rounded-lg'
|
||||||
style={{ minHeight: 180 }}
|
ref={ref}
|
||||||
>
|
style={{ minHeight: 180 }}
|
||||||
{group.header && (
|
>
|
||||||
<img
|
{group.header && (
|
||||||
src={group.header}
|
<img
|
||||||
alt='Group cover'
|
src={group.header}
|
||||||
className='absolute inset-0 object-cover'
|
alt='Group cover'
|
||||||
/>
|
className='absolute inset-0 object-cover'
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Stack justifyContent='end' className='z-10 p-4 text-white' space={3}>
|
<Stack justifyContent='end' className='z-10 p-4 text-white' space={3}>
|
||||||
<Avatar
|
<Avatar
|
||||||
className='ring-2 ring-white'
|
className='ring-2 ring-white'
|
||||||
src={group.avatar}
|
src={group.avatar}
|
||||||
size={44}
|
size={44}
|
||||||
/>
|
|
||||||
|
|
||||||
<Stack space={1}>
|
|
||||||
<Text
|
|
||||||
weight='bold'
|
|
||||||
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
|
|
||||||
theme='inherit'
|
|
||||||
truncate
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<HStack space={1} alignItems='center'>
|
<Stack space={1}>
|
||||||
<Icon
|
<Text
|
||||||
className='h-4.5 w-4.5'
|
weight='bold'
|
||||||
src={group.locked ? require('@tabler/icons/lock.svg') : require('@tabler/icons/world.svg')}
|
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
|
||||||
|
theme='inherit'
|
||||||
|
truncate
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{typeof group.members_count === 'undefined' ? (
|
<HStack alignItems='center' space={1}>
|
||||||
<Text theme='inherit' tag='span' size='sm'>
|
<GroupPrivacy group={group} />
|
||||||
{group.locked ? (
|
<span>•</span>
|
||||||
<FormattedMessage id='group.privacy.locked' defaultMessage='Private' />
|
<GroupMemberCount group={group} />
|
||||||
) : (
|
</HStack>
|
||||||
<FormattedMessage id='group.privacy.public' defaultMessage='Public' />
|
</Stack>
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<Text theme='inherit' tag='span' size='sm'>
|
|
||||||
{shortNumberFormat(group.members_count)}
|
|
||||||
{' '}
|
|
||||||
<FormattedMessage
|
|
||||||
id='groups.discover.search.results.member_count'
|
|
||||||
defaultMessage='{members, plural, one {member} other {members}}'
|
|
||||||
values={{
|
|
||||||
members: group.members_count,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className='absolute inset-x-0 bottom-0 z-0 flex justify-center rounded-b-lg bg-gradient-to-t from-gray-900 to-transparent pt-12 pb-8 transition-opacity duration-500'
|
className='absolute inset-x-0 bottom-0 z-0 flex justify-center rounded-b-lg bg-gradient-to-t from-gray-900 to-transparent pt-12 pb-8 transition-opacity duration-500'
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</Link>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
theme='primary'
|
theme='primary'
|
||||||
block
|
block
|
||||||
>
|
>
|
||||||
{group.locked ? 'Request to Join' : 'Join Group'}
|
{group.locked
|
||||||
|
? <FormattedMessage id='group.join.private' defaultMessage='Request Access' />
|
||||||
|
: <FormattedMessage id='group.join.public' defaultMessage='Join Group' />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -96,7 +96,7 @@ export default (props: Props) => {
|
||||||
|
|
||||||
<Button theme='primary'>
|
<Button theme='primary'>
|
||||||
{group.locked
|
{group.locked
|
||||||
? <FormattedMessage id='group.join.private' defaultMessage='Request to Join' />
|
? <FormattedMessage id='group.join.private' defaultMessage='Request Access' />
|
||||||
: <FormattedMessage id='group.join.public' defaultMessage='Join Group' />}
|
: <FormattedMessage id='group.join.public' defaultMessage='Join Group' />}
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
|
@ -745,7 +745,7 @@
|
||||||
"gdpr.title": "{siteTitle} uses cookies",
|
"gdpr.title": "{siteTitle} uses cookies",
|
||||||
"getting_started.open_source_notice": "{code_name} is open source software. You can contribute or report issues at {code_link} (v{code_version}).",
|
"getting_started.open_source_notice": "{code_name} is open source software. You can contribute or report issues at {code_link} (v{code_version}).",
|
||||||
"group.admin_subheading": "Group administrators",
|
"group.admin_subheading": "Group administrators",
|
||||||
"group.cancel_request": "Cancel request",
|
"group.cancel_request": "Cancel Request",
|
||||||
"group.group_mod_authorize": "Accept",
|
"group.group_mod_authorize": "Accept",
|
||||||
"group.group_mod_authorize.success": "Accepted @{name} to group",
|
"group.group_mod_authorize.success": "Accepted @{name} to group",
|
||||||
"group.group_mod_block": "Block @{name} from group",
|
"group.group_mod_block": "Block @{name} from group",
|
||||||
|
@ -763,13 +763,13 @@
|
||||||
"group.group_mod_unblock": "Unblock",
|
"group.group_mod_unblock": "Unblock",
|
||||||
"group.group_mod_unblock.success": "Unblocked @{name} from group",
|
"group.group_mod_unblock.success": "Unblocked @{name} from group",
|
||||||
"group.header.alt": "Group header",
|
"group.header.alt": "Group header",
|
||||||
"group.join.private": "Request to Join",
|
"group.join.private": "Request Access",
|
||||||
"group.join.public": "Join Group",
|
"group.join.public": "Join Group",
|
||||||
"group.join.request_success": "Requested to join the group",
|
"group.join.request_success": "Requested to join the group",
|
||||||
"group.join.success": "Joined the group",
|
"group.join.success": "Joined the group",
|
||||||
"group.leave": "Leave group",
|
"group.leave": "Leave Group",
|
||||||
"group.leave.success": "Left the group",
|
"group.leave.success": "Left the group",
|
||||||
"group.manage": "Manage group",
|
"group.manage": "Manage Group",
|
||||||
"group.moderator_subheading": "Group moderators",
|
"group.moderator_subheading": "Group moderators",
|
||||||
"group.privacy.locked": "Private",
|
"group.privacy.locked": "Private",
|
||||||
"group.privacy.public": "Public",
|
"group.privacy.public": "Public",
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import React, { useCallback, useEffect } from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { useRouteMatch } from 'react-router-dom';
|
import { useRouteMatch } from 'react-router-dom';
|
||||||
|
|
||||||
import { fetchGroup } from 'soapbox/actions/groups';
|
import { Column, Icon, Layout, Stack, Text } from 'soapbox/components/ui';
|
||||||
import MissingIndicator from 'soapbox/components/missing-indicator';
|
|
||||||
import { Column, Layout } from 'soapbox/components/ui';
|
|
||||||
import GroupHeader from 'soapbox/features/group/components/group-header';
|
import GroupHeader from 'soapbox/features/group/components/group-header';
|
||||||
import LinkFooter from 'soapbox/features/ui/components/link-footer';
|
import LinkFooter from 'soapbox/features/ui/components/link-footer';
|
||||||
import BundleContainer from 'soapbox/features/ui/containers/bundle-container';
|
import BundleContainer from 'soapbox/features/ui/containers/bundle-container';
|
||||||
|
@ -13,8 +11,8 @@ import {
|
||||||
GroupMediaPanel,
|
GroupMediaPanel,
|
||||||
SignUpPanel,
|
SignUpPanel,
|
||||||
} from 'soapbox/features/ui/util/async-components';
|
} from 'soapbox/features/ui/util/async-components';
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { useOwnAccount } from 'soapbox/hooks';
|
||||||
import { makeGetGroup } from 'soapbox/selectors';
|
import { useGroup } from 'soapbox/queries/groups';
|
||||||
|
|
||||||
import { Tabs } from '../components/ui';
|
import { Tabs } from '../components/ui';
|
||||||
|
|
||||||
|
@ -34,23 +32,20 @@ interface IGroupPage {
|
||||||
const GroupPage: React.FC<IGroupPage> = ({ params, children }) => {
|
const GroupPage: React.FC<IGroupPage> = ({ params, children }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
const dispatch = useAppDispatch();
|
const me = useOwnAccount();
|
||||||
|
|
||||||
const id = params?.id || '';
|
const id = params?.id || '';
|
||||||
|
|
||||||
const getGroup = useCallback(makeGetGroup(), []);
|
const { group } = useGroup(id);
|
||||||
const group = useAppSelector(state => getGroup(state, id));
|
|
||||||
const me = useAppSelector(state => state.me);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const isNonMember = !group?.relationship || !group.relationship.member;
|
||||||
dispatch(fetchGroup(id));
|
const isPrivate = group?.locked;
|
||||||
}, [id]);
|
|
||||||
|
|
||||||
if ((group as any) === false) {
|
// if ((group as any) === false) {
|
||||||
return (
|
// return (
|
||||||
<MissingIndicator />
|
// <MissingIndicator />
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
|
@ -76,7 +71,18 @@ const GroupPage: React.FC<IGroupPage> = ({ params, children }) => {
|
||||||
activeItem={match.path}
|
activeItem={match.path}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{children}
|
{(isNonMember && isPrivate) ? (
|
||||||
|
<Stack space={4} className='py-10' alignItems='center'>
|
||||||
|
<div className='rounded-full bg-gray-200 p-3'>
|
||||||
|
<Icon src={require('@tabler/icons/eye-off.svg')} className='h-6 w-6 text-gray-600' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Text theme='muted'>
|
||||||
|
Content is only visible to group members
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
) : children}
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
{!me && (
|
{!me && (
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { Group } from 'soapbox/types/entities';
|
||||||
import { flattenPages, PaginatedResult } from 'soapbox/utils/queries';
|
import { flattenPages, PaginatedResult } from 'soapbox/utils/queries';
|
||||||
|
|
||||||
const GroupKeys = {
|
const GroupKeys = {
|
||||||
|
group: (id: string) => ['groups', 'group', id] as const,
|
||||||
myGroups: (userId: string) => ['groups', userId] as const,
|
myGroups: (userId: string) => ['groups', userId] as const,
|
||||||
popularGroups: ['groups', 'popular'] as const,
|
popularGroups: ['groups', 'popular'] as const,
|
||||||
suggestedGroups: ['groups', 'suggested'] as const,
|
suggestedGroups: ['groups', 'suggested'] as const,
|
||||||
|
@ -70,7 +71,7 @@ const usePopularGroups = () => {
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
|
|
||||||
const getQuery = async () => {
|
const getQuery = async () => {
|
||||||
const { data } = await api.get<Group[]>('/api/mock/groups'); // '/api/v1/truth/trends/groups'
|
const { data } = await api.get<Group[]>('/api/v1/groups/search?q=group'); // '/api/v1/truth/trends/groups'
|
||||||
const result = data.map(normalizeGroup);
|
const result = data.map(normalizeGroup);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -109,4 +110,23 @@ const useSuggestedGroups = () => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export { useGroups, usePopularGroups, useSuggestedGroups };
|
const useGroup = (id: string) => {
|
||||||
|
const api = useApi();
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
|
const getGroup = async () => {
|
||||||
|
const { data } = await api.get(`/api/v1/groups/${id}`);
|
||||||
|
return normalizeGroup(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryInfo = useQuery(GroupKeys.group(id), getGroup, {
|
||||||
|
enabled: features.groups && !!id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...queryInfo,
|
||||||
|
group: queryInfo.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useGroups, useGroup, usePopularGroups, useSuggestedGroups };
|
||||||
|
|
Ładowanie…
Reference in New Issue