Merge branch 'contextual-compose' into 'develop'

Contextual compose button

See merge request soapbox-pub/soapbox!2478
environments/review-develop-3zknud/deployments/3287
Alex Gleason 1 year ago
commit 95364a46d9

@ -22,6 +22,7 @@ import { createStatus } from './statuses';
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
import type { Emoji } from 'soapbox/features/emoji';
import type { Group } from 'soapbox/schemas';
import type { AppDispatch, RootState } from 'soapbox/store';
import type { Account, APIEntity, Status, Tag } from 'soapbox/types/entities';
import type { History } from 'soapbox/types/history';
@ -168,6 +169,14 @@ const cancelQuoteCompose = () => ({
id: 'compose-modal',
});
const groupComposeModal = (group: Group) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const composeId = `group:${group.id}`;
dispatch(groupCompose(composeId, group.id));
dispatch(openModal('COMPOSE', { composeId }));
};
const resetCompose = (composeId = 'compose-modal') => ({
type: COMPOSE_RESET,
id: composeId,
@ -829,6 +838,7 @@ export {
uploadComposeFail,
undoUploadCompose,
groupCompose,
groupComposeModal,
setGroupTimelineVisible,
clearComposeSuggestions,
fetchComposeSuggestions,

@ -3,6 +3,7 @@ import React from 'react';
import { IntlProvider } from 'react-intl';
import { Provider } from 'react-redux';
import '@testing-library/jest-dom';
import { MemoryRouter } from 'react-router-dom';
import { MODAL_OPEN } from 'soapbox/actions/modals';
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
@ -14,7 +15,9 @@ const renderComposeButton = () => {
render(
<Provider store={store}>
<IntlProvider locale='en'>
<ComposeButton />
<MemoryRouter>
<ComposeButton />
</MemoryRouter>
</IntlProvider>
</Provider>,
);

@ -1,11 +1,24 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { useLocation, useRouteMatch } from 'react-router-dom';
import { groupComposeModal } from 'soapbox/actions/compose';
import { openModal } from 'soapbox/actions/modals';
import { Button } from 'soapbox/components/ui';
import { Avatar, Button, HStack } from 'soapbox/components/ui';
import { useAppDispatch } from 'soapbox/hooks';
import { useGroupLookup } from 'soapbox/hooks/api/groups/useGroupLookup';
const ComposeButton = () => {
const location = useLocation();
if (location.pathname.startsWith('/group/')) {
return <GroupComposeButton />;
}
return <HomeComposeButton />;
};
const HomeComposeButton = () => {
const dispatch = useAppDispatch();
const onOpenCompose = () => dispatch(openModal('COMPOSE'));
@ -22,4 +35,32 @@ const ComposeButton = () => {
);
};
const GroupComposeButton = () => {
const dispatch = useAppDispatch();
const match = useRouteMatch<{ groupSlug: string }>('/group/:groupSlug');
const { entity: group } = useGroupLookup(match?.params.groupSlug || '');
if (!group) return null;
const onOpenCompose = () => {
dispatch(groupComposeModal(group));
};
return (
<Button
theme='accent'
size='lg'
onClick={onOpenCompose}
block
>
<HStack space={3} alignItems='center'>
<Avatar className='-my-1 border-2 border-white' size={30} src={group.avatar} />
<span>
<FormattedMessage id='navigation.compose_group' defaultMessage='Compose to Group' />
</span>
</HStack>
</Button>
);
};
export default ComposeButton;

@ -1,20 +1,30 @@
import clsx from 'clsx';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useLocation, useRouteMatch } from 'react-router-dom';
import { groupComposeModal } from 'soapbox/actions/compose';
import { openModal } from 'soapbox/actions/modals';
import { Icon } from 'soapbox/components/ui';
import { Avatar, HStack, Icon } from 'soapbox/components/ui';
import { useAppDispatch } from 'soapbox/hooks';
import { useGroupLookup } from 'soapbox/hooks/api/groups/useGroupLookup';
const messages = defineMessages({
publish: { id: 'compose_form.publish', defaultMessage: 'Publish' },
});
interface IFloatingActionButton {
}
/** FloatingActionButton (aka FAB), a composer button that floats in the corner on mobile. */
const FloatingActionButton: React.FC<IFloatingActionButton> = () => {
const FloatingActionButton: React.FC = () => {
const location = useLocation();
if (location.pathname.startsWith('/group/')) {
return <GroupFAB />;
}
return <HomeFAB />;
};
const HomeFAB: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
@ -39,4 +49,37 @@ const FloatingActionButton: React.FC<IFloatingActionButton> = () => {
);
};
const GroupFAB: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const match = useRouteMatch<{ groupSlug: string }>('/group/:groupSlug');
const { entity: group } = useGroupLookup(match?.params.groupSlug || '');
if (!group) return null;
const handleOpenComposeModal = () => {
dispatch(groupComposeModal(group));
};
return (
<button
onClick={handleOpenComposeModal}
className={clsx(
'inline-flex appearance-none items-center rounded-full border p-4 font-medium transition-all focus:outline-none focus:ring-2 focus:ring-offset-2',
'border-transparent bg-secondary-500 text-gray-100 hover:bg-secondary-400 focus:bg-secondary-500 focus:ring-secondary-300',
)}
aria-label={intl.formatMessage(messages.publish)}
>
<HStack space={3} alignItems='center'>
<Avatar className='-my-3 -ml-2 border-white' size={42} src={group.avatar} />
<Icon
src={require('@tabler/icons/pencil-plus.svg')}
className='h-6 w-6'
/>
</HStack>
</button>
);
};
export default FloatingActionButton;

@ -2,11 +2,12 @@ import clsx from 'clsx';
import React, { useRef } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { cancelReplyCompose, uploadCompose } from 'soapbox/actions/compose';
import { cancelReplyCompose, setGroupTimelineVisible, uploadCompose } from 'soapbox/actions/compose';
import { openModal, closeModal } from 'soapbox/actions/modals';
import { checkComposeContent } from 'soapbox/components/modal-root';
import { Modal } from 'soapbox/components/ui';
import { useAppDispatch, useCompose, useDraggedFiles } from 'soapbox/hooks';
import { HStack, Modal, Text, Toggle } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector, useCompose, useDraggedFiles } from 'soapbox/hooks';
import { useGroup } from 'soapbox/hooks/api';
import ComposeForm from '../../../compose/components/compose-form';
@ -18,17 +19,16 @@ const messages = defineMessages({
interface IComposeModal {
onClose: (type?: string) => void
composeId?: string
}
const ComposeModal: React.FC<IComposeModal> = ({ onClose }) => {
const ComposeModal: React.FC<IComposeModal> = ({ onClose, composeId = 'compose-modal' }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const node = useRef<HTMLDivElement>(null);
const composeId = 'compose-modal';
const compose = useCompose(composeId);
const { id: statusId, privacy, in_reply_to: inReplyTo, quote } = compose!;
const { id: statusId, privacy, in_reply_to: inReplyTo, quote, group_id: groupId } = compose!;
const { isDragging, isDraggedOver } = useDraggedFiles(node, (files) => {
dispatch(uploadCompose(composeId, files, intl));
@ -60,6 +60,10 @@ const ComposeModal: React.FC<IComposeModal> = ({ onClose }) => {
return <FormattedMessage id='navigation_bar.compose_edit' defaultMessage='Edit post' />;
} else if (privacy === 'direct') {
return <FormattedMessage id='navigation_bar.compose_direct' defaultMessage='Direct message' />;
} else if (inReplyTo && groupId) {
return <FormattedMessage id='navigation_bar.compose_group_reply' defaultMessage='Reply to group post' />;
} else if (groupId) {
return <FormattedMessage id='navigation_bar.compose_group' defaultMessage='Compose to group' />;
} else if (inReplyTo) {
return <FormattedMessage id='navigation_bar.compose_reply' defaultMessage='Reply to post' />;
} else if (quote) {
@ -79,9 +83,49 @@ const ComposeModal: React.FC<IComposeModal> = ({ onClose }) => {
'ring-2 ring-offset-2 ring-primary-600': isDraggedOver,
})}
>
<ComposeForm id='compose-modal' />
<ComposeForm
id={composeId}
extra={<ComposeFormGroupToggle composeId={composeId} groupId={groupId} />}
/>
</Modal>
);
};
interface IComposeFormGroupToggle {
composeId: string
groupId: string | null
}
const ComposeFormGroupToggle: React.FC<IComposeFormGroupToggle> = ({ composeId, groupId }) => {
const dispatch = useAppDispatch();
const { group } = useGroup(groupId || '', false);
const groupTimelineVisible = useAppSelector((state) => !!state.compose.get(composeId)?.group_timeline_visible);
const handleToggleChange = () => {
dispatch(setGroupTimelineVisible(composeId, !groupTimelineVisible));
};
const labelId = `group-timeline-visible+${composeId}`;
if (!group) return null;
if (group.locked) return null;
return (
<HStack alignItems='center' space={4}>
<label className='ml-auto cursor-pointer' htmlFor={labelId}>
<Text theme='muted'>
<FormattedMessage id='compose_group.share_to_followers' defaultMessage='Share with my followers' />
</Text>
</label>
<Toggle
id={labelId}
checked={groupTimelineVisible}
onChange={handleToggleChange}
size='sm'
/>
</HStack>
);
};
export default ComposeModal;

@ -1042,6 +1042,7 @@
"navbar.login.username.placeholder": "Email or username",
"navigation.chats": "Chats",
"navigation.compose": "Compose",
"navigation.compose_group": "Compose to Group",
"navigation.dashboard": "Dashboard",
"navigation.developers": "Developers",
"navigation.direct_messages": "Messages",
@ -1055,6 +1056,8 @@
"navigation_bar.compose_direct": "Direct message",
"navigation_bar.compose_edit": "Edit post",
"navigation_bar.compose_event": "Manage event",
"navigation_bar.compose_group": "Compose to group",
"navigation_bar.compose_group_reply": "Reply to group post",
"navigation_bar.compose_quote": "Quote post",
"navigation_bar.compose_reply": "Reply to post",
"navigation_bar.create_event": "Create new event",

Loading…
Cancel
Save