diff --git a/src/actions/interactions.ts b/src/actions/interactions.ts index bfe63801c..ddd29c2c7 100644 --- a/src/actions/interactions.ts +++ b/src/actions/interactions.ts @@ -78,6 +78,10 @@ const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL'; const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS'; const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL'; +const ZAP_REQUEST = 'ZAP_REQUEST'; +const ZAP_SUCCESS = 'ZAP_SUCCESS'; +const ZAP_FAIL = 'ZAP_FAIL'; + const messages = defineMessages({ bookmarkAdded: { id: 'status.bookmarked', defaultMessage: 'Bookmark added.' }, bookmarkRemoved: { id: 'status.unbookmarked', defaultMessage: 'Bookmark removed.' }, @@ -306,6 +310,38 @@ const undislikeFail = (status: StatusEntity, error: unknown) => ({ skipLoading: true, }); +const zap = (status: StatusEntity, amount: number) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(zapRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.id}/zap`, { amount }).then(function(response) { + dispatch(zapSuccess(status)); + }).catch(function(error) { + dispatch(zapFail(status, error)); + }); + }; + +const zapRequest = (status: StatusEntity) => ({ + type: ZAP_REQUEST, + status: status, + skipLoading: true, +}); + +const zapSuccess = (status: StatusEntity) => ({ + type: ZAP_SUCCESS, + status: status, + skipLoading: true, +}); + +const zapFail = (status: StatusEntity, error: unknown) => ({ + type: ZAP_FAIL, + status: status, + error: error, + skipLoading: true, +}); + const bookmark = (status: StatusEntity) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(bookmarkRequest(status)); @@ -801,4 +837,5 @@ export { remoteInteractionRequest, remoteInteractionSuccess, remoteInteractionFail, + zap, }; diff --git a/src/components/status-action-bar.tsx b/src/components/status-action-bar.tsx index b07550ed7..f8d252dc4 100644 --- a/src/components/status-action-bar.tsx +++ b/src/components/status-action-bar.tsx @@ -6,7 +6,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 { pinToGroup, toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog, unpinFromGroup } from 'soapbox/actions/interactions'; +import { pinToGroup, toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog, unpinFromGroup, zap } from 'soapbox/actions/interactions'; import { openModal } from 'soapbox/actions/modals'; import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation'; import { initMuteModal } from 'soapbox/actions/mutes'; @@ -103,6 +103,7 @@ const messages = defineMessages({ unmuteSuccess: { id: 'group.unmute.success', defaultMessage: 'Unmuted the group' }, unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, unpinFromGroup: { id: 'status.unpin_to_group', defaultMessage: 'Unpin from Group' }, + zap: { id: 'status.zap', defaultMessage: 'Zap' }, }); interface IStatusActionBar { @@ -188,6 +189,14 @@ const StatusActionBar: React.FC = ({ } }; + const handleZapClick: React.EventHandler = (e) => { + if (me) { + dispatch(zap(status, 1337)); + } else { + onOpenUnauthorizedModal('ZAP'); + } + }; + const handleBookmarkClick: React.EventHandler = (e) => { dispatch(toggleBookmark(status)); }; @@ -694,6 +703,7 @@ const StatusActionBar: React.FC = ({ } const canShare = ('share' in navigator) && (status.visibility === 'public' || status.visibility === 'group'); + const acceptsZaps = status.account.ditto.accepts_zaps === true; const spacing: { [key: string]: React.ComponentProps['space']; @@ -781,6 +791,19 @@ const StatusActionBar: React.FC = ({ /> )} + {acceptsZaps && ( + + )} + {canShare && ( | null, // Internal fields diff --git a/src/schemas/account.ts b/src/schemas/account.ts index fec6c18b2..a0a718db7 100644 --- a/src/schemas/account.ts +++ b/src/schemas/account.ts @@ -6,7 +6,7 @@ import { unescapeHTML } from 'soapbox/utils/html'; import { customEmojiSchema } from './custom-emoji'; import { relationshipSchema } from './relationship'; -import { contentSchema, filteredArray, makeCustomEmojiMap } from './utils'; +import { coerceObject, contentSchema, filteredArray, makeCustomEmojiMap } from './utils'; import type { Resolve } from 'soapbox/utils/types'; @@ -29,6 +29,9 @@ const baseAccountSchema = z.object({ created_at: z.string().datetime().catch(new Date().toUTCString()), discoverable: z.boolean().catch(false), display_name: z.string().catch(''), + ditto: coerceObject({ + accepts_zaps: z.boolean().catch(false), + }), emojis: filteredArray(customEmojiSchema), fields: filteredArray(fieldSchema), followers_count: z.number().catch(0), diff --git a/src/schemas/status.ts b/src/schemas/status.ts index cf66e4e3d..cc370871a 100644 --- a/src/schemas/status.ts +++ b/src/schemas/status.ts @@ -67,6 +67,7 @@ const baseStatusSchema = z.object({ uri: z.string().url().catch(''), url: z.string().url().catch(''), visibility: z.string().catch('public'), + zapped: z.coerce.boolean(), }); type BaseStatus = z.infer;