some basic groups ui
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
12825f9350
commit
7c4aca51dc
13 changed files with 425 additions and 9 deletions
|
@ -47,6 +47,7 @@ const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
|
|||
const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
|
||||
const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
|
||||
const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
|
||||
const COMPOSE_GROUP_POST = 'COMPOSE_GROUP_POST';
|
||||
|
||||
const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
|
||||
const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
|
||||
|
@ -469,6 +470,15 @@ const undoUploadCompose = (composeId: string, media_id: string) => ({
|
|||
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) => {
|
||||
if (cancelFetchComposeSuggestionsAccounts) {
|
||||
cancelFetchComposeSuggestionsAccounts();
|
||||
|
@ -721,7 +731,7 @@ const eventDiscussionCompose = (composeId: string, status: Status) =>
|
|||
const instance = state.instance;
|
||||
const { explicitAddressing } = getFeatures(instance);
|
||||
|
||||
dispatch({
|
||||
return dispatch({
|
||||
type: COMPOSE_EVENT_REPLY,
|
||||
id: composeId,
|
||||
status: status,
|
||||
|
@ -748,6 +758,7 @@ export {
|
|||
COMPOSE_UPLOAD_FAIL,
|
||||
COMPOSE_UPLOAD_PROGRESS,
|
||||
COMPOSE_UPLOAD_UNDO,
|
||||
COMPOSE_GROUP_POST,
|
||||
COMPOSE_SUGGESTIONS_CLEAR,
|
||||
COMPOSE_SUGGESTIONS_READY,
|
||||
COMPOSE_SUGGESTION_SELECT,
|
||||
|
@ -800,6 +811,7 @@ export {
|
|||
uploadComposeSuccess,
|
||||
uploadComposeFail,
|
||||
undoUploadCompose,
|
||||
groupCompose,
|
||||
clearComposeSuggestions,
|
||||
fetchComposeSuggestions,
|
||||
readyComposeSuggestionsEmojis,
|
||||
|
|
|
@ -65,6 +65,9 @@ const importFetchedAccounts = (accounts: APIEntity[], args = { should_refetch: f
|
|||
return importAccounts(normalAccounts);
|
||||
};
|
||||
|
||||
const importFetchedGroup = (group: APIEntity) =>
|
||||
importFetchedGroups([group]);
|
||||
|
||||
const importFetchedGroups = (groups: APIEntity[]) => {
|
||||
const normalGroups: APIEntity[] = [];
|
||||
|
||||
|
@ -112,6 +115,10 @@ const importFetchedStatus = (status: APIEntity, idempotencyKey?: string) =>
|
|||
dispatch(importFetchedPoll(status.poll));
|
||||
}
|
||||
|
||||
if (status.group?.id) {
|
||||
dispatch(importFetchedGroup(status.group));
|
||||
}
|
||||
|
||||
dispatch(importFetchedAccount(status.account));
|
||||
dispatch(importStatus(status, idempotencyKey));
|
||||
};
|
||||
|
@ -161,6 +168,10 @@ const importFetchedStatuses = (statuses: APIEntity[]) =>
|
|||
if (status.poll?.id) {
|
||||
polls.push(status.poll);
|
||||
}
|
||||
|
||||
if (status.group?.id) {
|
||||
dispatch(importFetchedGroup(status.group));
|
||||
}
|
||||
}
|
||||
|
||||
statuses.forEach(processStatus);
|
||||
|
@ -196,6 +207,7 @@ export {
|
|||
importPolls,
|
||||
importFetchedAccount,
|
||||
importFetchedAccounts,
|
||||
importFetchedGroup,
|
||||
importFetchedGroups,
|
||||
importFetchedStatus,
|
||||
importFetchedStatuses,
|
||||
|
|
|
@ -2,7 +2,7 @@ import classNames from 'clsx';
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
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 { 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 {
|
||||
Account as AccountEntity,
|
||||
Group as GroupEntity,
|
||||
Status as StatusEntity,
|
||||
} 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 : {
|
||||
reply: handleHotkeyReply,
|
||||
favourite: handleHotkeyFavourite,
|
||||
|
@ -342,6 +345,26 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
</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
|
||||
variant={variant}
|
||||
className={classNames('status__wrapper', `status-${actualStatus.visibility}`, {
|
||||
|
|
|
@ -63,9 +63,10 @@ interface IComposeForm<ID extends string> {
|
|||
autoFocus?: boolean,
|
||||
clickableAreaRef?: React.RefObject<HTMLDivElement>,
|
||||
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 intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
@ -228,7 +229,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
{features.media && <UploadButtonContainer composeId={id} />}
|
||||
<EmojiPickerDropdown onPickEmoji={handleEmojiPick} />
|
||||
{features.polls && <PollButton composeId={id} />}
|
||||
{features.privacyScopes && <PrivacyDropdown composeId={id} />}
|
||||
{features.privacyScopes && !group && <PrivacyDropdown composeId={id} />}
|
||||
{features.scheduledStatuses && <ScheduleButton composeId={id} />}
|
||||
{features.spoilers && <SpoilerButton composeId={id} />}
|
||||
{features.richText && <MarkdownButton composeId={id} />}
|
||||
|
@ -278,7 +279,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
|
||||
return (
|
||||
<Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}>
|
||||
{scheduledStatusCount > 0 && !event && (
|
||||
{scheduledStatusCount > 0 && !event && !group && (
|
||||
<Warning
|
||||
message={(
|
||||
<FormattedMessage
|
||||
|
@ -299,9 +300,9 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
|
||||
<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
|
||||
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} />
|
||||
</HStack>
|
||||
{/* <HStack alignItems='center' space={4}>
|
||||
</HStack> */}
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
|
|
166
app/soapbox/features/group/components/group-header.tsx
Normal file
166
app/soapbox/features/group/components/group-header.tsx
Normal 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;
|
27
app/soapbox/features/group/components/group-info-panel.tsx
Normal file
27
app/soapbox/features/group/components/group-info-panel.tsx
Normal 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;
|
62
app/soapbox/features/group/group-timeline.tsx
Normal file
62
app/soapbox/features/group/group-timeline.tsx
Normal 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;
|
|
@ -30,6 +30,7 @@ import AdminPage from 'soapbox/pages/admin-page';
|
|||
import ChatsPage from 'soapbox/pages/chats-page';
|
||||
import DefaultPage from 'soapbox/pages/default-page';
|
||||
import EventPage from 'soapbox/pages/event-page';
|
||||
import GroupPage from 'soapbox/pages/group-page';
|
||||
import HomePage from 'soapbox/pages/home-page';
|
||||
import ProfilePage from 'soapbox/pages/profile-page';
|
||||
import RemoteInstancePage from 'soapbox/pages/remote-instance-page';
|
||||
|
@ -111,6 +112,7 @@ import {
|
|||
EventInformation,
|
||||
EventDiscussion,
|
||||
Events,
|
||||
GroupTimeline,
|
||||
} from './util/async-components';
|
||||
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} />
|
||||
<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/:statusId' exact page={StatusPage} component={Status} content={children} />
|
||||
{features.scheduledStatuses && <WrappedRoute path='/scheduled_statuses' page={DefaultPage} component={ScheduledStatuses} content={children} />}
|
||||
|
|
|
@ -541,3 +541,11 @@ export function EventParticipantsModal() {
|
|||
export function 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');
|
||||
}
|
||||
|
|
80
app/soapbox/pages/group-page.tsx
Normal file
80
app/soapbox/pages/group-page.tsx
Normal 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;
|
|
@ -11,6 +11,7 @@ import {
|
|||
COMPOSE_REPLY_CANCEL,
|
||||
COMPOSE_QUOTE,
|
||||
COMPOSE_QUOTE_CANCEL,
|
||||
COMPOSE_GROUP_POST,
|
||||
COMPOSE_DIRECT,
|
||||
COMPOSE_MENTION,
|
||||
COMPOSE_SUBMIT_REQUEST,
|
||||
|
@ -386,6 +387,14 @@ export default function compose(state = initialState, action: AnyAction) {
|
|||
map.set('caretPosition', null);
|
||||
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:
|
||||
return updateCompose(state, action.id, compose => compose.update('suggestions', list => list?.clear()).set('suggestion_token', null));
|
||||
case COMPOSE_SUGGESTIONS_READY:
|
||||
|
|
|
@ -65,6 +65,7 @@ const minifyStatus = (status: StatusRecord): ReducerStatus => {
|
|||
reblog: normalizeId(status.getIn(['reblog', 'id'])),
|
||||
poll: normalizeId(status.getIn(['poll', 'id'])),
|
||||
quote: normalizeId(status.getIn(['quote', 'id'])),
|
||||
group: normalizeId(status.getIn(['group', 'id'])),
|
||||
}) as ReducerStatus;
|
||||
};
|
||||
|
||||
|
|
|
@ -353,3 +353,16 @@ export const makeGetStatusIds = () => createSelector([
|
|||
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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue