diff --git a/app/soapbox/actions/groups.ts b/app/soapbox/actions/groups.ts index d79ee63e0..271b03b63 100644 --- a/app/soapbox/actions/groups.ts +++ b/app/soapbox/actions/groups.ts @@ -1,18 +1,18 @@ -import { GroupRole } from 'soapbox/reducers/group-memberships'; +import { defineMessages } from 'react-intl'; import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; import { importFetchedGroups, importFetchedAccounts } from './importer'; import { closeModal, openModal } from './modals'; +import snackbar from './snackbar'; import { deleteFromTimelines } from './timelines'; import type { AxiosError } from 'axios'; +import type { GroupRole } from 'soapbox/reducers/group-memberships'; import type { AppDispatch, RootState } from 'soapbox/store'; 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'; @@ -110,6 +110,15 @@ const GROUP_EDITOR_MEDIA_CHANGE = 'GROUP_EDITOR_MEDIA_CHANGE'; const GROUP_EDITOR_RESET = 'GROUP_EDITOR_RESET'; +const messages = defineMessages({ + success: { id: 'manage_group.submit_success', defaultMessage: 'The group was created' }, + editSuccess: { id: 'manage_group.edit_success', defaultMessage: 'The group was edited' }, + joinSuccess: { id: 'group.join.success', defaultMessage: 'Joined the group' }, + joinRequestSuccess: { id: 'group.join.request_success', defaultMessage: 'Requested to join the group' }, + leaveSuccess: { id: 'group.leave.success', defaultMessage: 'Left the group' }, + view: { id: 'snackbar.view', defaultMessage: 'View' }, +}); + const editGroup = (group: Group) => (dispatch: AppDispatch) => { dispatch({ type: GROUP_EDITOR_SET, @@ -130,6 +139,7 @@ const createGroup = (params: Record, shouldReset?: boolean) => .then(({ data }) => { dispatch(importFetchedGroups([data])); dispatch(createGroupSuccess(data)); + dispatch(snackbar.success(messages.success, messages.view, `/groups/${data.id}`)); if (shouldReset) { dispatch(resetGroupEditor()); @@ -160,6 +170,7 @@ const updateGroup = (id: string, params: Record, shouldReset?: bool .then(({ data }) => { dispatch(importFetchedGroups([data])); dispatch(updateGroupSuccess(data)); + dispatch(snackbar.success(messages.editSuccess)); if (shouldReset) { dispatch(resetGroupEditor()); @@ -305,6 +316,7 @@ const joinGroup = (id: string) => api(getState).post(`/api/v1/groups/${id}/join`).then(response => { dispatch(joinGroupSuccess(response.data)); + dispatch(snackbar.success(locked ? messages.joinRequestSuccess : messages.joinSuccess)); }).catch(error => { dispatch(joinGroupFail(error, locked)); }); @@ -316,6 +328,7 @@ const leaveGroup = (id: string) => api(getState).post(`/api/v1/groups/${id}/leave`).then(response => { dispatch(leaveGroupSuccess(response.data)); + dispatch(snackbar.success(messages.leaveSuccess)); }).catch(error => { dispatch(leaveGroupFail(error)); }); @@ -826,7 +839,7 @@ const changeGroupEditorPrivacy = (value: boolean) => ({ value, }); -const changeGroupEditorMedia = (mediaType: GroupMedia, file: File) => ({ +const changeGroupEditorMedia = (mediaType: 'header' | 'avatar', file: File) => ({ type: GROUP_EDITOR_MEDIA_CHANGE, mediaType, value: file, diff --git a/app/soapbox/components/status-action-button.tsx b/app/soapbox/components/status-action-button.tsx index 2f355211b..4f70ab353 100644 --- a/app/soapbox/components/status-action-button.tsx +++ b/app/soapbox/components/status-action-button.tsx @@ -80,11 +80,12 @@ const StatusActionButton = React.forwardRef { divideType?: 'space' | 'border', /** Whether to display ads. */ showAds?: boolean, + /** Whether to show group information. */ + showGroup?: boolean, } /** Feed of statuses, built atop ScrollableList. */ @@ -59,6 +61,7 @@ const StatusList: React.FC = ({ isLoading, isPartial, showAds = false, + showGroup = true, ...other }) => { const { data: ads } = useAds(); @@ -135,6 +138,7 @@ const StatusList: React.FC = ({ onMoveUp={handleMoveUp} onMoveDown={handleMoveDown} contextType={timelineId} + showGroup={showGroup} /> ); }; @@ -167,6 +171,7 @@ const StatusList: React.FC = ({ onMoveUp={handleMoveUp} onMoveDown={handleMoveDown} contextType={timelineId} + showGroup={showGroup} /> )); }; diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index d4c91f6cd..b9e513fa2 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -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, useHistory } from 'react-router-dom'; import { mentionCompose, replyCompose } from 'soapbox/actions/compose'; import { toggleFavourite, toggleReblog } from 'soapbox/actions/interactions'; @@ -23,10 +23,9 @@ import StatusReplyMentions from './status-reply-mentions'; import SensitiveContentOverlay from './statuses/sensitive-content-overlay'; import { Card, HStack, Stack, Text } from './ui'; -import type { Map as ImmutableMap } from 'immutable'; import type { Account as AccountEntity, - // Group as GroupEntity, + Group as GroupEntity, Status as StatusEntity, } from 'soapbox/types/entities'; @@ -46,12 +45,12 @@ export interface IStatus { unread?: boolean, onMoveUp?: (statusId: string, featured?: boolean) => void, onMoveDown?: (statusId: string, featured?: boolean) => void, - group?: ImmutableMap, focusable?: boolean, featured?: boolean, hideActionBar?: boolean, hoverable?: boolean, variant?: 'default' | 'rounded', + showGroup?: boolean, withDismiss?: boolean, accountAction?: React.ReactElement, } @@ -70,6 +69,7 @@ const Status: React.FC = (props) => { unread, hideActionBar, variant = 'rounded', + showGroup = true, withDismiss, } = props; @@ -235,7 +235,7 @@ const Status: React.FC = (props) => { const displayNameHtml = { __html: String(status.getIn(['account', 'display_name_html'])) }; reblogElement = ( - event.stopPropagation()} className='hidden sm:flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-1 hover:underline' @@ -253,12 +253,12 @@ const Status: React.FC = (props) => { }} /> - + ); reblogElementMobile = (
- event.stopPropagation()} className='flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-1 hover:underline' @@ -276,7 +276,7 @@ const Status: React.FC = (props) => { }} /> - +
); @@ -300,7 +300,7 @@ const Status: React.FC = (props) => { } } - // const group = actualStatus.group as GroupEntity | null; + const group = actualStatus.group as GroupEntity | null; const handlers = muted ? undefined : { reply: handleHotkeyReply, @@ -345,26 +345,6 @@ const Status: React.FC = (props) => { )} - {/* {group && ( -
- - - - - e.stopPropagation()}> - - - ) }} - /> - - -
- )} */} - = (props) => { })} data-id={status.id} > + {showGroup && group && ( +
+ + + + + e.stopPropagation()}> + + + ) }} + /> + + +
+ )} + {reblogElementMobile}
diff --git a/app/soapbox/features/group/group-timeline.tsx b/app/soapbox/features/group/group-timeline.tsx index a99dd030b..bf56f869c 100644 --- a/app/soapbox/features/group/group-timeline.tsx +++ b/app/soapbox/features/group/group-timeline.tsx @@ -64,7 +64,8 @@ const GroupTimeline: React.FC = (props) => { timelineId={`group:${groupId}`} onLoadMore={handleLoadMore} emptyMessage={} - divideType='space' + divideType='border' + showGroup={false} /> ); diff --git a/app/soapbox/reducers/timelines.ts b/app/soapbox/reducers/timelines.ts index 97975d658..1001e3d87 100644 --- a/app/soapbox/reducers/timelines.ts +++ b/app/soapbox/reducers/timelines.ts @@ -35,7 +35,6 @@ import { } from '../actions/timelines'; import type { AnyAction } from 'redux'; -import type { StatusVisibility } from 'soapbox/normalizers/status'; import type { APIEntity, Status } from 'soapbox/types/entities'; const TRUNCATE_LIMIT = 40; @@ -242,8 +241,10 @@ const timelineDisconnect = (state: State, timelineId: string) => { })); }; -const getTimelinesByVisibility = (visibility: StatusVisibility) => { - switch (visibility) { +const getTimelinesForStatus = (status: APIEntity) => { + switch (status.visibility) { + case 'group': + return [`group:${status.group?.id || status.group_id}`]; case 'direct': return ['direct']; case 'public': @@ -269,7 +270,7 @@ const importPendingStatus = (state: State, params: APIEntity, idempotencyKey: st const statusId = `ęœ«pending-${idempotencyKey}`; return state.withMutations(state => { - const timelineIds = getTimelinesByVisibility(params.visibility); + const timelineIds = getTimelinesForStatus(params); timelineIds.forEach(timelineId => { updateTimelineQueue(state, timelineId, statusId); @@ -293,7 +294,7 @@ const importStatus = (state: State, status: APIEntity, idempotencyKey: string) = return state.withMutations(state => { replacePendingStatus(state, idempotencyKey, status.id); - const timelineIds = getTimelinesByVisibility(status.visibility); + const timelineIds = getTimelinesForStatus(status); timelineIds.forEach(timelineId => { updateTimeline(state, timelineId, status.id);