Add popover when trying to reply to Group status
This commit is contained in:
parent
966fcc617a
commit
1e69812078
5 changed files with 150 additions and 14 deletions
99
app/soapbox/components/groups/popover/group-popover.tsx
Normal file
99
app/soapbox/components/groups/popover/group-popover.tsx
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Button, Divider, HStack, Popover, 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 GroupAvatar from '../group-avatar';
|
||||||
|
|
||||||
|
import type { Group } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
interface IGroupPopoverContainer {
|
||||||
|
children: React.ReactElement<any, string | React.JSXElementConstructor<any>>
|
||||||
|
isEnabled: boolean
|
||||||
|
group: Group
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'group.popover.title', defaultMessage: 'Membership required' },
|
||||||
|
summary: { id: 'group.popover.summary', defaultMessage: 'You must be a member of the group in order to reply to this status.' },
|
||||||
|
action: { id: 'group.popover.action', defaultMessage: 'View Group' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const GroupPopover = (props: IGroupPopoverContainer) => {
|
||||||
|
const { children, group, isEnabled } = props;
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
if (!isEnabled) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
interaction='click'
|
||||||
|
referenceElementClassName='cursor-pointer'
|
||||||
|
content={
|
||||||
|
<Stack space={4} className='w-80'>
|
||||||
|
<Stack
|
||||||
|
className='relative h-60 rounded-lg bg-white dark:border-primary-800 dark:bg-primary-900'
|
||||||
|
data-testid='group-card'
|
||||||
|
>
|
||||||
|
{/* Group Cover Image */}
|
||||||
|
<Stack grow className='relative basis-1/2 rounded-t-lg bg-primary-100 dark:bg-gray-800'>
|
||||||
|
{group.header && (
|
||||||
|
<img
|
||||||
|
className='absolute inset-0 h-full w-full rounded-t-lg object-cover'
|
||||||
|
src={group.header}
|
||||||
|
alt=''
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Group Avatar */}
|
||||||
|
<div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
|
||||||
|
<GroupAvatar group={group} size={64} withRing />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Group Info */}
|
||||||
|
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
|
||||||
|
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
||||||
|
|
||||||
|
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
||||||
|
<GroupPrivacy group={group} />
|
||||||
|
<GroupMemberCount group={group} />
|
||||||
|
</HStack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Stack space={0.5} className='px-4'>
|
||||||
|
<Text weight='semibold'>
|
||||||
|
{intl.formatMessage(messages.title)}
|
||||||
|
</Text>
|
||||||
|
<Text theme='muted'>
|
||||||
|
{intl.formatMessage(messages.summary)}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<div className='px-4 pb-4'>
|
||||||
|
<Link to={`/groups/${group.id}`}>
|
||||||
|
<Button type='button' theme='secondary' block>
|
||||||
|
{intl.formatMessage(messages.action)}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
isFlush
|
||||||
|
children={
|
||||||
|
<div className='inline-block'>{children}</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GroupPopover;
|
|
@ -24,6 +24,8 @@ import { isLocal, isRemote } from 'soapbox/utils/accounts';
|
||||||
import copy from 'soapbox/utils/copy';
|
import copy from 'soapbox/utils/copy';
|
||||||
import { getReactForStatus, reduceEmoji } from 'soapbox/utils/emoji-reacts';
|
import { getReactForStatus, reduceEmoji } from 'soapbox/utils/emoji-reacts';
|
||||||
|
|
||||||
|
import GroupPopover from './groups/popover/group-popover';
|
||||||
|
|
||||||
import type { Menu } from 'soapbox/components/dropdown-menu';
|
import type { Menu } from 'soapbox/components/dropdown-menu';
|
||||||
import type { Account, Group, Status } from 'soapbox/types/entities';
|
import type { Account, Group, Status } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
@ -608,14 +610,19 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
grow={space === 'expand'}
|
grow={space === 'expand'}
|
||||||
onClick={e => e.stopPropagation()}
|
onClick={e => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<StatusActionButton
|
<GroupPopover
|
||||||
title={replyTitle}
|
group={status.group as any}
|
||||||
icon={require('@tabler/icons/message-circle-2.svg')}
|
isEnabled={replyDisabled}
|
||||||
onClick={handleReplyClick}
|
>
|
||||||
count={replyCount}
|
<StatusActionButton
|
||||||
text={withLabels ? intl.formatMessage(messages.reply) : undefined}
|
title={replyTitle}
|
||||||
disabled={replyDisabled}
|
icon={require('@tabler/icons/message-circle-2.svg')}
|
||||||
/>
|
onClick={handleReplyClick}
|
||||||
|
count={replyCount}
|
||||||
|
text={withLabels ? intl.formatMessage(messages.reply) : undefined}
|
||||||
|
disabled={replyDisabled}
|
||||||
|
/>
|
||||||
|
</GroupPopover>
|
||||||
|
|
||||||
{(features.quotePosts && me) ? (
|
{(features.quotePosts && me) ? (
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
|
|
|
@ -1,18 +1,28 @@
|
||||||
import {
|
import {
|
||||||
arrow,
|
arrow,
|
||||||
|
autoPlacement,
|
||||||
FloatingArrow,
|
FloatingArrow,
|
||||||
offset,
|
offset,
|
||||||
useClick,
|
useClick,
|
||||||
useDismiss,
|
useDismiss,
|
||||||
useFloating,
|
useFloating,
|
||||||
|
useHover,
|
||||||
useInteractions,
|
useInteractions,
|
||||||
useTransitionStyles,
|
useTransitionStyles,
|
||||||
} from '@floating-ui/react';
|
} from '@floating-ui/react';
|
||||||
|
import clsx from 'clsx';
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
|
|
||||||
interface IPopover {
|
interface IPopover {
|
||||||
children: React.ReactElement<any, string | React.JSXElementConstructor<any>>
|
children: React.ReactElement<any, string | React.JSXElementConstructor<any>>
|
||||||
|
/** The content of the popover */
|
||||||
content: React.ReactNode
|
content: React.ReactNode
|
||||||
|
/** Should we remove padding on the Popover */
|
||||||
|
isFlush?: boolean
|
||||||
|
/** Should the popover trigger via click or hover */
|
||||||
|
interaction?: 'click' | 'hover'
|
||||||
|
/** Add a class to the reference (trigger) element */
|
||||||
|
referenceElementClassName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,7 +32,7 @@ interface IPopover {
|
||||||
* of information.
|
* of information.
|
||||||
*/
|
*/
|
||||||
const Popover: React.FC<IPopover> = (props) => {
|
const Popover: React.FC<IPopover> = (props) => {
|
||||||
const { children, content } = props;
|
const { children, content, referenceElementClassName, interaction = 'hover', isFlush = false } = props;
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
@ -33,6 +43,9 @@ const Popover: React.FC<IPopover> = (props) => {
|
||||||
onOpenChange: setIsOpen,
|
onOpenChange: setIsOpen,
|
||||||
placement: 'top',
|
placement: 'top',
|
||||||
middleware: [
|
middleware: [
|
||||||
|
autoPlacement({
|
||||||
|
allowedPlacements: ['top', 'bottom'],
|
||||||
|
}),
|
||||||
offset(10),
|
offset(10),
|
||||||
arrow({
|
arrow({
|
||||||
element: arrowRef,
|
element: arrowRef,
|
||||||
|
@ -40,8 +53,6 @@ const Popover: React.FC<IPopover> = (props) => {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const click = useClick(context);
|
|
||||||
const dismiss = useDismiss(context);
|
|
||||||
const { isMounted, styles } = useTransitionStyles(context, {
|
const { isMounted, styles } = useTransitionStyles(context, {
|
||||||
initial: {
|
initial: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
|
@ -53,8 +64,13 @@ const Popover: React.FC<IPopover> = (props) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const click = useClick(context, { enabled: interaction === 'click' });
|
||||||
|
const hover = useHover(context, { enabled: interaction === 'hover' });
|
||||||
|
const dismiss = useDismiss(context);
|
||||||
|
|
||||||
const { getReferenceProps, getFloatingProps } = useInteractions([
|
const { getReferenceProps, getFloatingProps } = useInteractions([
|
||||||
click,
|
click,
|
||||||
|
hover,
|
||||||
dismiss,
|
dismiss,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -63,7 +79,7 @@ const Popover: React.FC<IPopover> = (props) => {
|
||||||
{React.cloneElement(children, {
|
{React.cloneElement(children, {
|
||||||
ref: refs.setReference,
|
ref: refs.setReference,
|
||||||
...getReferenceProps(),
|
...getReferenceProps(),
|
||||||
className: 'cursor-help',
|
className: clsx(children.props.className, referenceElementClassName),
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{(isMounted) && (
|
{(isMounted) && (
|
||||||
|
@ -75,12 +91,22 @@ const Popover: React.FC<IPopover> = (props) => {
|
||||||
left: x ?? 0,
|
left: x ?? 0,
|
||||||
...styles,
|
...styles,
|
||||||
}}
|
}}
|
||||||
className='rounded-lg bg-white p-6 shadow-2xl dark:bg-gray-900 dark:ring-2 dark:ring-primary-700'
|
className={
|
||||||
|
clsx({
|
||||||
|
'z-40 rounded-lg bg-white shadow-2xl dark:bg-gray-900 dark:ring-2 dark:ring-primary-700': true,
|
||||||
|
'p-6': !isFlush,
|
||||||
|
})
|
||||||
|
}
|
||||||
{...getFloatingProps()}
|
{...getFloatingProps()}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
|
||||||
<FloatingArrow ref={arrowRef} context={context} className='fill-white dark:hidden' />
|
<FloatingArrow
|
||||||
|
ref={arrowRef}
|
||||||
|
context={context}
|
||||||
|
className='-ml-2 fill-white dark:hidden' /** -ml-2 to fix offcenter arrow */
|
||||||
|
tipRadius={3}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -10,6 +10,7 @@ interface IGroupPolicy {
|
||||||
|
|
||||||
const GroupPrivacy = ({ group }: IGroupPolicy) => (
|
const GroupPrivacy = ({ group }: IGroupPolicy) => (
|
||||||
<Popover
|
<Popover
|
||||||
|
referenceElementClassName='cursor-help'
|
||||||
content={
|
content={
|
||||||
<Stack space={4} alignItems='center' className='w-72'>
|
<Stack space={4} alignItems='center' className='w-72'>
|
||||||
<div className='rounded-full bg-gray-200 p-3 dark:bg-gray-800'>
|
<div className='rounded-full bg-gray-200 p-3 dark:bg-gray-800'>
|
||||||
|
|
|
@ -785,6 +785,9 @@
|
||||||
"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.popover.action": "View Group",
|
||||||
|
"group.popover.summary": "You must be a member of the group in order to reply to this status.",
|
||||||
|
"group.popover.title": "Membership required",
|
||||||
"group.privacy.locked": "Private",
|
"group.privacy.locked": "Private",
|
||||||
"group.privacy.locked.full": "Private Group",
|
"group.privacy.locked.full": "Private Group",
|
||||||
"group.privacy.locked.info": "Discoverable. Users can join after their request is approved.",
|
"group.privacy.locked.info": "Discoverable. Users can join after their request is approved.",
|
||||||
|
|
Loading…
Reference in a new issue