Merge branch 'render-zaps-amount' into 'main'
Render zaps amount in posts - Fetch zapped_by endpoint - Create zaps amount modal See merge request soapbox-pub/soapbox!3062
This commit is contained in:
commit
f5d1ac7b27
|
@ -84,6 +84,10 @@ const ZAP_REQUEST = 'ZAP_REQUEST';
|
|||
const ZAP_SUCCESS = 'ZAP_SUCCESS';
|
||||
const ZAP_FAIL = 'ZAP_FAIL';
|
||||
|
||||
const ZAPS_FETCH_REQUEST = 'ZAPS_FETCH_REQUEST';
|
||||
const ZAPS_FETCH_SUCCESS = 'ZAPS_FETCH_SUCCESS';
|
||||
const ZAPS_FETCH_FAIL = 'ZAPS_FETCH_FAIL';
|
||||
|
||||
const messages = defineMessages({
|
||||
bookmarkAdded: { id: 'status.bookmarked', defaultMessage: 'Bookmark added.' },
|
||||
bookmarkRemoved: { id: 'status.unbookmarked', defaultMessage: 'Bookmark removed.' },
|
||||
|
@ -625,6 +629,35 @@ const fetchReactionsFail = (id: string, error: unknown) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
const fetchZaps = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchZapsRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/ditto/statuses/${id}/zapped_by`).then(response => {
|
||||
dispatch(importFetchedAccounts((response.data as APIEntity[]).map(({ account }) => account).flat()));
|
||||
dispatch(fetchZapsSuccess(id, response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchZapsFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const fetchZapsRequest = (id: string) => ({
|
||||
type: ZAPS_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
const fetchZapsSuccess = (id: string, zaps: APIEntity[]) => ({
|
||||
type: ZAPS_FETCH_SUCCESS,
|
||||
id,
|
||||
zaps,
|
||||
});
|
||||
|
||||
const fetchZapsFail = (id: string, error: unknown) => ({
|
||||
type: REACTIONS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const pin = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
@ -802,6 +835,9 @@ export {
|
|||
REBLOGS_EXPAND_FAIL,
|
||||
ZAP_REQUEST,
|
||||
ZAP_FAIL,
|
||||
ZAPS_FETCH_REQUEST,
|
||||
ZAPS_FETCH_SUCCESS,
|
||||
ZAPS_FETCH_FAIL,
|
||||
reblog,
|
||||
unreblog,
|
||||
toggleReblog,
|
||||
|
@ -872,4 +908,5 @@ export {
|
|||
remoteInteractionSuccess,
|
||||
remoteInteractionFail,
|
||||
zap,
|
||||
fetchZaps,
|
||||
};
|
||||
|
|
|
@ -810,6 +810,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
active={status.zapped}
|
||||
text={withLabels ? intl.formatMessage(messages.zap) : undefined}
|
||||
theme={statusActionButtonTheme}
|
||||
count={status?.zaps_amount ? status.zaps_amount / 1000 : 0}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
@ -55,6 +55,13 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
|
|||
}));
|
||||
};
|
||||
|
||||
const onOpenZapsModal = (username: string, statusId: string): void => {
|
||||
dispatch(openModal('ZAPS', {
|
||||
username,
|
||||
statusId,
|
||||
}));
|
||||
};
|
||||
|
||||
const getNormalizedReacts = () => {
|
||||
return reduceEmoji(
|
||||
status.reactions,
|
||||
|
@ -189,11 +196,36 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
|
|||
return null;
|
||||
};
|
||||
|
||||
const handleOpenZapsModal = () => {
|
||||
if (!me) {
|
||||
return onOpenUnauthorizedModal();
|
||||
}
|
||||
|
||||
onOpenZapsModal(account.acct, status.id);
|
||||
};
|
||||
|
||||
const getZaps = () => {
|
||||
if (status.zaps_amount) {
|
||||
return (
|
||||
<InteractionCounter count={status.zaps_amount / 1000} onClick={handleOpenZapsModal}>
|
||||
<FormattedMessage
|
||||
id='status.interactions.zaps'
|
||||
defaultMessage='{count, plural, one {Zap} other {Zaps}}'
|
||||
values={{ count: status.zaps_amount }}
|
||||
/>
|
||||
</InteractionCounter>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<HStack space={3}>
|
||||
{getReposts()}
|
||||
{getQuotes()}
|
||||
{(features.emojiReacts || features.emojiReactsMastodon) ? getEmojiReacts() : getFavourites()}
|
||||
{getZaps()}
|
||||
{getDislikes()}
|
||||
</HStack>
|
||||
);
|
||||
|
|
|
@ -44,6 +44,7 @@ import {
|
|||
EditRuleModal,
|
||||
ZapPayRequestModal,
|
||||
ZapInvoiceModal,
|
||||
ZapsModal,
|
||||
} from 'soapbox/features/ui/util/async-components';
|
||||
|
||||
import ModalLoading from './modal-loading';
|
||||
|
@ -90,6 +91,7 @@ const MODAL_COMPONENTS: Record<string, React.LazyExoticComponent<any>> = {
|
|||
'SELECT_BOOKMARK_FOLDER': SelectBookmarkFolderModal,
|
||||
'UNAUTHORIZED': UnauthorizedModal,
|
||||
'VIDEO': VideoModal,
|
||||
'ZAPS': ZapsModal,
|
||||
'ZAP_INVOICE': ZapInvoiceModal,
|
||||
'ZAP_PAY_REQUEST': ZapPayRequestModal,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { fetchZaps } from 'soapbox/actions/interactions';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Modal, Spinner, Text } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||
|
||||
interface IAccountWithZaps {
|
||||
id: string;
|
||||
zap_comment: string;
|
||||
zap_amount: number;
|
||||
}
|
||||
|
||||
interface IZapsModal {
|
||||
onClose: (string: string) => void;
|
||||
statusId: string;
|
||||
}
|
||||
|
||||
const ZapsModal: React.FC<IZapsModal> = ({ onClose, statusId }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const zaps = useAppSelector((state) => state.user_lists.zapped_by.get(statusId)?.items);
|
||||
|
||||
|
||||
const accounts = useMemo((): ImmutableList<IAccountWithZaps> | undefined => {
|
||||
if (!zaps) return;
|
||||
|
||||
return zaps.map(({ account, zap_amount, zap_comment }) =>({ id: account, zap_amount, zap_comment })).flatten() as ImmutableList<IAccountWithZaps>;
|
||||
}, [zaps]);
|
||||
|
||||
const fetchData = () => {
|
||||
dispatch(fetchZaps(statusId));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const onClickClose = () => {
|
||||
onClose('ZAPS');
|
||||
};
|
||||
|
||||
let body;
|
||||
|
||||
if (!zaps || !accounts) {
|
||||
body = <Spinner />;
|
||||
} else {
|
||||
const emptyMessage = <FormattedMessage id='status.zaps.empty' defaultMessage='No one has zapped this post yet. When someone does, they will show up here.' />;
|
||||
|
||||
body = (
|
||||
<ScrollableList
|
||||
scrollKey='zaps'
|
||||
emptyMessage={emptyMessage}
|
||||
listClassName='max-w-full'
|
||||
itemClassName='pb-3'
|
||||
style={{ height: '80vh' }}
|
||||
useWindowScroll={false}
|
||||
>
|
||||
{accounts.map((account) => {
|
||||
return (
|
||||
<div>
|
||||
<Text weight='bold'>
|
||||
{shortNumberFormat(account.zap_amount / 1000)}
|
||||
</Text>
|
||||
<AccountContainer key={account.id} id={account.id} note={account.zap_comment} emoji='⚡' />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</ScrollableList>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<FormattedMessage id='column.zaps' defaultMessage='Zaps' />}
|
||||
onClose={onClickClose}
|
||||
>
|
||||
{body}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ZapsModal;
|
|
@ -178,3 +178,4 @@ export const EditRuleModal = lazy(() => import('soapbox/features/ui/components/m
|
|||
export const AdminNostrRelays = lazy(() => import('soapbox/features/admin/nostr-relays'));
|
||||
export const ZapPayRequestModal = lazy(() => import('soapbox/features/ui/components/modals/zap-pay-request'));
|
||||
export const ZapInvoiceModal = lazy(() => import('soapbox/features/ui/components/modals/zap-invoice'));
|
||||
export const ZapsModal = lazy(() => import('soapbox/features/ui/components/modals/zaps-modal'));
|
||||
|
|
|
@ -436,6 +436,7 @@
|
|||
"column.settings_store": "Settings store",
|
||||
"column.soapbox_config": "Soapbox config",
|
||||
"column.test": "Test timeline",
|
||||
"column.zaps": "Zaps",
|
||||
"column_forbidden.body": "You do not have permission to access this page.",
|
||||
"column_forbidden.title": "Forbidden",
|
||||
"common.cancel": "Cancel",
|
||||
|
@ -1519,6 +1520,7 @@
|
|||
"status.interactions.favourites": "{count, plural, one {Like} other {Likes}}",
|
||||
"status.interactions.quotes": "{count, plural, one {Quote} other {Quotes}}",
|
||||
"status.interactions.reblogs": "{count, plural, one {Repost} other {Reposts}}",
|
||||
"status.interactions.zaps": "{count, plural, one {Zap} other {Zaps}}",
|
||||
"status.load_more": "Load more",
|
||||
"status.mention": "Mention @{name}",
|
||||
"status.more": "More",
|
||||
|
@ -1566,6 +1568,7 @@
|
|||
"status.unpin": "Unpin from profile",
|
||||
"status.unpin_to_group": "Unpin from Group",
|
||||
"status.zap": "Zap",
|
||||
"status.zaps.empty": "No one has zapped this post yet. When someone does, they will show up here.",
|
||||
"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.",
|
||||
|
|
|
@ -75,6 +75,7 @@ export const StatusRecord = ImmutableRecord({
|
|||
reblogged: false,
|
||||
reblogs_count: 0,
|
||||
replies_count: 0,
|
||||
zaps_amount: 0,
|
||||
sensitive: false,
|
||||
spoiler_text: '',
|
||||
tags: ImmutableList<ImmutableMap<string, any>>(),
|
||||
|
|
|
@ -64,6 +64,7 @@ import {
|
|||
FAVOURITES_EXPAND_SUCCESS,
|
||||
DISLIKES_FETCH_SUCCESS,
|
||||
REACTIONS_FETCH_SUCCESS,
|
||||
ZAPS_FETCH_SUCCESS,
|
||||
} from 'soapbox/actions/interactions';
|
||||
import {
|
||||
NOTIFICATIONS_UPDATE,
|
||||
|
@ -90,6 +91,17 @@ const ReactionListRecord = ImmutableRecord({
|
|||
isLoading: false,
|
||||
});
|
||||
|
||||
export const ZapRecord = ImmutableRecord({
|
||||
account: '',
|
||||
zap_comment: '',
|
||||
zap_amount: 0, // in millisats
|
||||
});
|
||||
|
||||
const ZapListRecord = ImmutableRecord({
|
||||
items: ImmutableOrderedSet<Zap>(),
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
export const ParticipationRequestRecord = ImmutableRecord({
|
||||
account: '',
|
||||
participation_message: null as string | null,
|
||||
|
@ -108,6 +120,7 @@ export const ReducerRecord = ImmutableRecord({
|
|||
favourited_by: ImmutableMap<string, List>(),
|
||||
disliked_by: ImmutableMap<string, List>(),
|
||||
reactions: ImmutableMap<string, ReactionList>(),
|
||||
zapped_by: ImmutableMap<string, ZapList>(),
|
||||
follow_requests: ListRecord(),
|
||||
blocks: ListRecord(),
|
||||
mutes: ListRecord(),
|
||||
|
@ -125,10 +138,12 @@ type State = ReturnType<typeof ReducerRecord>;
|
|||
export type List = ReturnType<typeof ListRecord>;
|
||||
type Reaction = ReturnType<typeof ReactionRecord>;
|
||||
type ReactionList = ReturnType<typeof ReactionListRecord>;
|
||||
type Zap = ReturnType<typeof ZapRecord>;
|
||||
type ZapList = ReturnType<typeof ZapListRecord>;
|
||||
type ParticipationRequest = ReturnType<typeof ParticipationRequestRecord>;
|
||||
type ParticipationRequestList = ReturnType<typeof ParticipationRequestListRecord>;
|
||||
type Items = ImmutableOrderedSet<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 NestedListPath = ['followers' | 'following' | 'reblogged_by' | 'favourited_by' | 'disliked_by' | 'reactions' | 'pinned' | 'birthday_reminders' | 'familiar_followers' | 'event_participations' | 'event_participation_requests' | 'membership_requests' | 'group_blocks' | 'zapped_by', string];
|
||||
type ListPath = ['follow_requests' | 'blocks' | 'mutes' | 'directory'];
|
||||
|
||||
const normalizeList = (state: State, path: NestedListPath | ListPath, accounts: APIEntity[], next?: string | null) => {
|
||||
|
@ -186,6 +201,13 @@ export default function userLists(state = ReducerRecord(), action: AnyAction) {
|
|||
accounts: ImmutableOrderedSet(accounts.map((account: APIEntity) => account.id)),
|
||||
}))),
|
||||
}));
|
||||
case ZAPS_FETCH_SUCCESS:
|
||||
return state.setIn(['zapped_by', action.id], ZapListRecord({
|
||||
items: ImmutableOrderedSet<Zap>(action.zaps.map(({ account, ...zap }: APIEntity) => ZapRecord({
|
||||
...zap,
|
||||
account: account.id,
|
||||
}))),
|
||||
}));
|
||||
case NOTIFICATIONS_UPDATE:
|
||||
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
|
||||
case FOLLOW_REQUESTS_FETCH_SUCCESS:
|
||||
|
|
Loading…
Reference in New Issue