Support Group pins
This commit is contained in:
@@ -7,10 +7,11 @@ import api from '../api';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||
import { expandGroupFeaturedTimeline } from './timelines';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
||||
import type { APIEntity, Group, Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
||||
const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
||||
@@ -511,6 +512,20 @@ const pin = (status: StatusEntity) =>
|
||||
});
|
||||
};
|
||||
|
||||
const pinToGroup = (status: StatusEntity, group: Group) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
return api(getState)
|
||||
.post(`/api/v1/groups/${group.id}/statuses/${status.get('id')}/pin`)
|
||||
.then(() => dispatch(expandGroupFeaturedTimeline(group.id)));
|
||||
};
|
||||
|
||||
const unpinFromGroup = (status: StatusEntity, group: Group) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
return api(getState)
|
||||
.post(`/api/v1/groups/${group.id}/statuses/${status.get('id')}/unpin`)
|
||||
.then(() => dispatch(expandGroupFeaturedTimeline(group.id)));
|
||||
};
|
||||
|
||||
const pinRequest = (status: StatusEntity) => ({
|
||||
type: PIN_REQUEST,
|
||||
status,
|
||||
@@ -715,6 +730,8 @@ export {
|
||||
unpinSuccess,
|
||||
unpinFail,
|
||||
togglePin,
|
||||
pinToGroup,
|
||||
unpinFromGroup,
|
||||
remoteInteraction,
|
||||
remoteInteractionRequest,
|
||||
remoteInteractionSuccess,
|
||||
|
||||
@@ -248,6 +248,9 @@ const expandListTimeline = (id: string, { maxId }: Record<string, any> = {}, don
|
||||
const expandGroupTimeline = (id: string, { maxId }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { max_id: maxId }, done);
|
||||
|
||||
const expandGroupFeaturedTimeline = (id: string) =>
|
||||
expandTimeline(`group:${id}:pinned`, `/api/v1/timelines/group/${id}`, { pinned: true });
|
||||
|
||||
const expandGroupTimelineFromTag = (id: string, tagName: string, { maxId }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`group:tags:${id}:${tagName}`, `/api/v1/timelines/group/${id}/tags/${tagName}`, { max_id: maxId }, done);
|
||||
|
||||
@@ -353,6 +356,7 @@ export {
|
||||
expandAccountMediaTimeline,
|
||||
expandListTimeline,
|
||||
expandGroupTimeline,
|
||||
expandGroupFeaturedTimeline,
|
||||
expandGroupTimelineFromTag,
|
||||
expandGroupMediaTimeline,
|
||||
expandHashtagTimeline,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { blockAccount } from 'soapbox/actions/accounts';
|
||||
import { launchChat } from 'soapbox/actions/chats';
|
||||
import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'soapbox/actions/compose';
|
||||
import { editEvent } from 'soapbox/actions/events';
|
||||
import { toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog } from 'soapbox/actions/interactions';
|
||||
import { pinToGroup, toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog, unpinFromGroup } from 'soapbox/actions/interactions';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
|
||||
import { initMuteModal } from 'soapbox/actions/mutes';
|
||||
@@ -67,6 +67,9 @@ const messages = defineMessages({
|
||||
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
||||
open: { id: 'status.open', defaultMessage: 'Expand this post' },
|
||||
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
|
||||
pinToGroup: { id: 'status.pin_to_group', defaultMessage: 'Pin to Group' },
|
||||
pinToGroupSuccess: { id: 'status.pin_to_group.success', defaultMessage: 'Pinned to Group!' },
|
||||
unpinFromGroup: { id: 'status.unpin_to_group', defaultMessage: 'Unpin from Group' },
|
||||
quotePost: { id: 'status.quote', defaultMessage: 'Quote post' },
|
||||
reactionCry: { id: 'status.reactions.cry', defaultMessage: 'Sad' },
|
||||
reactionHeart: { id: 'status.reactions.heart', defaultMessage: 'Love' },
|
||||
@@ -232,6 +235,18 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||
dispatch(togglePin(status));
|
||||
};
|
||||
|
||||
const handleGroupPinClick: React.EventHandler<React.MouseEvent> = () => {
|
||||
const group = status.group as Group;
|
||||
|
||||
if (status.pinned) {
|
||||
dispatch(unpinFromGroup(status, group));
|
||||
} else {
|
||||
dispatch(pinToGroup(status, group))
|
||||
.then(() => toast.success(intl.formatMessage(messages.pinToGroupSuccess)))
|
||||
.catch(() => null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMentionClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
dispatch(mentionCompose(status.account as Account));
|
||||
};
|
||||
@@ -358,6 +373,19 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||
return menu;
|
||||
}
|
||||
|
||||
const isGroupStatus = typeof status.group === 'object';
|
||||
if (isGroupStatus && !!status.group) {
|
||||
const isGroupOwner = groupRelationship?.role === GroupRoles.OWNER;
|
||||
|
||||
if (isGroupOwner) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(status.pinned ? messages.unpinFromGroup : messages.pinToGroup),
|
||||
action: handleGroupPinClick,
|
||||
icon: status.pinned ? require('@tabler/icons/pinned-off.svg') : require('@tabler/icons/pin.svg'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (features.bookmarks) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(status.bookmarked ? messages.unbookmark : messages.bookmark),
|
||||
@@ -460,7 +488,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||
});
|
||||
}
|
||||
|
||||
const isGroupStatus = typeof status.group === 'object';
|
||||
if (isGroupStatus && !!status.group) {
|
||||
const group = status.group as Group;
|
||||
const account = status.account as Account;
|
||||
|
||||
@@ -5,11 +5,12 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
import { groupCompose, setGroupTimelineVisible, uploadCompose } from 'soapbox/actions/compose';
|
||||
import { connectGroupStream } from 'soapbox/actions/streaming';
|
||||
import { expandGroupTimeline } from 'soapbox/actions/timelines';
|
||||
import { expandGroupFeaturedTimeline, expandGroupTimeline } from 'soapbox/actions/timelines';
|
||||
import { useGroup } from 'soapbox/api/hooks';
|
||||
import { Avatar, HStack, Icon, Stack, Text, Toggle } from 'soapbox/components/ui';
|
||||
import ComposeForm from 'soapbox/features/compose/components/compose-form';
|
||||
import { useAppDispatch, useAppSelector, useDraggedFiles, useOwnAccount } from 'soapbox/hooks';
|
||||
import { makeGetStatusIds } from 'soapbox/selectors';
|
||||
|
||||
import Timeline from '../ui/components/timeline';
|
||||
|
||||
@@ -19,6 +20,8 @@ interface IGroupTimeline {
|
||||
params: RouteParams
|
||||
}
|
||||
|
||||
const getStatusIds = makeGetStatusIds();
|
||||
|
||||
const GroupTimeline: React.FC<IGroupTimeline> = (props) => {
|
||||
const intl = useIntl();
|
||||
const account = useOwnAccount();
|
||||
@@ -32,6 +35,7 @@ const GroupTimeline: React.FC<IGroupTimeline> = (props) => {
|
||||
const composeId = `group:${groupId}`;
|
||||
const canComposeGroupStatus = !!account && group?.relationship?.member;
|
||||
const groupTimelineVisible = useAppSelector((state) => !!state.compose.get(composeId)?.group_timeline_visible);
|
||||
const featuredStatusIds = useAppSelector((state) => getStatusIds(state, { type: `group:${group?.id}:pinned` }));
|
||||
|
||||
const { isDragging, isDraggedOver } = useDraggedFiles(composer, (files) => {
|
||||
dispatch(uploadCompose(composeId, files, intl));
|
||||
@@ -47,6 +51,7 @@ const GroupTimeline: React.FC<IGroupTimeline> = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(expandGroupTimeline(groupId));
|
||||
dispatch(expandGroupFeaturedTimeline(groupId));
|
||||
dispatch(groupCompose(composeId, groupId));
|
||||
|
||||
const disconnect = dispatch(connectGroupStream(groupId));
|
||||
@@ -123,6 +128,7 @@ const GroupTimeline: React.FC<IGroupTimeline> = (props) => {
|
||||
emptyMessageCard={false}
|
||||
divideType='border'
|
||||
showGroup={false}
|
||||
featuredStatusIds={featuredStatusIds}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -1466,6 +1466,8 @@
|
||||
"status.mute_conversation": "Mute Conversation",
|
||||
"status.open": "Show Post Details",
|
||||
"status.pin": "Pin on profile",
|
||||
"status.pin_to_group": "Pin to Group",
|
||||
"status.pin_to_group.success": "Pinned to Group!",
|
||||
"status.pinned": "Pinned post",
|
||||
"status.quote": "Quote post",
|
||||
"status.reactions.cry": "Sad",
|
||||
@@ -1502,6 +1504,7 @@
|
||||
"status.unbookmarked": "Bookmark removed.",
|
||||
"status.unmute_conversation": "Unmute Conversation",
|
||||
"status.unpin": "Unpin from profile",
|
||||
"status.unpin_to_group": "Unpin from Group",
|
||||
"status_list.queue_label": "Click to see {count} new {count, plural, one {post} other {posts}}",
|
||||
"statuses.quote_tombstone": "Post is unavailable.",
|
||||
"statuses.tombstone": "One or more posts are unavailable.",
|
||||
|
||||
Reference in New Issue
Block a user