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
environments/review-main-yi2y9f/deployments/4694
Alex Gleason 3 months ago
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…
Cancel
Save