some basic groups ui

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2022-12-12 23:36:56 +01:00
parent 12825f9350
commit 7c4aca51dc
13 changed files with 425 additions and 9 deletions

View file

@ -47,6 +47,7 @@ const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL'; const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS'; const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO'; const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
const COMPOSE_GROUP_POST = 'COMPOSE_GROUP_POST';
const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR'; const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY'; const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
@ -469,6 +470,15 @@ const undoUploadCompose = (composeId: string, media_id: string) => ({
media_id: media_id, media_id: media_id,
}); });
const groupCompose = (composeId: string, groupId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({
type: COMPOSE_GROUP_POST,
id: composeId,
group_id: groupId,
});
};
const clearComposeSuggestions = (composeId: string) => { const clearComposeSuggestions = (composeId: string) => {
if (cancelFetchComposeSuggestionsAccounts) { if (cancelFetchComposeSuggestionsAccounts) {
cancelFetchComposeSuggestionsAccounts(); cancelFetchComposeSuggestionsAccounts();
@ -721,7 +731,7 @@ const eventDiscussionCompose = (composeId: string, status: Status) =>
const instance = state.instance; const instance = state.instance;
const { explicitAddressing } = getFeatures(instance); const { explicitAddressing } = getFeatures(instance);
dispatch({ return dispatch({
type: COMPOSE_EVENT_REPLY, type: COMPOSE_EVENT_REPLY,
id: composeId, id: composeId,
status: status, status: status,
@ -748,6 +758,7 @@ export {
COMPOSE_UPLOAD_FAIL, COMPOSE_UPLOAD_FAIL,
COMPOSE_UPLOAD_PROGRESS, COMPOSE_UPLOAD_PROGRESS,
COMPOSE_UPLOAD_UNDO, COMPOSE_UPLOAD_UNDO,
COMPOSE_GROUP_POST,
COMPOSE_SUGGESTIONS_CLEAR, COMPOSE_SUGGESTIONS_CLEAR,
COMPOSE_SUGGESTIONS_READY, COMPOSE_SUGGESTIONS_READY,
COMPOSE_SUGGESTION_SELECT, COMPOSE_SUGGESTION_SELECT,
@ -800,6 +811,7 @@ export {
uploadComposeSuccess, uploadComposeSuccess,
uploadComposeFail, uploadComposeFail,
undoUploadCompose, undoUploadCompose,
groupCompose,
clearComposeSuggestions, clearComposeSuggestions,
fetchComposeSuggestions, fetchComposeSuggestions,
readyComposeSuggestionsEmojis, readyComposeSuggestionsEmojis,

View file

@ -65,6 +65,9 @@ const importFetchedAccounts = (accounts: APIEntity[], args = { should_refetch: f
return importAccounts(normalAccounts); return importAccounts(normalAccounts);
}; };
const importFetchedGroup = (group: APIEntity) =>
importFetchedGroups([group]);
const importFetchedGroups = (groups: APIEntity[]) => { const importFetchedGroups = (groups: APIEntity[]) => {
const normalGroups: APIEntity[] = []; const normalGroups: APIEntity[] = [];
@ -112,6 +115,10 @@ const importFetchedStatus = (status: APIEntity, idempotencyKey?: string) =>
dispatch(importFetchedPoll(status.poll)); dispatch(importFetchedPoll(status.poll));
} }
if (status.group?.id) {
dispatch(importFetchedGroup(status.group));
}
dispatch(importFetchedAccount(status.account)); dispatch(importFetchedAccount(status.account));
dispatch(importStatus(status, idempotencyKey)); dispatch(importStatus(status, idempotencyKey));
}; };
@ -161,6 +168,10 @@ const importFetchedStatuses = (statuses: APIEntity[]) =>
if (status.poll?.id) { if (status.poll?.id) {
polls.push(status.poll); polls.push(status.poll);
} }
if (status.group?.id) {
dispatch(importFetchedGroup(status.group));
}
} }
statuses.forEach(processStatus); statuses.forEach(processStatus);
@ -196,6 +207,7 @@ export {
importPolls, importPolls,
importFetchedAccount, importFetchedAccount,
importFetchedAccounts, importFetchedAccounts,
importFetchedGroup,
importFetchedGroups, importFetchedGroups,
importFetchedStatus, importFetchedStatus,
importFetchedStatuses, importFetchedStatuses,

View file

@ -2,7 +2,7 @@ import classNames from 'clsx';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import { useIntl, FormattedMessage, defineMessages } from 'react-intl'; import { useIntl, FormattedMessage, defineMessages } from 'react-intl';
import { NavLink, useHistory } from 'react-router-dom'; import { Link, NavLink, useHistory } from 'react-router-dom';
import { mentionCompose, replyCompose } from 'soapbox/actions/compose'; import { mentionCompose, replyCompose } from 'soapbox/actions/compose';
import { toggleFavourite, toggleReblog } from 'soapbox/actions/interactions'; import { toggleFavourite, toggleReblog } from 'soapbox/actions/interactions';
@ -26,6 +26,7 @@ import { Card, HStack, Stack, Text } from './ui';
import type { Map as ImmutableMap } from 'immutable'; import type { Map as ImmutableMap } from 'immutable';
import type { import type {
Account as AccountEntity, Account as AccountEntity,
Group as GroupEntity,
Status as StatusEntity, Status as StatusEntity,
} from 'soapbox/types/entities'; } from 'soapbox/types/entities';
@ -299,6 +300,8 @@ const Status: React.FC<IStatus> = (props) => {
} }
} }
const group = actualStatus.group as GroupEntity | null;
const handlers = muted ? undefined : { const handlers = muted ? undefined : {
reply: handleHotkeyReply, reply: handleHotkeyReply,
favourite: handleHotkeyFavourite, favourite: handleHotkeyFavourite,
@ -342,6 +345,26 @@ const Status: React.FC<IStatus> = (props) => {
</div> </div>
)} )}
{group && (
<div className='pt-4 px-4'>
<HStack alignItems='center' space={1}>
<Icon src={require('@tabler/icons/users.svg')} className='text-gray-600 dark:text-gray-400' />
<Text size='sm' theme='muted' weight='medium'>
<FormattedMessage
id='status.group'
defaultMessage='Posted in {group}'
values={{ group: (
<Link className='hover:underline' to={`/groups/${group.id}`} onClick={(e) => e.stopPropagation()}>
<span dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
</Link>
) }}
/>
</Text>
</HStack>
</div>
)}
<Card <Card
variant={variant} variant={variant}
className={classNames('status__wrapper', `status-${actualStatus.visibility}`, { className={classNames('status__wrapper', `status-${actualStatus.visibility}`, {

View file

@ -63,9 +63,10 @@ interface IComposeForm<ID extends string> {
autoFocus?: boolean, autoFocus?: boolean,
clickableAreaRef?: React.RefObject<HTMLDivElement>, clickableAreaRef?: React.RefObject<HTMLDivElement>,
event?: string, event?: string,
group?: string,
} }
const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickableAreaRef, event }: IComposeForm<ID>) => { const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickableAreaRef, event, group }: IComposeForm<ID>) => {
const history = useHistory(); const history = useHistory();
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -228,7 +229,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
{features.media && <UploadButtonContainer composeId={id} />} {features.media && <UploadButtonContainer composeId={id} />}
<EmojiPickerDropdown onPickEmoji={handleEmojiPick} /> <EmojiPickerDropdown onPickEmoji={handleEmojiPick} />
{features.polls && <PollButton composeId={id} />} {features.polls && <PollButton composeId={id} />}
{features.privacyScopes && <PrivacyDropdown composeId={id} />} {features.privacyScopes && !group && <PrivacyDropdown composeId={id} />}
{features.scheduledStatuses && <ScheduleButton composeId={id} />} {features.scheduledStatuses && <ScheduleButton composeId={id} />}
{features.spoilers && <SpoilerButton composeId={id} />} {features.spoilers && <SpoilerButton composeId={id} />}
{features.richText && <MarkdownButton composeId={id} />} {features.richText && <MarkdownButton composeId={id} />}
@ -278,7 +279,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
return ( return (
<Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}> <Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}>
{scheduledStatusCount > 0 && !event && ( {scheduledStatusCount > 0 && !event && !group && (
<Warning <Warning
message={( message={(
<FormattedMessage <FormattedMessage
@ -299,9 +300,9 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
<WarningContainer composeId={id} /> <WarningContainer composeId={id} />
{!shouldCondense && !event && <ReplyIndicatorContainer composeId={id} />} {!shouldCondense && !event && !group && <ReplyIndicatorContainer composeId={id} />}
{!shouldCondense && !event && <ReplyMentions composeId={id} />} {!shouldCondense && !event && !group && <ReplyMentions composeId={id} />}
<AutosuggestTextarea <AutosuggestTextarea
ref={(isModalOpen && shouldCondense) ? undefined : autosuggestTextareaRef} ref={(isModalOpen && shouldCondense) ? undefined : autosuggestTextareaRef}
@ -357,8 +358,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
<Button type='submit' theme='primary' text={publishText} disabled={disabledButton} /> <Button type='submit' theme='primary' text={publishText} disabled={disabledButton} />
</HStack> </HStack>
{/* <HStack alignItems='center' space={4}>
</HStack> */}
</div> </div>
</Stack> </Stack>
); );

View file

@ -0,0 +1,166 @@
import { List as ImmutableList } from 'immutable';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { openModal } from 'soapbox/actions/modals';
import StillImage from 'soapbox/components/still-image';
import { Avatar, HStack, IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuLink, MenuList } from 'soapbox/components/ui';
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
import { normalizeAttachment } from 'soapbox/normalizers';
import type { Menu as MenuType } from 'soapbox/components/dropdown-menu';
import type { Group } from 'soapbox/types/entities';
const messages = defineMessages({
header: { id: 'group.header.alt', defaultMessage: 'Group header' },
});
interface IGroupHeader {
group?: Group | false | null,
}
const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const ownAccount = useOwnAccount();
if (!group) {
return (
<div className='-mt-4 -mx-4'>
<div>
<div className='relative h-32 w-full lg:h-48 md:rounded-t-xl bg-gray-200 dark:bg-gray-900/50' />
</div>
<div className='px-4 sm:px-6'>
<HStack alignItems='bottom' space={5} className='-mt-12'>
<div className='flex relative'>
<div
className='h-24 w-24 bg-gray-400 rounded-full ring-4 ring-white dark:ring-gray-800'
/>
</div>
</HStack>
</div>
</div>
);
}
const onAvatarClick = () => {
const avatar = normalizeAttachment({
type: 'image',
url: group.avatar,
});
dispatch(openModal('MEDIA', { media: ImmutableList.of(avatar), index: 0 }));
};
const handleAvatarClick: React.MouseEventHandler = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
onAvatarClick();
}
};
const onHeaderClick = () => {
const header = normalizeAttachment({
type: 'image',
url: group.header,
});
dispatch(openModal('MEDIA', { media: ImmutableList.of(header), index: 0 }));
};
const handleHeaderClick: React.MouseEventHandler = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
onHeaderClick();
}
};
const makeMenu = () => {
const menu: MenuType = [];
return menu;
};
const menu = makeMenu();
return (
<div className='-mt-4 -mx-4'>
<div>
<div className='relative flex flex-col justify-center h-32 w-full lg:h-48 md:rounded-t-xl bg-gray-200 dark:bg-gray-900/50 overflow-hidden isolate'>
{group.header && (
<a href={group.header} onClick={handleHeaderClick} target='_blank'>
<StillImage
src={group.header}
alt={intl.formatMessage(messages.header)}
/>
</a>
)}
<div className='absolute top-2 left-2'>
<HStack alignItems='center' space={1}>
{/* {info} */}
</HStack>
</div>
</div>
</div>
<div className='px-4 sm:px-6'>
<HStack className='-mt-12' alignItems='bottom' space={5}>
<div className='flex'>
<a href={group.avatar} onClick={handleAvatarClick} target='_blank'>
<Avatar
src={group.avatar}
size={96}
className='relative h-24 w-24 rounded-full ring-4 ring-white dark:ring-primary-900'
/>
</a>
</div>
<div className='mt-6 flex justify-end w-full sm:pb-1'>
<HStack space={2} className='mt-10'>
{ownAccount && (
<Menu>
<MenuButton
as={IconButton}
src={require('@tabler/icons/dots.svg')}
theme='outlined'
className='px-2'
iconClassName='w-4 h-4'
children={null}
/>
<MenuList className='w-56'>
{menu.map((menuItem, idx) => {
if (typeof menuItem?.text === 'undefined') {
return <MenuDivider key={idx} />;
} else {
const Comp = (menuItem.action ? MenuItem : MenuLink) as any;
const itemProps = menuItem.action ? { onSelect: menuItem.action } : { to: menuItem.to, as: Link, target: menuItem.newTab ? '_blank' : '_self' };
return (
<Comp key={idx} {...itemProps} className='group'>
<HStack space={3} alignItems='center'>
{menuItem.icon && (
<SvgIcon src={menuItem.icon} className='h-5 w-5 text-gray-400 flex-none group-hover:text-gray-500' />
)}
<div className='truncate'>{menuItem.text}</div>
</HStack>
</Comp>
);
}
})}
</MenuList>
</Menu>
)}
</HStack>
</div>
</HStack>
</div>
</div>
);
};
export default GroupHeader;

View file

@ -0,0 +1,27 @@
import React from 'react';
import Markup from 'soapbox/components/markup';
import { HStack, Stack, Text } from 'soapbox/components/ui';
import { Group } from 'soapbox/types/entities';
interface IGroupInfoPanel {
group: Group,
}
const GroupInfoPanel: React.FC<IGroupInfoPanel> = ({ group }) => (
<div className='mt-6 min-w-0 flex-1 sm:px-2'>
<Stack space={2}>
<Stack>
<HStack space={1} alignItems='center'>
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
</HStack>
</Stack>
{group.note.length > 0 && (
<Markup size='sm' dangerouslySetInnerHTML={{ __html: group.note_emojified }} />
)}
</Stack>
</div>
);
export default GroupInfoPanel;

View file

@ -0,0 +1,62 @@
import React, { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { groupCompose } from 'soapbox/actions/compose';
import { fetchGroup } from 'soapbox/actions/groups';
import { connectGroupStream } from 'soapbox/actions/streaming';
import { expandGroupTimeline } from 'soapbox/actions/timelines';
import { Stack } from 'soapbox/components/ui';
import ComposeForm from 'soapbox/features/compose/components/compose-form';
import { useAppDispatch } from 'soapbox/hooks';
import Timeline from '../ui/components/timeline';
type RouteParams = { id: string };
interface IGroupTimeline {
params: RouteParams,
}
const GroupTimeline: React.FC<IGroupTimeline> = (props) => {
const dispatch = useAppDispatch();
const groupId = props.params.id;
const handleLoadMore = () => {
return dispatch(expandGroupTimeline(groupId));
};
useEffect(() => {
dispatch(fetchGroup(groupId));
dispatch(expandGroupTimeline(groupId));
const disconnect = dispatch(connectGroupStream(groupId));
return () => {
disconnect();
};
}, []);
useEffect(() => {
dispatch(groupCompose(`group:${groupId}`, groupId));
}, []);
return (
<Stack space={2}>
<div className='p-2 pt-0 border-b border-solid border-gray-200 dark:border-gray-800'>
<ComposeForm id={`group:${groupId}`} autoFocus={false} group={groupId} />
</div>
<div className='p-0 sm:p-2 shadow-none'>
<Timeline
scrollKey='group_timeline'
timelineId={`group:${groupId}`}
onLoadMore={handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.group' defaultMessage='There is no post in this group yet.' />}
divideType='space'
/>
</div>
</Stack>
);
};
export default GroupTimeline;

View file

@ -30,6 +30,7 @@ import AdminPage from 'soapbox/pages/admin-page';
import ChatsPage from 'soapbox/pages/chats-page'; import ChatsPage from 'soapbox/pages/chats-page';
import DefaultPage from 'soapbox/pages/default-page'; import DefaultPage from 'soapbox/pages/default-page';
import EventPage from 'soapbox/pages/event-page'; import EventPage from 'soapbox/pages/event-page';
import GroupPage from 'soapbox/pages/group-page';
import HomePage from 'soapbox/pages/home-page'; import HomePage from 'soapbox/pages/home-page';
import ProfilePage from 'soapbox/pages/profile-page'; import ProfilePage from 'soapbox/pages/profile-page';
import RemoteInstancePage from 'soapbox/pages/remote-instance-page'; import RemoteInstancePage from 'soapbox/pages/remote-instance-page';
@ -111,6 +112,7 @@ import {
EventInformation, EventInformation,
EventDiscussion, EventDiscussion,
Events, Events,
GroupTimeline,
} from './util/async-components'; } from './util/async-components';
import { WrappedRoute } from './util/react-router-helpers'; import { WrappedRoute } from './util/react-router-helpers';
@ -272,6 +274,8 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {
<WrappedRoute path='/@:username/events/:statusId/discussion' publicRoute exact page={EventPage} component={EventDiscussion} content={children} /> <WrappedRoute path='/@:username/events/:statusId/discussion' publicRoute exact page={EventPage} component={EventDiscussion} content={children} />
<Redirect from='/@:username/:statusId' to='/@:username/posts/:statusId' /> <Redirect from='/@:username/:statusId' to='/@:username/posts/:statusId' />
<WrappedRoute path='/groups/:id' exact page={GroupPage} component={GroupTimeline} content={children} />
<WrappedRoute path='/statuses/new' page={DefaultPage} component={NewStatus} content={children} exact /> <WrappedRoute path='/statuses/new' page={DefaultPage} component={NewStatus} content={children} exact />
<WrappedRoute path='/statuses/:statusId' exact page={StatusPage} component={Status} content={children} /> <WrappedRoute path='/statuses/:statusId' exact page={StatusPage} component={Status} content={children} />
{features.scheduledStatuses && <WrappedRoute path='/scheduled_statuses' page={DefaultPage} component={ScheduledStatuses} content={children} />} {features.scheduledStatuses && <WrappedRoute path='/scheduled_statuses' page={DefaultPage} component={ScheduledStatuses} content={children} />}

View file

@ -541,3 +541,11 @@ export function EventParticipantsModal() {
export function Events() { export function Events() {
return import(/* webpackChunkName: "features/events" */'../../events'); return import(/* webpackChunkName: "features/events" */'../../events');
} }
export function GroupTimeline() {
return import(/* webpackChunkName: "features/groups" */'../../group/group-timeline');
}
export function GroupInfoPanel() {
return import(/* webpackChunkName: "features/groups" */'../../group/components/group-info-panel');
}

View file

@ -0,0 +1,80 @@
import React, { useCallback, useEffect } from 'react';
import { fetchGroup } from 'soapbox/actions/groups';
import MissingIndicator from 'soapbox/components/missing-indicator';
import { Column, Layout } from 'soapbox/components/ui';
import GroupHeader from 'soapbox/features/group/components/group-header';
import LinkFooter from 'soapbox/features/ui/components/link-footer';
import BundleContainer from 'soapbox/features/ui/containers/bundle-container';
import {
GroupInfoPanel,
SignUpPanel,
CtaBanner,
} from 'soapbox/features/ui/util/async-components';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { makeGetGroup } from 'soapbox/selectors';
interface IGroupPage {
params?: {
id?: string,
},
}
/** Page to display a group. */
const ProfilePage: React.FC<IGroupPage> = ({ params, children }) => {
const id = params?.id || '';
const dispatch = useAppDispatch();
const getGroup = useCallback(makeGetGroup(), []);
const group = useAppSelector(state => getGroup(state, id));
const me = useAppSelector(state => state.me);
useEffect(() => {
dispatch(fetchGroup(id));
}, [id]);
if (group === false) {
return (
<MissingIndicator />
);
}
return (
<>
<Layout.Main>
<Column label={group ? group.display_name : ''} withHeader={false}>
<div className='space-y-4'>
<GroupHeader group={group} />
{group && (
<BundleContainer fetchComponent={GroupInfoPanel}>
{Component => <Component group={group} />}
</BundleContainer>
)}
{children}
</div>
</Column>
{!me && (
<BundleContainer fetchComponent={CtaBanner}>
{Component => <Component key='cta-banner' />}
</BundleContainer>
)}
</Layout.Main>
<Layout.Aside>
{!me && (
<BundleContainer fetchComponent={SignUpPanel}>
{Component => <Component key='sign-up-panel' />}
</BundleContainer>
)}
<LinkFooter key='link-footer' />
</Layout.Aside>
</>
);
};
export default ProfilePage;

View file

@ -11,6 +11,7 @@ import {
COMPOSE_REPLY_CANCEL, COMPOSE_REPLY_CANCEL,
COMPOSE_QUOTE, COMPOSE_QUOTE,
COMPOSE_QUOTE_CANCEL, COMPOSE_QUOTE_CANCEL,
COMPOSE_GROUP_POST,
COMPOSE_DIRECT, COMPOSE_DIRECT,
COMPOSE_MENTION, COMPOSE_MENTION,
COMPOSE_SUBMIT_REQUEST, COMPOSE_SUBMIT_REQUEST,
@ -386,6 +387,14 @@ export default function compose(state = initialState, action: AnyAction) {
map.set('caretPosition', null); map.set('caretPosition', null);
map.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid());
})); }));
case COMPOSE_GROUP_POST:
return updateCompose(state, action.id, compose => compose.withMutations(map => {
map.set('privacy', 'group');
map.set('group_id', action.group_id);
map.set('focusDate', new Date());
map.set('caretPosition', null);
map.set('idempotencyKey', uuid());
}));
case COMPOSE_SUGGESTIONS_CLEAR: case COMPOSE_SUGGESTIONS_CLEAR:
return updateCompose(state, action.id, compose => compose.update('suggestions', list => list?.clear()).set('suggestion_token', null)); return updateCompose(state, action.id, compose => compose.update('suggestions', list => list?.clear()).set('suggestion_token', null));
case COMPOSE_SUGGESTIONS_READY: case COMPOSE_SUGGESTIONS_READY:

View file

@ -65,6 +65,7 @@ const minifyStatus = (status: StatusRecord): ReducerStatus => {
reblog: normalizeId(status.getIn(['reblog', 'id'])), reblog: normalizeId(status.getIn(['reblog', 'id'])),
poll: normalizeId(status.getIn(['poll', 'id'])), poll: normalizeId(status.getIn(['poll', 'id'])),
quote: normalizeId(status.getIn(['quote', 'id'])), quote: normalizeId(status.getIn(['quote', 'id'])),
group: normalizeId(status.getIn(['group', 'id'])),
}) as ReducerStatus; }) as ReducerStatus;
}; };

View file

@ -353,3 +353,16 @@ export const makeGetStatusIds = () => createSelector([
return !shouldFilter(status, columnSettings); return !shouldFilter(status, columnSettings);
}); });
}); });
export const makeGetGroup = () => {
return createSelector([
(state: RootState, id: string) => state.groups.get(id),
(state: RootState, id: string) => state.group_relationships.get(id),
], (base, relationship) => {
if (!base) return null;
return base.withMutations(map => {
if (relationship) map.set('relationship', relationship);
});
});
};