From 36350f1cc4f86f8e9c7f66a7b4e4575d0d099492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 16 Dec 2022 21:33:17 +0100 Subject: [PATCH] More group actions, allow to edit groups, visuals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/groups.ts | 18 ++- app/soapbox/components/group-card.tsx | 68 ++++---- .../group/components/group-header.tsx | 42 ++++- app/soapbox/features/group/group-timeline.tsx | 4 +- .../manage-group-modal/steps/details-step.tsx | 146 ++++++++++++------ app/soapbox/reducers/group-editor.ts | 7 + 6 files changed, 195 insertions(+), 90 deletions(-) diff --git a/app/soapbox/actions/groups.ts b/app/soapbox/actions/groups.ts index bc68cc3f2..d79ee63e0 100644 --- a/app/soapbox/actions/groups.ts +++ b/app/soapbox/actions/groups.ts @@ -4,15 +4,17 @@ import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; import { importFetchedGroups, importFetchedAccounts } from './importer'; -import { closeModal } from './modals'; +import { closeModal, openModal } from './modals'; import { deleteFromTimelines } from './timelines'; import type { AxiosError } from 'axios'; import type { AppDispatch, RootState } from 'soapbox/store'; -import type { APIEntity } from 'soapbox/types/entities'; +import type { APIEntity, Group } from 'soapbox/types/entities'; type GroupMedia = 'header' | 'avatar'; +const GROUP_EDITOR_SET = 'GROUP_EDITOR_SET'; + const GROUP_CREATE_REQUEST = 'GROUP_CREATE_REQUEST'; const GROUP_CREATE_SUCCESS = 'GROUP_CREATE_SUCCESS'; const GROUP_CREATE_FAIL = 'GROUP_CREATE_FAIL'; @@ -108,6 +110,14 @@ const GROUP_EDITOR_MEDIA_CHANGE = 'GROUP_EDITOR_MEDIA_CHANGE'; const GROUP_EDITOR_RESET = 'GROUP_EDITOR_RESET'; +const editGroup = (group: Group) => (dispatch: AppDispatch) => { + dispatch({ + type: GROUP_EDITOR_SET, + group, + }); + dispatch(openModal('MANAGE_GROUP')); +}; + const createGroup = (params: Record, shouldReset?: boolean) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(createGroupRequest()); @@ -844,11 +854,12 @@ const submitGroupEditor = (shouldReset?: boolean) => (dispatch: AppDispatch, get if (groupId === null) { dispatch(createGroup(params, shouldReset)); } else { - // TODO: dispatch(updateList(listId, title, shouldReset)); + dispatch(updateGroup(groupId, params, shouldReset)); } }; export { + GROUP_EDITOR_SET, GROUP_CREATE_REQUEST, GROUP_CREATE_SUCCESS, GROUP_CREATE_FAIL, @@ -920,6 +931,7 @@ export { GROUP_EDITOR_PRIVACY_CHANGE, GROUP_EDITOR_MEDIA_CHANGE, GROUP_EDITOR_RESET, + editGroup, createGroup, createGroupRequest, createGroupSuccess, diff --git a/app/soapbox/components/group-card.tsx b/app/soapbox/components/group-card.tsx index a1386dac4..462480fac 100644 --- a/app/soapbox/components/group-card.tsx +++ b/app/soapbox/components/group-card.tsx @@ -17,41 +17,43 @@ const GroupCard: React.FC = ({ group }) => { const intl = useIntl(); return ( - -
- {group.header && {intl.formatMessage(messages.groupHeader)}} -
- +
+ +
+ {group.header && {intl.formatMessage(messages.groupHeader)}} +
+ +
-
- - - - {group.relationship?.role === 'admin' ? ( - - - Owner - - ) : group.relationship?.role === 'moderator' && ( - - - Moderator - - )} - {group.locked ? ( - - - Private - - ) : ( - - - Public - - )} - + + + + {group.relationship?.role === 'admin' ? ( + + + Owner + + ) : group.relationship?.role === 'moderator' && ( + + + Moderator + + )} + {group.locked ? ( + + + Private + + ) : ( + + + Public + + )} + + - +
); }; diff --git a/app/soapbox/features/group/components/group-header.tsx b/app/soapbox/features/group/components/group-header.tsx index fa79d1aaa..3171885ae 100644 --- a/app/soapbox/features/group/components/group-header.tsx +++ b/app/soapbox/features/group/components/group-header.tsx @@ -2,6 +2,7 @@ import { List as ImmutableList } from 'immutable'; import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { editGroup, joinGroup, leaveGroup } from 'soapbox/actions/groups'; import { openModal } from 'soapbox/actions/modals'; import StillImage from 'soapbox/components/still-image'; import { Avatar, Button, HStack, Icon, Stack, Text } from 'soapbox/components/ui'; @@ -42,6 +43,25 @@ const GroupHeader: React.FC = ({ group }) => { ); } + const onJoinGroup = () => { + dispatch(joinGroup(group.id)); + }; + + const onLeaveGroup = () => { + dispatch(openModal('CONFIRM', { + heading: 'Leave group', + message: 'You are about to leave the group. Do you want to continue?', + confirm: 'Leave', + onConfirm: () => { + dispatch(leaveGroup(group.id)); + }, + })); + }; + + const onEditGroup = () => { + dispatch(editGroup(group)); + }; + const onAvatarClick = () => { const avatar = normalizeAttachment({ type: 'image', @@ -73,18 +93,36 @@ const GroupHeader: React.FC = ({ group }) => { }; const makeActionButton = () => { + if (!group.relationship || !group.relationship.member) { + return ( + + ); + } + if (group.relationship?.role === 'admin') { return ( ); } - return null; + return ( + + ); }; const actionButton = makeActionButton(); diff --git a/app/soapbox/features/group/group-timeline.tsx b/app/soapbox/features/group/group-timeline.tsx index 91e8ff2ce..c2dd3ac3d 100644 --- a/app/soapbox/features/group/group-timeline.tsx +++ b/app/soapbox/features/group/group-timeline.tsx @@ -24,8 +24,8 @@ const GroupTimeline: React.FC = (props) => { const groupId = props.params.id; - const handleLoadMore = () => { - return dispatch(expandGroupTimeline(groupId)); + const handleLoadMore = (maxId: string) => { + dispatch(expandGroupTimeline(groupId, { maxId })); }; useEffect(() => { diff --git a/app/soapbox/features/ui/components/modals/manage-group-modal/steps/details-step.tsx b/app/soapbox/features/ui/components/modals/manage-group-modal/steps/details-step.tsx index cd0c8251e..ee1ef8a9e 100644 --- a/app/soapbox/features/ui/components/modals/manage-group-modal/steps/details-step.tsx +++ b/app/soapbox/features/ui/components/modals/manage-group-modal/steps/details-step.tsx @@ -1,4 +1,5 @@ -import React, { useState } from 'react'; +import classNames from 'clsx'; +import React, { useEffect, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { @@ -7,21 +8,95 @@ import { changeGroupEditorMedia, } from 'soapbox/actions/groups'; import Icon from 'soapbox/components/icon'; -import { Avatar, Form, FormGroup, HStack, IconButton, Input, Text, Textarea } from 'soapbox/components/ui'; +import { Avatar, Form, FormGroup, HStack, Input, Text, Textarea } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import resizeImage from 'soapbox/utils/resize-image'; import type { List as ImmutableList } from 'immutable'; +interface IMediaInput { + src: string | null, + accept: string, + onChange: React.ChangeEventHandler + disabled: boolean +} + const messages = defineMessages({ groupNamePlaceholder: { id: 'manage_group.fields.name_placeholder', defaultMessage: 'Group Name' }, groupDescriptionPlaceholder: { id: 'manage_group.fields.description_placeholder', defaultMessage: 'Description' }, }); +const HeaderPicker: React.FC = ({ src, onChange, accept, disabled }) => { + return ( + + ); +}; + +const AvatarPicker: React.FC = ({ src, onChange, accept, disabled }) => { + return ( + + ); +}; + const DetailsStep = () => { const intl = useIntl(); const dispatch = useAppDispatch(); + const groupId = useAppSelector((state) => state.group_editor.groupId); const isUploading = useAppSelector((state) => state.group_editor.isUploading); const name = useAppSelector((state) => state.group_editor.displayName); const description = useAppSelector((state) => state.group_editor.note); @@ -29,7 +104,9 @@ const DetailsStep = () => { const [avatarSrc, setAvatarSrc] = useState(null); const [headerSrc, setHeaderSrc] = useState(null); - const attachmentTypes = useAppSelector(state => state.instance.configuration.getIn(['media_attachments', 'supported_mime_types']) as ImmutableList)?.filter(type => type.startsWith('image/')); + const attachmentTypes = useAppSelector( + state => state.instance.configuration.getIn(['media_attachments', 'supported_mime_types']) as ImmutableList, + )?.filter(type => type.startsWith('image/')).toArray().join(','); const onChangeName: React.ChangeEventHandler = ({ target }) => { dispatch(changeGroupEditorTitle(target.value)); @@ -56,55 +133,24 @@ const DetailsStep = () => { }).catch(console.error); } }; + + useEffect(() => { + if (!groupId) return; + + dispatch((_, getState) => { + const group = getState().groups.get(groupId); + if (!group) return; + if (group.avatar) setAvatarSrc(group.avatar); + if (group.header) setHeaderSrc(group.header); + }); + }, [groupId]); + + return (
-
- {headerSrc ? ( - <> - - {}} /> - - ) : ( - - - - - - - - - )} -
- {avatarSrc ? ( - - ) : ( - - )} -
+
+ +
} diff --git a/app/soapbox/reducers/group-editor.ts b/app/soapbox/reducers/group-editor.ts index 01b49224d..3669acd20 100644 --- a/app/soapbox/reducers/group-editor.ts +++ b/app/soapbox/reducers/group-editor.ts @@ -12,6 +12,7 @@ import { GROUP_UPDATE_REQUEST, GROUP_UPDATE_FAIL, GROUP_UPDATE_SUCCESS, + GROUP_EDITOR_SET, } from 'soapbox/actions/groups'; import type { AnyAction } from 'redux'; @@ -35,6 +36,12 @@ export default function groupEditor(state: State = ReducerRecord(), action: AnyA switch (action.type) { case GROUP_EDITOR_RESET: return ReducerRecord(); + case GROUP_EDITOR_SET: + return state.withMutations(map => { + map.set('groupId', action.group.id); + map.set('displayName', action.group.display_name); + map.set('note', action.group.note); + }); case GROUP_EDITOR_TITLE_CHANGE: return state.withMutations(map => { map.set('displayName', action.value);