Merge branch 'contextual-compose' into 'develop'
Contextual compose button See merge request soapbox-pub/soapbox!2478
This commit is contained in:
@ -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",
|
||||
|
||||
Reference in New Issue
Block a user