diff --git a/app/soapbox/components/groups/popover/group-popover.tsx b/app/soapbox/components/groups/popover/group-popover.tsx new file mode 100644 index 000000000..776506f99 --- /dev/null +++ b/app/soapbox/components/groups/popover/group-popover.tsx @@ -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> + 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 ( + + + {/* Group Cover Image */} + + {group.header && ( + + )} + + + {/* Group Avatar */} +
+ +
+ + {/* Group Info */} + + + + + + + + +
+ + + + + + {intl.formatMessage(messages.title)} + + + {intl.formatMessage(messages.summary)} + + + +
+ + + +
+ + } + isFlush + children={ +
{children}
+ } + /> + ); +}; + +export default GroupPopover; \ No newline at end of file diff --git a/app/soapbox/components/status-action-bar.tsx b/app/soapbox/components/status-action-bar.tsx index a17dc7194..2938e91e7 100644 --- a/app/soapbox/components/status-action-bar.tsx +++ b/app/soapbox/components/status-action-bar.tsx @@ -24,6 +24,8 @@ import { isLocal, isRemote } from 'soapbox/utils/accounts'; import copy from 'soapbox/utils/copy'; 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 { Account, Group, Status } from 'soapbox/types/entities'; @@ -617,14 +619,19 @@ const StatusActionBar: React.FC = ({ grow={space === 'expand'} onClick={e => e.stopPropagation()} > - + + + {(features.quotePosts && me) ? ( > + /** The content of the popover */ 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. */ const Popover: React.FC = (props) => { - const { children, content } = props; + const { children, content, referenceElementClassName, interaction = 'hover', isFlush = false } = props; const [isOpen, setIsOpen] = useState(false); @@ -33,6 +43,9 @@ const Popover: React.FC = (props) => { onOpenChange: setIsOpen, placement: 'top', middleware: [ + autoPlacement({ + allowedPlacements: ['top', 'bottom'], + }), offset(10), arrow({ element: arrowRef, @@ -40,8 +53,6 @@ const Popover: React.FC = (props) => { ], }); - const click = useClick(context); - const dismiss = useDismiss(context); const { isMounted, styles } = useTransitionStyles(context, { initial: { opacity: 0, @@ -53,8 +64,13 @@ const Popover: React.FC = (props) => { }, }); + const click = useClick(context, { enabled: interaction === 'click' }); + const hover = useHover(context, { enabled: interaction === 'hover' }); + const dismiss = useDismiss(context); + const { getReferenceProps, getFloatingProps } = useInteractions([ click, + hover, dismiss, ]); @@ -63,7 +79,7 @@ const Popover: React.FC = (props) => { {React.cloneElement(children, { ref: refs.setReference, ...getReferenceProps(), - className: 'cursor-help', + className: clsx(children.props.className, referenceElementClassName), })} {(isMounted) && ( @@ -75,12 +91,22 @@ const Popover: React.FC = (props) => { left: x ?? 0, ...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()} > {content} - + )} diff --git a/app/soapbox/features/group/components/group-privacy.tsx b/app/soapbox/features/group/components/group-privacy.tsx index d4f1b5b90..952c8bd49 100644 --- a/app/soapbox/features/group/components/group-privacy.tsx +++ b/app/soapbox/features/group/components/group-privacy.tsx @@ -10,6 +10,7 @@ interface IGroupPolicy { const GroupPrivacy = ({ group }: IGroupPolicy) => (
diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index c37af2b27..ce45fc577 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -787,6 +787,9 @@ "group.leave": "Leave Group", "group.leave.success": "Left the 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.full": "Private Group", "group.privacy.locked.info": "Discoverable. Users can join after their request is approved.",