Support Friendica dislikes, quotes
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
4bee42f86d
commit
12f3b4fbc3
10 changed files with 200 additions and 10 deletions
|
@ -44,6 +44,10 @@ const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
|
|||
const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
|
||||
const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
|
||||
|
||||
const DISLIKES_FETCH_REQUEST = 'DISLIKES_FETCH_REQUEST';
|
||||
const DISLIKES_FETCH_SUCCESS = 'DISLIKES_FETCH_SUCCESS';
|
||||
const DISLIKES_FETCH_FAIL = 'DISLIKES_FETCH_FAIL';
|
||||
|
||||
const REACTIONS_FETCH_REQUEST = 'REACTIONS_FETCH_REQUEST';
|
||||
const REACTIONS_FETCH_SUCCESS = 'REACTIONS_FETCH_SUCCESS';
|
||||
const REACTIONS_FETCH_FAIL = 'REACTIONS_FETCH_FAIL';
|
||||
|
@ -104,7 +108,7 @@ const unreblog = (status: StatusEntity) =>
|
|||
};
|
||||
|
||||
const toggleReblog = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
(dispatch: AppDispatch) => {
|
||||
if (status.reblogged) {
|
||||
dispatch(unreblog(status));
|
||||
} else {
|
||||
|
@ -177,7 +181,7 @@ const unfavourite = (status: StatusEntity) =>
|
|||
};
|
||||
|
||||
const toggleFavourite = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
(dispatch: AppDispatch) => {
|
||||
if (status.favourited) {
|
||||
dispatch(unfavourite(status));
|
||||
} else {
|
||||
|
@ -229,7 +233,7 @@ const dislike = (status: StatusEntity) =>
|
|||
|
||||
dispatch(dislikeRequest(status));
|
||||
|
||||
api(getState).post(`/api/friendica/${status.get('id')}/dislike`).then(function() {
|
||||
api(getState).post(`/api/friendica/statuses/${status.get('id')}/dislike`).then(function() {
|
||||
dispatch(dislikeSuccess(status));
|
||||
}).catch(function(error) {
|
||||
dispatch(dislikeFail(status, error));
|
||||
|
@ -242,7 +246,7 @@ const undislike = (status: StatusEntity) =>
|
|||
|
||||
dispatch(undislikeRequest(status));
|
||||
|
||||
api(getState).post(`/api/friendica/${status.get('id')}/undislike`).then(() => {
|
||||
api(getState).post(`/api/friendica/statuses/${status.get('id')}/undislike`).then(() => {
|
||||
dispatch(undislikeSuccess(status));
|
||||
}).catch(error => {
|
||||
dispatch(undislikeFail(status, error));
|
||||
|
@ -250,7 +254,7 @@ const undislike = (status: StatusEntity) =>
|
|||
};
|
||||
|
||||
const toggleDislike = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
(dispatch: AppDispatch) => {
|
||||
if (status.disliked) {
|
||||
dispatch(undislike(status));
|
||||
} else {
|
||||
|
@ -432,6 +436,38 @@ const fetchFavouritesFail = (id: string, error: AxiosError) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
const fetchDislikes = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(fetchDislikesRequest(id));
|
||||
|
||||
api(getState).get(`/api/friendica/statuses/${id}/disliked_by`).then(response => {
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(fetchDislikesSuccess(id, response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchDislikesFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const fetchDislikesRequest = (id: string) => ({
|
||||
type: DISLIKES_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
const fetchDislikesSuccess = (id: string, accounts: APIEntity[]) => ({
|
||||
type: DISLIKES_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
});
|
||||
|
||||
const fetchDislikesFail = (id: string, error: AxiosError) => ({
|
||||
type: DISLIKES_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const fetchReactions = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchReactionsRequest(id));
|
||||
|
@ -597,6 +633,9 @@ export {
|
|||
FAVOURITES_FETCH_REQUEST,
|
||||
FAVOURITES_FETCH_SUCCESS,
|
||||
FAVOURITES_FETCH_FAIL,
|
||||
DISLIKES_FETCH_REQUEST,
|
||||
DISLIKES_FETCH_SUCCESS,
|
||||
DISLIKES_FETCH_FAIL,
|
||||
REACTIONS_FETCH_REQUEST,
|
||||
REACTIONS_FETCH_SUCCESS,
|
||||
REACTIONS_FETCH_FAIL,
|
||||
|
@ -659,6 +698,10 @@ export {
|
|||
fetchFavouritesRequest,
|
||||
fetchFavouritesSuccess,
|
||||
fetchFavouritesFail,
|
||||
fetchDislikes,
|
||||
fetchDislikesRequest,
|
||||
fetchDislikesSuccess,
|
||||
fetchDislikesFail,
|
||||
fetchReactions,
|
||||
fetchReactionsRequest,
|
||||
fetchReactionsSuccess,
|
||||
|
|
|
@ -671,8 +671,8 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
color='accent'
|
||||
filled
|
||||
onClick={handleDislikeClick}
|
||||
active={status.friendica.get('disliked')}
|
||||
count={status.friendica.get('dislikes_count')}
|
||||
active={status.disliked}
|
||||
count={status.dislikes_count}
|
||||
text={withLabels ? intl.formatMessage(messages.disfavourite) : undefined }
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -46,6 +46,13 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
|
|||
}));
|
||||
};
|
||||
|
||||
const onOpenDislikesModal = (username: string, statusId: string): void => {
|
||||
dispatch(openModal('DISLIKES', {
|
||||
username,
|
||||
statusId,
|
||||
}));
|
||||
};
|
||||
|
||||
const onOpenReactionsModal = (username: string, statusId: string): void => {
|
||||
dispatch(openModal('REACTIONS', {
|
||||
username,
|
||||
|
@ -114,6 +121,13 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
|
|||
else onOpenFavouritesModal(account.acct, status.id);
|
||||
};
|
||||
|
||||
const handleOpenDislikesModal: React.EventHandler<React.MouseEvent<HTMLButtonElement>> = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!me) onOpenUnauthorizedModal();
|
||||
else onOpenDislikesModal(account.acct, status.id);
|
||||
};
|
||||
|
||||
const getFavourites = () => {
|
||||
if (status.favourites_count) {
|
||||
return (
|
||||
|
@ -130,6 +144,24 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
|
|||
return null;
|
||||
};
|
||||
|
||||
const getDislikes = () => {
|
||||
const dislikesCount = status.dislikes_count;
|
||||
|
||||
if (dislikesCount) {
|
||||
return (
|
||||
<InteractionCounter count={status.favourites_count} onClick={features.exposableReactions ? handleOpenDislikesModal : undefined}>
|
||||
<FormattedMessage
|
||||
id='status.interactions.dislikes'
|
||||
defaultMessage='{count, plural, one {Dislike} other {Dislikes}}'
|
||||
values={{ count: dislikesCount }}
|
||||
/>
|
||||
</InteractionCounter>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleOpenReactionsModal = () => {
|
||||
if (!me) {
|
||||
return onOpenUnauthorizedModal();
|
||||
|
@ -171,6 +203,7 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
|
|||
{getReposts()}
|
||||
{getQuotes()}
|
||||
{features.emojiReacts ? getEmojiReacts() : getFavourites()}
|
||||
{getDislikes()}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
ComposeModal,
|
||||
ConfirmationModal,
|
||||
CryptoDonateModal,
|
||||
DislikesModal,
|
||||
EditAnnouncementModal,
|
||||
EditFederationModal,
|
||||
EmbedModal,
|
||||
|
@ -59,6 +60,7 @@ const MODAL_COMPONENTS = {
|
|||
'COMPOSE_EVENT': ComposeEventModal,
|
||||
'CONFIRM': ConfirmationModal,
|
||||
'CRYPTO_DONATE': CryptoDonateModal,
|
||||
'DISLIKES': DislikesModal,
|
||||
'EDIT_ANNOUNCEMENT': EditAnnouncementModal,
|
||||
'EDIT_FEDERATION': EditFederationModal,
|
||||
'EMBED': EmbedModal,
|
||||
|
|
63
app/soapbox/features/ui/components/modals/dislikes-modal.tsx
Normal file
63
app/soapbox/features/ui/components/modals/dislikes-modal.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { fetchDislikes } from 'soapbox/actions/interactions';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Modal, Spinner } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
interface IDislikesModal {
|
||||
onClose: (type: string) => void
|
||||
statusId: string
|
||||
}
|
||||
|
||||
const DislikesModal: React.FC<IDislikesModal> = ({ onClose, statusId }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const accountIds = useAppSelector((state) => state.user_lists.disliked_by.get(statusId)?.items);
|
||||
|
||||
const fetchData = () => {
|
||||
dispatch(fetchDislikes(statusId));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const onClickClose = () => {
|
||||
onClose('DISLIKES');
|
||||
};
|
||||
|
||||
let body;
|
||||
|
||||
if (!accountIds) {
|
||||
body = <Spinner />;
|
||||
} else {
|
||||
const emptyMessage = <FormattedMessage id='empty_column.dislikes' defaultMessage='No one has disliked this post yet. When someone does, they will show up here.' />;
|
||||
|
||||
body = (
|
||||
<ScrollableList
|
||||
scrollKey='dislikes'
|
||||
emptyMessage={emptyMessage}
|
||||
className='max-w-full'
|
||||
itemClassName='pb-3'
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<FormattedMessage id='column.dislikes' defaultMessage='Dislikes' />}
|
||||
onClose={onClickClose}
|
||||
>
|
||||
{body}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DislikesModal;
|
|
@ -190,6 +190,10 @@ export function FavouritesModal() {
|
|||
return import(/* webpackChunkName: "features/ui" */'../components/modals/favourites-modal');
|
||||
}
|
||||
|
||||
export function DislikesModal() {
|
||||
return import(/* webpackChunkName: "features/ui" */'../components/modals/dislikes-modal');
|
||||
}
|
||||
|
||||
export function ReactionsModal() {
|
||||
return import(/* webpackChunkName: "features/ui" */'../components/modals/reactions-modal');
|
||||
}
|
||||
|
|
|
@ -46,12 +46,13 @@ export const StatusRecord = ImmutableRecord({
|
|||
card: null as Card | null,
|
||||
content: '',
|
||||
created_at: '',
|
||||
dislikes_count: 0,
|
||||
disliked: false,
|
||||
edited_at: null as string | null,
|
||||
emojis: ImmutableList<Emoji>(),
|
||||
favourited: false,
|
||||
favourites_count: 0,
|
||||
filtered: ImmutableList<string>(),
|
||||
friendica: ImmutableMap<string, any>(),
|
||||
group: null as EmbeddedEntity<Group>,
|
||||
in_reply_to_account_id: null as string | null,
|
||||
in_reply_to_id: null as string | null,
|
||||
|
@ -218,6 +219,16 @@ const normalizeFilterResults = (status: ImmutableMap<string, any>) =>
|
|||
),
|
||||
);
|
||||
|
||||
const normalizeDislikes = (status: ImmutableMap<string, any>) => {
|
||||
if (status.get('friendica')) {
|
||||
return status
|
||||
.set('dislikes_count', status.getIn(['friendica', 'dislikes_count']))
|
||||
.set('disliked', status.getIn(['friendica', 'disliked']))
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
export const normalizeStatus = (status: Record<string, any>) => {
|
||||
return StatusRecord(
|
||||
ImmutableMap(fromJS(status)).withMutations(status => {
|
||||
|
@ -233,6 +244,7 @@ export const normalizeStatus = (status: Record<string, any>) => {
|
|||
normalizeEvent(status);
|
||||
fixContent(status);
|
||||
normalizeFilterResults(status);
|
||||
normalizeDislikes(status);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
|
@ -26,6 +26,9 @@ import {
|
|||
FAVOURITE_REQUEST,
|
||||
UNFAVOURITE_REQUEST,
|
||||
FAVOURITE_FAIL,
|
||||
DISLIKE_REQUEST,
|
||||
UNDISLIKE_REQUEST,
|
||||
DISLIKE_FAIL,
|
||||
} from '../actions/interactions';
|
||||
import {
|
||||
STATUS_CREATE_REQUEST,
|
||||
|
@ -204,6 +207,25 @@ const simulateFavourite = (
|
|||
return state.set(statusId, updatedStatus);
|
||||
};
|
||||
|
||||
/** Simulate dislike/undislike of status for optimistic interactions */
|
||||
const simulateDislike = (
|
||||
state: State,
|
||||
statusId: string,
|
||||
disliked: boolean,
|
||||
): State => {
|
||||
const status = state.get(statusId);
|
||||
if (!status) return state;
|
||||
|
||||
const delta = disliked ? +1 : -1;
|
||||
|
||||
const updatedStatus = status.merge({
|
||||
disliked,
|
||||
dislikes_count: Math.max(0, status.dislikes_count + delta),
|
||||
});
|
||||
|
||||
return state.set(statusId, updatedStatus);
|
||||
};
|
||||
|
||||
interface Translation {
|
||||
content: string
|
||||
detected_source_language: string
|
||||
|
@ -238,6 +260,10 @@ export default function statuses(state = initialState, action: AnyAction): State
|
|||
return simulateFavourite(state, action.status.id, true);
|
||||
case UNFAVOURITE_REQUEST:
|
||||
return simulateFavourite(state, action.status.id, false);
|
||||
case DISLIKE_REQUEST:
|
||||
return simulateDislike(state, action.status.id, true);
|
||||
case UNDISLIKE_REQUEST:
|
||||
return simulateDislike(state, action.status.id, false);
|
||||
case EMOJI_REACT_REQUEST:
|
||||
return state
|
||||
.updateIn(
|
||||
|
@ -252,6 +278,8 @@ export default function statuses(state = initialState, action: AnyAction): State
|
|||
);
|
||||
case FAVOURITE_FAIL:
|
||||
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false);
|
||||
case DISLIKE_FAIL:
|
||||
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'disliked'], false);
|
||||
case REBLOG_REQUEST:
|
||||
return state.setIn([action.status.get('id'), 'reblogged'], true);
|
||||
case REBLOG_FAIL:
|
||||
|
|
|
@ -60,6 +60,7 @@ import {
|
|||
import {
|
||||
REBLOGS_FETCH_SUCCESS,
|
||||
FAVOURITES_FETCH_SUCCESS,
|
||||
DISLIKES_FETCH_SUCCESS,
|
||||
REACTIONS_FETCH_SUCCESS,
|
||||
} from 'soapbox/actions/interactions';
|
||||
import {
|
||||
|
@ -107,6 +108,7 @@ export const ReducerRecord = ImmutableRecord({
|
|||
following: ImmutableMap<string, List>(),
|
||||
reblogged_by: ImmutableMap<string, List>(),
|
||||
favourited_by: ImmutableMap<string, List>(),
|
||||
disliked_by: ImmutableMap<string, List>(),
|
||||
reactions: ImmutableMap<string, ReactionList>(),
|
||||
follow_requests: ListRecord(),
|
||||
blocks: ListRecord(),
|
||||
|
@ -128,7 +130,7 @@ type ReactionList = ReturnType<typeof ReactionListRecord>;
|
|||
type ParticipationRequest = ReturnType<typeof ParticipationRequestRecord>;
|
||||
type ParticipationRequestList = ReturnType<typeof ParticipationRequestListRecord>;
|
||||
type Items = ImmutableOrderedSet<string>;
|
||||
type NestedListPath = ['followers' | 'following' | 'reblogged_by' | 'favourited_by' | 'reactions' | 'pinned' | 'birthday_reminders' | 'familiar_followers' | 'event_participations' | 'event_participation_requests' | 'membership_requests' | 'group_blocks', string];
|
||||
type NestedListPath = ['followers' | 'following' | 'reblogged_by' | 'favourited_by' | 'disliked_by' | 'reactions' | 'pinned' | 'birthday_reminders' | 'familiar_followers' | 'event_participations' | 'event_participation_requests' | 'membership_requests' | 'group_blocks', string];
|
||||
type ListPath = ['follow_requests' | 'blocks' | 'mutes' | 'directory'];
|
||||
|
||||
const normalizeList = (state: State, path: NestedListPath | ListPath, accounts: APIEntity[], next?: string | null) => {
|
||||
|
@ -173,6 +175,8 @@ export default function userLists(state = ReducerRecord(), action: AnyAction) {
|
|||
return normalizeList(state, ['reblogged_by', action.id], action.accounts);
|
||||
case FAVOURITES_FETCH_SUCCESS:
|
||||
return normalizeList(state, ['favourited_by', action.id], action.accounts);
|
||||
case DISLIKES_FETCH_SUCCESS:
|
||||
return normalizeList(state, ['disliked_by', action.id], action.accounts);
|
||||
case REACTIONS_FETCH_SUCCESS:
|
||||
return state.setIn(['reactions', action.id], ReactionListRecord({
|
||||
items: ImmutableOrderedSet<Reaction>(action.reactions.map(({ accounts, ...reaction }: APIEntity) => ReactionRecord({
|
||||
|
|
|
@ -348,7 +348,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
* @see POST /api/friendica/statuses/:id/undislike
|
||||
* @see GET /api/friendica/statuses/:id/disliked_by
|
||||
*/
|
||||
dislikes: v.software === FRIENDICA && gte(v.version, '2023.03.0'),
|
||||
dislikes: v.software === FRIENDICA && gte(v.version, '2023.3.0'),
|
||||
|
||||
/**
|
||||
* Ability to edit profile information.
|
||||
|
@ -723,6 +723,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
* @see POST /api/v1/statuses
|
||||
*/
|
||||
quotePosts: any([
|
||||
v.software === FRIENDICA && gte(v.version, '2023.3.0'),
|
||||
v.software === PLEROMA && [REBASED, AKKOMA].includes(v.build!) && gte(v.version, '2.4.50'),
|
||||
features.includes('quote_posting'),
|
||||
instance.feature_quote === true,
|
||||
|
|
Loading…
Reference in a new issue