kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge remote-tracking branch 'origin/main' into dompurify
commit
c0325498c8
|
@ -16,6 +16,10 @@
|
|||
"bugs": {
|
||||
"url": "https://gitlab.com/soapbox-pub/soapbox/-/issues"
|
||||
},
|
||||
"funding": {
|
||||
"type": "lightning",
|
||||
"url": "lightning:alex@alexgleason.me"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "npx vite serve",
|
||||
"dev": "${npm_execpath} run start",
|
||||
|
@ -92,6 +96,7 @@
|
|||
"@types/semver": "^7.3.9",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@vitejs/plugin-react": "^4.0.4",
|
||||
"@webbtc/webln-types": "^3.0.0",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"axios": "^1.2.2",
|
||||
"axios-mock-adapter": "^1.22.0",
|
||||
|
|
|
@ -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));
|
||||
|
@ -732,6 +768,8 @@ export {
|
|||
FAVOURITES_EXPAND_FAIL,
|
||||
REBLOGS_EXPAND_SUCCESS,
|
||||
REBLOGS_EXPAND_FAIL,
|
||||
ZAP_REQUEST,
|
||||
ZAP_FAIL,
|
||||
reblog,
|
||||
unreblog,
|
||||
toggleReblog,
|
||||
|
@ -801,4 +839,5 @@ export {
|
|||
remoteInteractionRequest,
|
||||
remoteInteractionSuccess,
|
||||
remoteInteractionFail,
|
||||
zap,
|
||||
};
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { NiceRelay } from 'nostr-machina';
|
||||
import { type Event } from 'nostr-tools';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { nip04, signEvent } from 'soapbox/features/nostr/sign';
|
||||
import { useInstance } from 'soapbox/hooks';
|
||||
import { connectRequestSchema } from 'soapbox/schemas/nostr';
|
||||
import { connectRequestSchema, nwcRequestSchema } from 'soapbox/schemas/nostr';
|
||||
import { jsonSchema } from 'soapbox/schemas/utils';
|
||||
|
||||
function useSignerStream() {
|
||||
|
@ -18,35 +19,62 @@ function useSignerStream() {
|
|||
}
|
||||
}, [relayUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
async function handleConnectEvent(event: Event) {
|
||||
if (!relay || !pubkey) return;
|
||||
const decrypted = await nip04.decrypt(pubkey, event.content);
|
||||
|
||||
const reqMsg = jsonSchema.pipe(connectRequestSchema).safeParse(decrypted);
|
||||
if (!reqMsg.success) {
|
||||
console.warn(decrypted);
|
||||
console.warn(reqMsg.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const respMsg = {
|
||||
id: reqMsg.data.id,
|
||||
result: await signEvent(reqMsg.data.params[0], reqMsg.data.params[1]),
|
||||
};
|
||||
|
||||
const respEvent = await signEvent({
|
||||
kind: 24133,
|
||||
content: await nip04.encrypt(pubkey, JSON.stringify(respMsg)),
|
||||
tags: [['p', pubkey]],
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
|
||||
relay.send(['EVENT', respEvent]);
|
||||
}
|
||||
|
||||
async function handleWalletEvent(event: Event) {
|
||||
if (!relay || !pubkey) return;
|
||||
|
||||
const sub = relay.req([{ kinds: [24133], authors: [pubkey], limit: 0 }]);
|
||||
const decrypted = await nip04.decrypt(pubkey, event.content);
|
||||
|
||||
const reqMsg = jsonSchema.pipe(nwcRequestSchema).safeParse(decrypted);
|
||||
if (!reqMsg.success) {
|
||||
console.warn(decrypted);
|
||||
console.warn(reqMsg.error);
|
||||
return;
|
||||
}
|
||||
|
||||
await window.webln?.enable();
|
||||
await window.webln?.sendPayment(reqMsg.data.params.invoice);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!relay || !pubkey) return;
|
||||
const sub = relay.req([{ kinds: [24133, 23194], authors: [pubkey], limit: 0 }]);
|
||||
|
||||
const readEvents = async () => {
|
||||
for await (const event of sub) {
|
||||
const decrypted = await nip04.decrypt(pubkey, event.content);
|
||||
|
||||
const reqMsg = jsonSchema.pipe(connectRequestSchema).safeParse(decrypted);
|
||||
if (!reqMsg.success) {
|
||||
console.warn(decrypted);
|
||||
console.warn(reqMsg.error);
|
||||
return;
|
||||
switch (event.kind) {
|
||||
case 24133:
|
||||
await handleConnectEvent(event);
|
||||
break;
|
||||
case 23194:
|
||||
await handleWalletEvent(event);
|
||||
break;
|
||||
}
|
||||
|
||||
const respMsg = {
|
||||
id: reqMsg.data.id,
|
||||
result: await signEvent(reqMsg.data.params[0], reqMsg.data.params[1]),
|
||||
};
|
||||
|
||||
const respEvent = await signEvent({
|
||||
kind: 24133,
|
||||
content: await nip04.encrypt(pubkey, JSON.stringify(respMsg)),
|
||||
tags: [['p', pubkey]],
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
|
||||
relay.send(['EVENT', respEvent]);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import { useHistory } from 'react-router-dom';
|
|||
import { closeDropdownMenu as closeDropdownMenuRedux, openDropdownMenu } from 'soapbox/actions/dropdown-menu';
|
||||
import { closeModal, openModal } from 'soapbox/actions/modals';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import { isUserTouching } from 'soapbox/is-mobile';
|
||||
import { userTouching } from 'soapbox/is-mobile';
|
||||
|
||||
import { IconButton, Portal } from '../ui';
|
||||
|
||||
|
@ -53,8 +53,6 @@ const DropdownMenu = (props: IDropdownMenu) => {
|
|||
|
||||
const arrowRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isOnMobile = isUserTouching();
|
||||
|
||||
const { x, y, strategy, refs, middlewareData, placement } = useFloating<HTMLButtonElement>({
|
||||
placement: initialPlacement,
|
||||
middleware: [
|
||||
|
@ -92,7 +90,7 @@ const DropdownMenu = (props: IDropdownMenu) => {
|
|||
* On mobile screens, let's replace the Popper dropdown with a Modal.
|
||||
*/
|
||||
const handleOpen = () => {
|
||||
if (isOnMobile) {
|
||||
if (userTouching.matches) {
|
||||
dispatch(
|
||||
openModal('ACTIONS', {
|
||||
status: filteredProps.status,
|
||||
|
@ -113,7 +111,7 @@ const DropdownMenu = (props: IDropdownMenu) => {
|
|||
const handleClose = () => {
|
||||
(refs.reference.current as HTMLButtonElement)?.focus();
|
||||
|
||||
if (isOnMobile) {
|
||||
if (userTouching.matches) {
|
||||
dispatch(closeModal('ACTIONS'));
|
||||
} else {
|
||||
closeDropdownMenu();
|
||||
|
|
|
@ -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<IStatusActionBar> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleZapClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
if (me) {
|
||||
dispatch(zap(status, 1337));
|
||||
} else {
|
||||
onOpenUnauthorizedModal('ZAP');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBookmarkClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
dispatch(toggleBookmark(status));
|
||||
};
|
||||
|
@ -694,6 +703,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
}
|
||||
|
||||
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<typeof HStack>['space'];
|
||||
|
@ -781,6 +791,19 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{(acceptsZaps && window.webln) && (
|
||||
<StatusActionButton
|
||||
title={intl.formatMessage(messages.zap)}
|
||||
icon={require('@tabler/icons/bolt.svg')}
|
||||
color='accent'
|
||||
filled
|
||||
onClick={handleZapClick}
|
||||
active={status.zapped}
|
||||
text={withLabels ? intl.formatMessage(messages.zap) : undefined}
|
||||
theme={statusActionButtonTheme}
|
||||
/>
|
||||
)}
|
||||
|
||||
{canShare && (
|
||||
<StatusActionButton
|
||||
title={intl.formatMessage(messages.share)}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { simpleEmojiReact } from 'soapbox/actions/emoji-reacts';
|
|||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { EmojiSelector, Portal } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { isUserTouching } from 'soapbox/is-mobile';
|
||||
import { userTouching } from 'soapbox/is-mobile';
|
||||
import { getReactForStatus } from 'soapbox/utils/emoji-reacts';
|
||||
|
||||
interface IStatusReactionWrapper {
|
||||
|
@ -39,7 +39,7 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
|
|||
clearTimeout(timeout.current);
|
||||
}
|
||||
|
||||
if (!isUserTouching()) {
|
||||
if (!userTouching.matches) {
|
||||
setVisible(true);
|
||||
}
|
||||
};
|
||||
|
@ -51,7 +51,7 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
|
|||
|
||||
// Unless the user is touching, delay closing the emoji selector briefly
|
||||
// so the user can move the mouse diagonally to make a selection.
|
||||
if (isUserTouching()) {
|
||||
if (userTouching.matches) {
|
||||
setVisible(false);
|
||||
} else {
|
||||
timeout.current = setTimeout(() => {
|
||||
|
@ -73,7 +73,7 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
|
|||
const handleClick: React.EventHandler<React.MouseEvent> = e => {
|
||||
const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji)?.name || '👍';
|
||||
|
||||
if (isUserTouching()) {
|
||||
if (userTouching.matches) {
|
||||
if (ownAccount) {
|
||||
if (visible) {
|
||||
handleReact(meEmojiReact);
|
||||
|
|
|
@ -11,7 +11,7 @@ import { closeModal, openModal } from 'soapbox/actions/modals';
|
|||
import Icon from 'soapbox/components/icon';
|
||||
import { IconButton } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useCompose } from 'soapbox/hooks';
|
||||
import { isUserTouching } from 'soapbox/is-mobile';
|
||||
import { userTouching } from 'soapbox/is-mobile';
|
||||
|
||||
import Motion from '../../ui/util/optional-motion';
|
||||
|
||||
|
@ -173,7 +173,7 @@ const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
|
|||
const onModalClose = () => dispatch(closeModal('ACTIONS'));
|
||||
|
||||
const handleToggle: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
if (isUserTouching()) {
|
||||
if (userTouching.matches) {
|
||||
if (open) {
|
||||
onModalClose();
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import CopyableInput from 'soapbox/components/copyable-input';
|
||||
import { Text, Stack, HStack, Emoji } from 'soapbox/components/ui';
|
||||
|
||||
export interface ILightningAddress {
|
||||
address: string;
|
||||
}
|
||||
|
||||
const LightningAddress: React.FC<ILightningAddress> = (props): JSX.Element => {
|
||||
const { address } = props;
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<HStack alignItems='center' className='mb-1'>
|
||||
<Emoji
|
||||
className='mr-2.5 flex w-6 items-start justify-center rtl:ml-2.5 rtl:mr-0'
|
||||
emoji='⚡'
|
||||
/>
|
||||
|
||||
<Text weight='bold'>
|
||||
<FormattedMessage id='crypto.lightning' defaultMessage='Lightning' />
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<CopyableInput value={address} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default LightningAddress;
|
|
@ -15,7 +15,7 @@ import PlaceholderStatus from 'soapbox/features/placeholder/components/placehold
|
|||
import Thread from 'soapbox/features/status/components/thread';
|
||||
import Video from 'soapbox/features/video';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { isUserTouching } from 'soapbox/is-mobile';
|
||||
import { userTouching } from 'soapbox/is-mobile';
|
||||
import { makeGetStatus } from 'soapbox/selectors';
|
||||
|
||||
import ImageLoader from '../image-loader';
|
||||
|
@ -104,7 +104,7 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
|
|||
const getIndex = () => index !== null ? index : props.index;
|
||||
|
||||
const toggleNavigation = () => {
|
||||
setNavigationHidden(value => !value && isUserTouching());
|
||||
setNavigationHidden(value => !value && userTouching.matches);
|
||||
};
|
||||
|
||||
const handleStatusClick: React.MouseEventHandler = e => {
|
||||
|
|
|
@ -4,12 +4,13 @@ import { defineMessages, useIntl, FormatDateOptions } from 'react-intl';
|
|||
|
||||
import Markup from 'soapbox/components/markup';
|
||||
import { HStack, Icon } from 'soapbox/components/ui';
|
||||
import { CryptoAddress } from 'soapbox/features/ui/util/async-components';
|
||||
import { CryptoAddress, LightningAddress } from 'soapbox/features/ui/util/async-components';
|
||||
|
||||
import type { Account } from 'soapbox/schemas';
|
||||
|
||||
const getTicker = (value: string): string => (value.match(/\$([a-zA-Z]*)/i) || [])[1];
|
||||
const isTicker = (value: string): boolean => Boolean(getTicker(value));
|
||||
const isZapEmoji = (value: string) => /^\u26A1[\uFE00-\uFE0F]?$/.test(value);
|
||||
|
||||
const messages = defineMessages({
|
||||
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
|
||||
|
@ -39,6 +40,8 @@ const ProfileField: React.FC<IProfileField> = ({ field }) => {
|
|||
address={field.value_plain}
|
||||
/>
|
||||
);
|
||||
} else if (isZapEmoji(field.name)) {
|
||||
return <LightningAddress address={field.value_plain} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -102,6 +102,7 @@ export const CryptoDonate = lazy(() => import('soapbox/features/crypto-donate'))
|
|||
export const CryptoDonatePanel = lazy(() => import('soapbox/features/crypto-donate/components/crypto-donate-panel'));
|
||||
export const CryptoAddress = lazy(() => import('soapbox/features/crypto-donate/components/crypto-address'));
|
||||
export const CryptoDonateModal = lazy(() => import('soapbox/features/ui/components/modals/crypto-donate-modal'));
|
||||
export const LightningAddress = lazy(() => import('soapbox/features/crypto-donate/components/lightning-address'));
|
||||
export const ScheduledStatuses = lazy(() => import('soapbox/features/scheduled-statuses'));
|
||||
export const UserIndex = lazy(() => import('soapbox/features/admin/user-index'));
|
||||
export const FederationRestrictions = lazy(() => import('soapbox/features/federation-restrictions'));
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
|
||||
/** Breakpoint at which the application is considered "mobile". */
|
||||
const LAYOUT_BREAKPOINT = 630;
|
||||
|
||||
|
@ -11,20 +9,7 @@ export function isMobile(width: number) {
|
|||
/** Whether the device is iOS (best guess). */
|
||||
const iOS: boolean = /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream;
|
||||
|
||||
let userTouching = false;
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } as EventListenerOptions : false;
|
||||
|
||||
function touchListener(): void {
|
||||
userTouching = true;
|
||||
window.removeEventListener('touchstart', touchListener, listenerOptions);
|
||||
}
|
||||
|
||||
window.addEventListener('touchstart', touchListener, listenerOptions);
|
||||
|
||||
/** Whether the user has touched the screen since the page loaded. */
|
||||
export function isUserTouching(): boolean {
|
||||
return userTouching;
|
||||
}
|
||||
export const userTouching = window.matchMedia('(pointer: coarse)');
|
||||
|
||||
/** Whether the device is iOS (best guess). */
|
||||
export function isIOS(): boolean {
|
||||
|
|
|
@ -528,6 +528,7 @@
|
|||
"confirmations.scheduled_status_delete.message": "Are you sure you want to discard this scheduled post?",
|
||||
"confirmations.unfollow.confirm": "Unfollow",
|
||||
"copy.success": "Copied to clipboard!",
|
||||
"crypto.lightning": "Lightning",
|
||||
"crypto_donate.explanation_box.message": "{siteTitle} accepts cryptocurrency donations. You may send a donation to any of the addresses below. Thank you for your support!",
|
||||
"crypto_donate.explanation_box.title": "Sending cryptocurrency donations",
|
||||
"crypto_donate_panel.actions.view": "Click to see {count, plural, one {# wallet} other {# wallets}}",
|
||||
|
@ -1453,6 +1454,7 @@
|
|||
"status.unmute_conversation": "Unmute Conversation",
|
||||
"status.unpin": "Unpin from profile",
|
||||
"status.unpin_to_group": "Unpin from Group",
|
||||
"status.zap": "Zap",
|
||||
"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.",
|
||||
|
|
|
@ -82,6 +82,7 @@ export const StatusRecord = ImmutableRecord({
|
|||
uri: '',
|
||||
url: '',
|
||||
visibility: 'public' as StatusVisibility,
|
||||
zapped: false,
|
||||
event: null as ReturnType<typeof EventRecord> | null,
|
||||
|
||||
// Internal fields
|
||||
|
|
|
@ -30,6 +30,8 @@ import {
|
|||
DISLIKE_REQUEST,
|
||||
UNDISLIKE_REQUEST,
|
||||
DISLIKE_FAIL,
|
||||
ZAP_REQUEST,
|
||||
ZAP_FAIL,
|
||||
} from '../actions/interactions';
|
||||
import {
|
||||
STATUS_CREATE_REQUEST,
|
||||
|
@ -234,6 +236,18 @@ const simulateDislike = (
|
|||
return state.set(statusId, updatedStatus);
|
||||
};
|
||||
|
||||
/** Simulate zap of status for optimistic interactions */
|
||||
const simulateZap = (state: State, statusId: string, zapped: boolean): State => {
|
||||
const status = state.get(statusId);
|
||||
if (!status) return state;
|
||||
|
||||
const updatedStatus = status.merge({
|
||||
zapped,
|
||||
});
|
||||
|
||||
return state.set(statusId, updatedStatus);
|
||||
};
|
||||
|
||||
interface Translation {
|
||||
content: string;
|
||||
detected_source_language: string;
|
||||
|
@ -288,6 +302,10 @@ export default function statuses(state = initialState, action: AnyAction): State
|
|||
return state.get(action.status.id) === undefined ? state : state.setIn([action.status.id, 'favourited'], false);
|
||||
case DISLIKE_FAIL:
|
||||
return state.get(action.status.id) === undefined ? state : state.setIn([action.status.id, 'disliked'], false);
|
||||
case ZAP_REQUEST:
|
||||
return simulateZap(state, action.status.id, true);
|
||||
case ZAP_FAIL:
|
||||
return simulateZap(state, action.status.id, false);
|
||||
case REBLOG_REQUEST:
|
||||
return state.setIn([action.status.id, 'reblogged'], true);
|
||||
case REBLOG_FAIL:
|
||||
|
|
|
@ -7,7 +7,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';
|
||||
|
||||
|
@ -30,6 +30,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),
|
||||
|
|
|
@ -36,4 +36,12 @@ const connectRequestSchema = z.object({
|
|||
params: z.tuple([eventTemplateSchema]).or(z.tuple([eventTemplateSchema, signEventOptsSchema])),
|
||||
});
|
||||
|
||||
export { nostrIdSchema, kindSchema, eventSchema, signedEventSchema, connectRequestSchema };
|
||||
/** NIP-47 signer response. */
|
||||
const nwcRequestSchema = z.object({
|
||||
method: z.literal('pay_invoice'),
|
||||
params: z.object({
|
||||
invoice: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export { nostrIdSchema, kindSchema, eventSchema, signedEventSchema, connectRequestSchema, nwcRequestSchema };
|
|
@ -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<typeof baseStatusSchema>;
|
||||
|
|
|
@ -24,7 +24,8 @@
|
|||
"types": [
|
||||
"vite/client",
|
||||
"vitest/globals",
|
||||
"vite-plugin-compile-time/client"
|
||||
"vite-plugin-compile-time/client",
|
||||
"@webbtc/webln-types"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2914,6 +2914,11 @@
|
|||
"@webassemblyjs/ast" "1.11.6"
|
||||
"@xtuc/long" "4.2.2"
|
||||
|
||||
"@webbtc/webln-types@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@webbtc/webln-types/-/webln-types-3.0.0.tgz#448b2138423865087ba8859e9e6430fc2463b864"
|
||||
integrity sha512-aXfTHLKz5lysd+6xTeWl+qHNh/p3qVYbeLo+yDN5cUDmhie2ZoGvkppfWxzbGkcFBzb6dJyQ2/i2cbmDHas+zQ==
|
||||
|
||||
"@xtuc/ieee754@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
|
||||
|
|
Ładowanie…
Reference in New Issue