kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Porównaj commity
22 Commity
191876a3cf
...
29f4322d01
Autor | SHA1 | Data |
---|---|---|
marcin mikołajczak | 29f4322d01 | |
Alex Gleason | 1f665defb6 | |
Alex Gleason | 80b500e710 | |
Alex Gleason | 2869b2e9f3 | |
Alex Gleason | 235476b7b2 | |
Alex Gleason | ac46a89068 | |
Alex Gleason | 61ead81ed9 | |
Alex Gleason | 060014ff92 | |
Alex Gleason | 4e7a259dd4 | |
Alex Gleason | 8d20a14251 | |
Alex Gleason | c41b2c794e | |
Alex Gleason | 1a0970c3bd | |
Alex Gleason | 35354d16b5 | |
Alex Gleason | 49a25f2eb0 | |
Alex Gleason | df407381b2 | |
Alex Gleason | 6bfc0f95a4 | |
Alex Gleason | 1cd41450ee | |
Alex Gleason | d2a28ea3c9 | |
marcin mikołajczak | 39a9e174d4 | |
marcin mikołajczak | 43cc692c37 | |
marcin mikołajczak | 8ef1de5f7a | |
marcin mikołajczak | 75be34ce45 |
|
@ -63,6 +63,7 @@
|
|||
"@lexical/utils": "^0.14.2",
|
||||
"@mkljczk/react-hotkeys": "^1.2.2",
|
||||
"@noble/hashes": "^1.3.3",
|
||||
"@nostrify/nostrify": "npm:@jsr/nostrify__nostrify",
|
||||
"@popperjs/core": "^2.11.5",
|
||||
"@reach/combobox": "^0.18.0",
|
||||
"@reach/menu-button": "^0.18.0",
|
||||
|
@ -73,7 +74,6 @@
|
|||
"@sentry/browser": "^7.74.1",
|
||||
"@sentry/react": "^7.74.1",
|
||||
"@soapbox.pub/wasmboy": "^0.8.0",
|
||||
"@soapbox/nspec": "npm:@jsr/soapbox__nspec",
|
||||
"@soapbox/weblock": "npm:@jsr/soapbox__weblock",
|
||||
"@tabler/icons": "^3.1.0",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
|
@ -187,7 +187,7 @@
|
|||
"vite-plugin-require": "^1.1.10",
|
||||
"vite-plugin-static-copy": "^1.0.0",
|
||||
"wicg-inert": "^3.1.1",
|
||||
"zod": "^3.22.4"
|
||||
"zod": "^3.23.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^6.2.0",
|
||||
|
|
|
@ -1,37 +1,91 @@
|
|||
import { type NostrEvent } from '@soapbox/nspec';
|
||||
import { NostrEvent, NostrConnectResponse, NSchema as n } from '@nostrify/nostrify';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useNostr } from 'soapbox/contexts/nostr-context';
|
||||
import { connectRequestSchema, nwcRequestSchema } from 'soapbox/schemas/nostr';
|
||||
import { jsonSchema } from 'soapbox/schemas/utils';
|
||||
import { nwcRequestSchema } from 'soapbox/schemas/nostr';
|
||||
|
||||
function useSignerStream() {
|
||||
const { relay, pubkey, signer } = useNostr();
|
||||
|
||||
async function sendConnect(response: NostrConnectResponse) {
|
||||
if (!relay || !pubkey || !signer) return;
|
||||
|
||||
const event = await signer.signEvent({
|
||||
kind: 24133,
|
||||
content: await signer.nip04!.encrypt(pubkey, JSON.stringify(response)),
|
||||
tags: [['p', pubkey]],
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
|
||||
relay.event(event);
|
||||
}
|
||||
|
||||
async function handleConnectEvent(event: NostrEvent) {
|
||||
if (!relay || !pubkey || !signer) return;
|
||||
const decrypted = await signer.nip04!.decrypt(pubkey, event.content);
|
||||
|
||||
const reqMsg = jsonSchema.pipe(connectRequestSchema).safeParse(decrypted);
|
||||
const reqMsg = n.json().pipe(n.connectRequest()).safeParse(decrypted);
|
||||
if (!reqMsg.success) {
|
||||
console.warn(decrypted);
|
||||
console.warn(reqMsg.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const respMsg = {
|
||||
id: reqMsg.data.id,
|
||||
result: JSON.stringify(await signer.signEvent(JSON.parse(reqMsg.data.params[0]))),
|
||||
};
|
||||
const request = reqMsg.data;
|
||||
|
||||
const respEvent = await signer.signEvent({
|
||||
kind: 24133,
|
||||
content: await signer.nip04!.encrypt(pubkey, JSON.stringify(respMsg)),
|
||||
tags: [['p', pubkey]],
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
|
||||
relay.event(respEvent);
|
||||
switch (request.method) {
|
||||
case 'connect':
|
||||
return sendConnect({
|
||||
id: request.id,
|
||||
result: 'ack',
|
||||
});
|
||||
case 'sign_event':
|
||||
return sendConnect({
|
||||
id: request.id,
|
||||
result: JSON.stringify(await signer.signEvent(JSON.parse(request.params[0]))),
|
||||
});
|
||||
case 'ping':
|
||||
return sendConnect({
|
||||
id: request.id,
|
||||
result: 'pong',
|
||||
});
|
||||
case 'get_relays':
|
||||
return sendConnect({
|
||||
id: request.id,
|
||||
result: JSON.stringify(await signer.getRelays?.() ?? []),
|
||||
});
|
||||
case 'get_public_key':
|
||||
return sendConnect({
|
||||
id: request.id,
|
||||
result: await signer.getPublicKey(),
|
||||
});
|
||||
case 'nip04_encrypt':
|
||||
return sendConnect({
|
||||
id: request.id,
|
||||
result: await signer.nip04!.encrypt(request.params[0], request.params[1]),
|
||||
});
|
||||
case 'nip04_decrypt':
|
||||
return sendConnect({
|
||||
id: request.id,
|
||||
result: await signer.nip04!.decrypt(request.params[0], request.params[1]),
|
||||
});
|
||||
case 'nip44_encrypt':
|
||||
return sendConnect({
|
||||
id: request.id,
|
||||
result: await signer.nip44!.encrypt(request.params[0], request.params[1]),
|
||||
});
|
||||
case 'nip44_decrypt':
|
||||
return sendConnect({
|
||||
id: request.id,
|
||||
result: await signer.nip44!.decrypt(request.params[0], request.params[1]),
|
||||
});
|
||||
default:
|
||||
return sendConnect({
|
||||
id: request.id,
|
||||
result: '',
|
||||
error: `Unrecognized method: ${request.method}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWalletEvent(event: NostrEvent) {
|
||||
|
@ -39,7 +93,7 @@ function useSignerStream() {
|
|||
|
||||
const decrypted = await signer.nip04!.decrypt(pubkey, event.content);
|
||||
|
||||
const reqMsg = jsonSchema.pipe(nwcRequestSchema).safeParse(decrypted);
|
||||
const reqMsg = n.json().pipe(nwcRequestSchema).safeParse(decrypted);
|
||||
if (!reqMsg.success) {
|
||||
console.warn(decrypted);
|
||||
console.warn(reqMsg.error);
|
||||
|
|
|
@ -11,7 +11,7 @@ import { useAccount } from 'soapbox/api/hooks';
|
|||
import Account from 'soapbox/components/account';
|
||||
import { Stack, Divider, HStack, Icon, IconButton, Text } from 'soapbox/components/ui';
|
||||
import ProfileStats from 'soapbox/features/ui/components/profile-stats';
|
||||
import { useAppDispatch, useAppSelector, useGroupsPath, useFeatures } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector, useGroupsPath, useFeatures, useInstance } from 'soapbox/hooks';
|
||||
import { makeGetOtherAccounts } from 'soapbox/selectors';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
|
@ -89,6 +89,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
const settings = useAppSelector((state) => getSettings(state));
|
||||
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
|
||||
const groupsPath = useGroupsPath();
|
||||
const instance = useInstance();
|
||||
|
||||
const closeButtonRef = React.useRef(null);
|
||||
|
||||
|
@ -248,16 +249,16 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
|
||||
<SidebarLink
|
||||
to='/timeline/local'
|
||||
icon={features.federating ? require('@tabler/icons/outline/affiliate.svg') : require('@tabler/icons/outline/world.svg')}
|
||||
text={features.federating ? <FormattedMessage id='tabs_bar.local' defaultMessage='Local' /> : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />}
|
||||
icon={features.federating ? require('@tabler/icons/outline/users-group.svg') : require('@tabler/icons/outline/world.svg')}
|
||||
text={features.federating ? instance.title : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{features.federating && (
|
||||
<SidebarLink
|
||||
to='/timeline/fediverse'
|
||||
icon={require('@tabler/icons/outline/topology-star-ring-3.svg')}
|
||||
text={<FormattedMessage id='tabs_bar.fediverse' defaultMessage='Fediverse' />}
|
||||
to='/timeline/global'
|
||||
icon={require('@tabler/icons/outline/world.svg')}
|
||||
text={<FormattedMessage id='tabs_bar.global' defaultMessage='Global' />}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
@ -265,12 +266,14 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
|
||||
<Divider />
|
||||
|
||||
<SidebarLink
|
||||
to='/blocks'
|
||||
icon={require('@tabler/icons/outline/ban.svg')}
|
||||
text={intl.formatMessage(messages.blocks)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
{features.blocks && (
|
||||
<SidebarLink
|
||||
to='/blocks'
|
||||
icon={require('@tabler/icons/outline/ban.svg')}
|
||||
text={intl.formatMessage(messages.blocks)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SidebarLink
|
||||
to='/mutes'
|
||||
|
|
|
@ -179,17 +179,16 @@ const SidebarNavigation = () => {
|
|||
{(account || !restrictUnauth.timelines.local) && (
|
||||
<SidebarNavigationLink
|
||||
to='/timeline/local'
|
||||
icon={features.federating ? require('@tabler/icons/outline/affiliate.svg') : require('@tabler/icons/outline/world.svg')}
|
||||
activeIcon={features.federating ? require('@tabler/icons/filled/affiliate.svg') : undefined}
|
||||
text={features.federating ? <FormattedMessage id='tabs_bar.local' defaultMessage='Local' /> : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />}
|
||||
icon={features.federating ? require('@tabler/icons/outline/users-group.svg') : require('@tabler/icons/outline/world.svg')}
|
||||
text={features.federating ? instance.title : <FormattedMessage id='tabs_bar.global' defaultMessage='Global' />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(features.federating && (account || !restrictUnauth.timelines.federated)) && (
|
||||
<SidebarNavigationLink
|
||||
to='/timeline/fediverse'
|
||||
icon={require('@tabler/icons/outline/topology-star-ring-3.svg')}
|
||||
text={<FormattedMessage id='tabs_bar.fediverse' defaultMessage='Fediverse' />}
|
||||
to='/timeline/global'
|
||||
icon={require('@tabler/icons/outline/world.svg')}
|
||||
text={<FormattedMessage id='tabs_bar.global' defaultMessage='Global' />}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { NRelay, NRelay1, NostrSigner } from '@soapbox/nspec';
|
||||
import { NRelay, NRelay1, NostrSigner } from '@nostrify/nostrify';
|
||||
import React, { createContext, useContext, useState, useEffect, useMemo } from 'react';
|
||||
|
||||
import { NKeys } from 'soapbox/features/nostr/keys';
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
|
||||
const relayEntitySchema = z.object({
|
||||
url: z.string().url(),
|
||||
marker: z.enum(['read', 'write']).optional(),
|
||||
});
|
||||
|
||||
export function useAdminNostrRelays() {
|
||||
const api = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['NostrRelay'],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get('/api/v1/admin/ditto/relays');
|
||||
return relayEntitySchema.array().parse(data);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { useMutation } from '@tanstack/react-query';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { Button, Column, Form, FormActions, Stack } from 'soapbox/components/ui';
|
||||
import RelayEditor, { RelayData } from 'soapbox/features/nostr-relays/components/relay-editor';
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
|
||||
import { useAdminNostrRelays } from './hooks/useAdminNostrRelays';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.admin.nostr_relays', defaultMessage: 'Relays' },
|
||||
});
|
||||
|
||||
const AdminNostrRelays: React.FC = () => {
|
||||
const api = useApi();
|
||||
const intl = useIntl();
|
||||
const result = useAdminNostrRelays();
|
||||
|
||||
const [relays, setRelays] = useState<RelayData[]>(result.data ?? []);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => api.put('/api/v1/admin/ditto/relays', relays),
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
mutation.mutate();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setRelays(result.data ?? []);
|
||||
}, [result.data]);
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Stack space={4}>
|
||||
<RelayEditor relays={relays} setRelays={setRelays} />
|
||||
|
||||
<FormActions>
|
||||
<Button to='/soapbox/admin' theme='tertiary'>
|
||||
<FormattedMessage id='common.cancel' defaultMessage='Cancel' />
|
||||
</Button>
|
||||
|
||||
<Button theme='primary' type='submit' disabled={mutation.isPending}>
|
||||
<FormattedMessage id='edit_profile.save' defaultMessage='Save' />
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Stack>
|
||||
</Form>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminNostrRelays;
|
|
@ -113,6 +113,13 @@ const Dashboard: React.FC = () => {
|
|||
label={<FormattedMessage id='column.admin.domains' defaultMessage='Domains' />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{features.nostr && (
|
||||
<ListItem
|
||||
to='/soapbox/admin/nostr/relays'
|
||||
label={<FormattedMessage id='column.admin.nostr_relays' defaultMessage='Relays' />}
|
||||
/>
|
||||
)}
|
||||
</List>
|
||||
|
||||
{account.admin && (
|
||||
|
|
|
@ -118,10 +118,6 @@ const SettingsStore: React.FC = () => {
|
|||
<SettingToggle settings={settings} settingPath={['unfollowModal']} onChange={onToggleChange} />
|
||||
</ListItem>
|
||||
|
||||
<ListItem label={<FormattedMessage id='preferences.fields.missing_description_modal_label' defaultMessage='Show confirmation dialog before sending a post without media descriptions' />}>
|
||||
<SettingToggle settings={settings} settingPath={['missingDescriptionModal']} onChange={onToggleChange} />
|
||||
</ListItem>
|
||||
|
||||
<ListItem label={<FormattedMessage id='preferences.fields.reduce_motion_label' defaultMessage='Reduce motion in animations' />}>
|
||||
<SettingToggle settings={settings} settingPath={['reduceMotion']} onChange={onToggleChange} />
|
||||
</ListItem>
|
||||
|
|
|
@ -18,8 +18,8 @@ const NostrRelays = () => {
|
|||
const { relay, signer } = useNostr();
|
||||
|
||||
const { events } = useNostrReq(
|
||||
account?.nostr
|
||||
? [{ kinds: [10002], authors: [account?.nostr.pubkey], limit: 1 }]
|
||||
account?.nostr?.pubkey
|
||||
? [{ kinds: [10002], authors: [account.nostr.pubkey], limit: 1 }]
|
||||
: [],
|
||||
);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { NSchema as n, NostrSigner, NSecSigner } from '@soapbox/nspec';
|
||||
import { NSchema as n, NostrSigner, NSecSigner } from '@nostrify/nostrify';
|
||||
import { WebLock } from '@soapbox/weblock';
|
||||
import { getPublicKey, nip19 } from 'nostr-tools';
|
||||
import { z } from 'zod';
|
||||
|
@ -22,7 +22,7 @@ export class NKeyStorage implements ReadonlyMap<string, NostrSigner> {
|
|||
WebLock.storages.lockKey(storageKey);
|
||||
|
||||
try {
|
||||
const nsecs = new Set(this.#dataSchema().parse(data));
|
||||
const nsecs = new Set(this.dataSchema().parse(data));
|
||||
|
||||
for (const nsec of nsecs) {
|
||||
const { data: secretKey } = nip19.decode(nsec);
|
||||
|
@ -34,8 +34,8 @@ export class NKeyStorage implements ReadonlyMap<string, NostrSigner> {
|
|||
}
|
||||
}
|
||||
|
||||
#dataSchema(): z.ZodType<`nsec1${string}`[]> {
|
||||
return n.json().pipe(n.bech32('nsec').array());
|
||||
private dataSchema(): z.ZodType<`nsec1${string}`[]> {
|
||||
return n.json().pipe(n.bech32('nsec').array()) as z.ZodType<`nsec1${string}`[]>;
|
||||
}
|
||||
|
||||
#syncStorage() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { NSet, NostrEvent, NostrFilter } from '@soapbox/nspec';
|
||||
import { NSet, NostrEvent, NostrFilter } from '@nostrify/nostrify';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
|
|
|
@ -190,6 +190,10 @@ const Preferences = () => {
|
|||
<ListItem label={<FormattedMessage id='preferences.fields.delete_modal_label' defaultMessage='Show confirmation dialog before deleting a post' />}>
|
||||
<SettingToggle settings={settings} settingPath={['deleteModal']} onChange={onToggleChange} />
|
||||
</ListItem>
|
||||
|
||||
<ListItem label={<FormattedMessage id='preferences.fields.missing_description_modal_label' defaultMessage='Show confirmation dialog before sending a post without media descriptions' />}>
|
||||
<SettingToggle settings={settings} settingPath={['missingDescriptionModal']} onChange={onToggleChange} />
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<List>
|
||||
|
|
|
@ -13,7 +13,7 @@ import PinnedHostsPicker from '../remote-timeline/components/pinned-hosts-picker
|
|||
import Timeline from '../ui/components/timeline';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.public', defaultMessage: 'Fediverse timeline' },
|
||||
title: { id: 'column.public', defaultMessage: 'Global timeline' },
|
||||
dismiss: { id: 'fediverse_tab.explanation_box.dismiss', defaultMessage: 'Don\'t show again' },
|
||||
});
|
||||
|
||||
|
|
|
@ -43,7 +43,9 @@ const LinkFooter: React.FC = (): JSX.Element => {
|
|||
{features.profileDirectory && (
|
||||
<FooterLink to='/directory'><FormattedMessage id='navigation_bar.profile_directory' defaultMessage='Profile directory' /></FooterLink>
|
||||
)}
|
||||
<FooterLink to='/blocks'><FormattedMessage id='navigation_bar.blocks' defaultMessage='Blocks' /></FooterLink>
|
||||
{features.blocks && (
|
||||
<FooterLink to='/blocks'><FormattedMessage id='navigation_bar.blocks' defaultMessage='Blocks' /></FooterLink>
|
||||
)}
|
||||
<FooterLink to='/mutes'><FormattedMessage id='navigation_bar.mutes' defaultMessage='Mutes' /></FooterLink>
|
||||
{(features.filters || features.filtersV2) && (
|
||||
<FooterLink to='/filters'><FormattedMessage id='navigation_bar.filters' defaultMessage='Filters' /></FooterLink>
|
||||
|
|
|
@ -142,6 +142,7 @@ import {
|
|||
Bech32Redirect,
|
||||
Relays,
|
||||
Rules,
|
||||
AdminNostrRelays,
|
||||
} from './util/async-components';
|
||||
import GlobalHotkeys from './util/global-hotkeys';
|
||||
import { WrappedRoute } from './util/react-router-helpers';
|
||||
|
@ -187,7 +188,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
|||
https://stackoverflow.com/a/68637108
|
||||
*/}
|
||||
{features.federating && <WrappedRoute path='/timeline/local' exact page={HomePage} component={CommunityTimeline} content={children} publicRoute />}
|
||||
{features.federating && <WrappedRoute path='/timeline/fediverse' exact page={HomePage} component={PublicTimeline} content={children} publicRoute />}
|
||||
{features.federating && <WrappedRoute path='/timeline/global' exact page={HomePage} component={PublicTimeline} content={children} publicRoute />}
|
||||
{features.federating && <WrappedRoute path='/timeline/:instance' exact page={RemoteInstancePage} component={RemoteTimeline} content={children} />}
|
||||
|
||||
{features.conversations && <WrappedRoute path='/conversations' page={DefaultPage} component={Conversations} content={children} />}
|
||||
|
@ -202,11 +203,11 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
|||
<Redirect from='/web/:path' to='/:path' />
|
||||
<Redirect from='/timelines/home' to='/' />
|
||||
<Redirect from='/timelines/public/local' to='/timeline/local' />
|
||||
<Redirect from='/timelines/public' to='/timeline/fediverse' />
|
||||
<Redirect from='/timelines/public' to='/timeline/global' />
|
||||
<Redirect from='/timelines/direct' to='/messages' />
|
||||
|
||||
{/* Pleroma FE web routes */}
|
||||
<Redirect from='/main/all' to='/timeline/fediverse' />
|
||||
<Redirect from='/main/all' to='/timeline/global' />
|
||||
<Redirect from='/main/public' to='/timeline/local' />
|
||||
<Redirect from='/main/friends' to='/' />
|
||||
<Redirect from='/tag/:id' to='/tags/:id' />
|
||||
|
@ -244,6 +245,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
|||
<Redirect from='/auth/mfa' to='/settings/mfa' />
|
||||
<Redirect from='/auth/password/new' to='/reset-password' />
|
||||
<Redirect from='/auth/password/edit' to={`/edit-password${search}`} />
|
||||
<Redirect from='/timeline/fediverse' to='/timeline/global' />
|
||||
|
||||
<WrappedRoute path='/tags/:id' publicRoute page={DefaultPage} component={HashtagTimeline} content={children} />
|
||||
|
||||
|
@ -266,7 +268,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
|||
{features.chats && <WrappedRoute path='/chats/:chatId' page={ChatsPage} component={ChatIndex} content={children} />}
|
||||
|
||||
<WrappedRoute path='/follow_requests' page={DefaultPage} component={FollowRequests} content={children} />
|
||||
<WrappedRoute path='/blocks' page={DefaultPage} component={Blocks} content={children} />
|
||||
{features.blocks && <WrappedRoute path='/blocks' page={DefaultPage} component={Blocks} content={children} />}
|
||||
{features.federating && <WrappedRoute path='/domain_blocks' page={DefaultPage} component={DomainBlocks} content={children} />}
|
||||
<WrappedRoute path='/mutes' page={DefaultPage} component={Mutes} content={children} />
|
||||
{(features.filters || features.filtersV2) && <WrappedRoute path='/filters/new' page={DefaultPage} component={EditFilter} content={children} />}
|
||||
|
@ -333,6 +335,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
|||
<WrappedRoute path='/soapbox/admin/users' staffOnly page={AdminPage} component={UserIndex} content={children} exact />
|
||||
<WrappedRoute path='/soapbox/admin/theme' staffOnly page={AdminPage} component={ThemeEditor} content={children} exact />
|
||||
<WrappedRoute path='/soapbox/admin/relays' staffOnly page={AdminPage} component={Relays} content={children} exact />
|
||||
{features.nostr && <WrappedRoute path='/soapbox/admin/nostr/relays' staffOnly page={AdminPage} component={AdminNostrRelays} content={children} exact />}
|
||||
{features.adminAnnouncements && <WrappedRoute path='/soapbox/admin/announcements' staffOnly page={AdminPage} component={Announcements} content={children} exact />}
|
||||
{features.domains && <WrappedRoute path='/soapbox/admin/domains' staffOnly page={AdminPage} component={Domains} content={children} exact />}
|
||||
{features.adminRules && <WrappedRoute path='/soapbox/admin/rules' staffOnly page={AdminPage} component={Rules} content={children} exact />}
|
||||
|
|
|
@ -175,3 +175,4 @@ export const Bech32Redirect = lazy(() => import('soapbox/features/nostr/Bech32Re
|
|||
export const Relays = lazy(() => import('soapbox/features/admin/relays'));
|
||||
export const Rules = lazy(() => import('soapbox/features/admin/rules'));
|
||||
export const EditRuleModal = lazy(() => import('soapbox/features/ui/components/modals/edit-rule-modal'));
|
||||
export const AdminNostrRelays = lazy(() => import('soapbox/features/admin/nostr-relays'));
|
||||
|
|
|
@ -338,6 +338,7 @@
|
|||
"column.admin.edit_domain": "Edit domain",
|
||||
"column.admin.edit_rule": "Edit rule",
|
||||
"column.admin.moderation_log": "Moderation Log",
|
||||
"column.admin.nostr_relays": "Relays",
|
||||
"column.admin.relays": "Instance relays",
|
||||
"column.admin.reports": "Reports",
|
||||
"column.admin.reports.menu.moderation_log": "Moderation Log",
|
||||
|
@ -423,7 +424,7 @@
|
|||
"column.notifications": "Notifications",
|
||||
"column.pins": "Pinned posts",
|
||||
"column.preferences": "Preferences",
|
||||
"column.public": "Fediverse timeline",
|
||||
"column.public": "Global timeline",
|
||||
"column.quotes": "Post quotes",
|
||||
"column.reactions": "Reactions",
|
||||
"column.reblogs": "Reposts",
|
||||
|
@ -1569,10 +1570,9 @@
|
|||
"sw.url": "Script URL",
|
||||
"tabs_bar.all": "All",
|
||||
"tabs_bar.dashboard": "Dashboard",
|
||||
"tabs_bar.fediverse": "Fediverse",
|
||||
"tabs_bar.global": "Global",
|
||||
"tabs_bar.groups": "Groups",
|
||||
"tabs_bar.home": "Home",
|
||||
"tabs_bar.local": "Local",
|
||||
"tabs_bar.more": "More",
|
||||
"tabs_bar.notifications": "Notifications",
|
||||
"tabs_bar.profile": "Profile",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { NSchema as n } from '@soapbox/nspec';
|
||||
import { NSchema as n } from '@nostrify/nostrify';
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
import z from 'zod';
|
||||
|
|
|
@ -1,35 +1,9 @@
|
|||
import { NSchema as n } from '@nostrify/nostrify';
|
||||
import { verifyEvent } from 'nostr-tools';
|
||||
import { z } from 'zod';
|
||||
|
||||
/** Schema to validate Nostr hex IDs such as event IDs and pubkeys. */
|
||||
const nostrIdSchema = z.string().regex(/^[0-9a-f]{64}$/);
|
||||
/** Nostr kinds are positive integers. */
|
||||
const kindSchema = z.number().int().nonnegative();
|
||||
|
||||
/** Nostr event template schema. */
|
||||
const eventTemplateSchema = z.object({
|
||||
kind: kindSchema,
|
||||
tags: z.array(z.array(z.string())),
|
||||
content: z.string(),
|
||||
created_at: z.number(),
|
||||
});
|
||||
|
||||
/** Nostr event schema. */
|
||||
const eventSchema = eventTemplateSchema.extend({
|
||||
id: nostrIdSchema,
|
||||
pubkey: nostrIdSchema,
|
||||
sig: z.string(),
|
||||
});
|
||||
|
||||
/** Nostr event schema that also verifies the event's signature. */
|
||||
const signedEventSchema = eventSchema.refine(verifyEvent);
|
||||
|
||||
/** NIP-46 signer request. */
|
||||
const connectRequestSchema = z.object({
|
||||
id: z.string(),
|
||||
method: z.literal('sign_event'),
|
||||
params: z.tuple([z.string()]),
|
||||
});
|
||||
const signedEventSchema = n.event().refine(verifyEvent);
|
||||
|
||||
/** NIP-47 signer response. */
|
||||
const nwcRequestSchema = z.object({
|
||||
|
@ -39,4 +13,4 @@ const nwcRequestSchema = z.object({
|
|||
}),
|
||||
});
|
||||
|
||||
export { nostrIdSchema, kindSchema, eventSchema, signedEventSchema, connectRequestSchema, nwcRequestSchema };
|
||||
export { signedEventSchema, nwcRequestSchema };
|
|
@ -1,4 +1,4 @@
|
|||
import type { NostrSigner } from '@soapbox/nspec';
|
||||
import type { NostrSigner } from '@nostrify/nostrify';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
|
@ -231,7 +231,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
* @see DELETE /api/v1/announcements/:id/reactions/:name
|
||||
* @see {@link https://docs.joinmastodon.org/methods/announcements/}
|
||||
*/
|
||||
announcementsReactions: true, // v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
|
||||
announcementsReactions: v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
|
||||
|
||||
/**
|
||||
* Pleroma backups.
|
||||
|
@ -251,6 +251,14 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
/** Whether people who blocked you are visible through the API. */
|
||||
blockersVisible: features.includes('blockers_visible'),
|
||||
|
||||
/**
|
||||
* Ability to block users.
|
||||
* @see POST /api/v1/accounts/:id/block
|
||||
* @see POST /api/v1/accounts/:id/unblock
|
||||
* @see GET /api/v1/blocks
|
||||
*/
|
||||
blocks: v.software !== DITTO,
|
||||
|
||||
/**
|
||||
* Can group bookmarks in folders.
|
||||
* @see GET /api/v1/pleroma/bookmark_folders
|
||||
|
|
107
yarn.lock
107
yarn.lock
|
@ -1877,12 +1877,12 @@
|
|||
dependencies:
|
||||
"@noble/hashes" "1.3.2"
|
||||
|
||||
"@noble/curves@~1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.3.0.tgz#01be46da4fd195822dab821e72f71bf4aeec635e"
|
||||
integrity sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==
|
||||
"@noble/curves@~1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.0.tgz#f05771ef64da724997f69ee1261b2417a49522d6"
|
||||
integrity sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==
|
||||
dependencies:
|
||||
"@noble/hashes" "1.3.3"
|
||||
"@noble/hashes" "1.4.0"
|
||||
|
||||
"@noble/hashes@1.3.1":
|
||||
version "1.3.1"
|
||||
|
@ -1894,7 +1894,12 @@
|
|||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
|
||||
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
|
||||
|
||||
"@noble/hashes@1.3.3", "@noble/hashes@^1.3.3", "@noble/hashes@~1.3.2":
|
||||
"@noble/hashes@1.4.0", "@noble/hashes@^1.4.0", "@noble/hashes@~1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426"
|
||||
integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==
|
||||
|
||||
"@noble/hashes@^1.3.3":
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699"
|
||||
integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==
|
||||
|
@ -1920,6 +1925,21 @@
|
|||
"@nodelib/fs.scandir" "2.1.5"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@nostrify/nostrify@npm:@jsr/nostrify__nostrify":
|
||||
version "0.17.1"
|
||||
resolved "https://npm.jsr.io/~/10/@jsr/nostrify__nostrify/0.17.1.tgz#130487F4CF4715F1036A687E7EA819D3ADEC9B6F"
|
||||
integrity sha512-+mUwudIJFSsnVD3+7PdFG+s27tOf9F1OROyRv7+cWoPWK9UnWlGFAQ50GIeDGYv9nd+gp8N7nu31r/cmqHmORw==
|
||||
dependencies:
|
||||
"@noble/hashes" "^1.4.0"
|
||||
"@scure/base" "^1.1.6"
|
||||
"@scure/bip32" "^1.4.0"
|
||||
"@scure/bip39" "^1.3.0"
|
||||
kysely "^0.27.3"
|
||||
lru-cache "^10.2.0"
|
||||
nostr-tools "^2.5.0"
|
||||
websocket-ts "^2.1.5"
|
||||
zod "^3.23.4"
|
||||
|
||||
"@popperjs/core@^2.11.5", "@popperjs/core@^2.9.2":
|
||||
version "2.11.5"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64"
|
||||
|
@ -2153,10 +2173,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
|
||||
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
|
||||
|
||||
"@scure/base@^1.1.5", "@scure/base@~1.1.4":
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.5.tgz#1d85d17269fe97694b9c592552dd9e5e33552157"
|
||||
integrity sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==
|
||||
"@scure/base@^1.1.6", "@scure/base@~1.1.6":
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d"
|
||||
integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==
|
||||
|
||||
"@scure/bip32@1.3.1":
|
||||
version "1.3.1"
|
||||
|
@ -2167,14 +2187,14 @@
|
|||
"@noble/hashes" "~1.3.1"
|
||||
"@scure/base" "~1.1.0"
|
||||
|
||||
"@scure/bip32@^1.3.3":
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.3.tgz#a9624991dc8767087c57999a5d79488f48eae6c8"
|
||||
integrity sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ==
|
||||
"@scure/bip32@^1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.4.0.tgz#4e1f1e196abedcef395b33b9674a042524e20d67"
|
||||
integrity sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==
|
||||
dependencies:
|
||||
"@noble/curves" "~1.3.0"
|
||||
"@noble/hashes" "~1.3.2"
|
||||
"@scure/base" "~1.1.4"
|
||||
"@noble/curves" "~1.4.0"
|
||||
"@noble/hashes" "~1.4.0"
|
||||
"@scure/base" "~1.1.6"
|
||||
|
||||
"@scure/bip39@1.2.1":
|
||||
version "1.2.1"
|
||||
|
@ -2184,13 +2204,13 @@
|
|||
"@noble/hashes" "~1.3.0"
|
||||
"@scure/base" "~1.1.0"
|
||||
|
||||
"@scure/bip39@^1.2.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.2.tgz#f3426813f4ced11a47489cbcf7294aa963966527"
|
||||
integrity sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==
|
||||
"@scure/bip39@^1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.3.0.tgz#0f258c16823ddd00739461ac31398b4e7d6a18c3"
|
||||
integrity sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==
|
||||
dependencies:
|
||||
"@noble/hashes" "~1.3.2"
|
||||
"@scure/base" "~1.1.4"
|
||||
"@noble/hashes" "~1.4.0"
|
||||
"@scure/base" "~1.1.6"
|
||||
|
||||
"@sentry-internal/tracing@7.74.1":
|
||||
version "7.74.1"
|
||||
|
@ -2276,20 +2296,6 @@
|
|||
raf "^3.4.0"
|
||||
responsive-gamepad "1.1.0"
|
||||
|
||||
"@soapbox/nspec@npm:@jsr/soapbox__nspec":
|
||||
version "0.6.0"
|
||||
resolved "https://npm.jsr.io/~/6/@jsr/soapbox__nspec/0.6.0.tgz#60A75BCDBEC1B76DFA91BEFDF5505CCB8ADDAD3B"
|
||||
integrity sha512-HY+MssBjm532J9SAqLek8YGxBlEaXdT1Eek3bOWkq4uLJxipJhYkdQrW+NzXhfVvGZUt6YXBobeSqRQx1JFgkQ==
|
||||
dependencies:
|
||||
"@noble/hashes" "^1.3.3"
|
||||
"@scure/base" "^1.1.5"
|
||||
"@scure/bip32" "^1.3.3"
|
||||
"@scure/bip39" "^1.2.2"
|
||||
lru-cache "^10.2.0"
|
||||
nostr-tools "^2.3.1"
|
||||
websocket-ts "^2.1.5"
|
||||
zod "^3.22.4"
|
||||
|
||||
"@soapbox/weblock@npm:@jsr/soapbox__weblock":
|
||||
version "0.1.0"
|
||||
resolved "https://npm.jsr.io/~/7/@jsr/soapbox__weblock/0.1.0.tgz#749AEE0872D23CC4E37366D5F0D092B87986C5E1"
|
||||
|
@ -6153,6 +6159,11 @@ known-css-properties@^0.29.0:
|
|||
resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.29.0.tgz#e8ba024fb03886f23cb882e806929f32d814158f"
|
||||
integrity sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==
|
||||
|
||||
kysely@^0.27.3:
|
||||
version "0.27.3"
|
||||
resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.27.3.tgz#6cc6c757040500b43c4ac596cdbb12be400ee276"
|
||||
integrity sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==
|
||||
|
||||
language-subtag-registry@~0.3.2:
|
||||
version "0.3.21"
|
||||
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a"
|
||||
|
@ -6396,9 +6407,9 @@ lowercase-keys@^2.0.0:
|
|||
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
|
||||
|
||||
lru-cache@^10.2.0:
|
||||
version "10.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3"
|
||||
integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==
|
||||
version "10.2.2"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878"
|
||||
integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==
|
||||
|
||||
lru-cache@^4.1.2:
|
||||
version "4.1.5"
|
||||
|
@ -6724,10 +6735,10 @@ nostr-tools@^2.3.0:
|
|||
optionalDependencies:
|
||||
nostr-wasm v0.1.0
|
||||
|
||||
nostr-tools@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.3.1.tgz#348d3c4aab0ab00716f93d6e2a72333d8c7da982"
|
||||
integrity sha512-qjKx2C3EzwiQOe2LPSPyCnp07pGz1pWaWjDXcm+L2y2c8iTECbvlzujDANm3nJUjWL5+LVRUVDovTZ1a/DC4Bg==
|
||||
nostr-tools@^2.5.0:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.5.1.tgz#614d6aaf5c21df6b239d7ed42fdf77616a4621e7"
|
||||
integrity sha512-bpkhGGAhdiCN0irfV+xoH3YP5CQeOXyXzUq7SYeM6D56xwTXZCPEmBlUGqFVfQidvRsoVeVxeAiOXW2c2HxoRQ==
|
||||
dependencies:
|
||||
"@noble/ciphers" "^0.5.1"
|
||||
"@noble/curves" "1.2.0"
|
||||
|
@ -9805,7 +9816,7 @@ zod@^3.21.0:
|
|||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.3.tgz#2fbc96118b174290d94e8896371c95629e87a060"
|
||||
integrity sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==
|
||||
|
||||
zod@^3.22.4:
|
||||
version "3.22.4"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"
|
||||
integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==
|
||||
zod@^3.23.4, zod@^3.23.5:
|
||||
version "3.23.5"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.5.tgz#c7b7617d017d4a2f21852f533258d26a9a5ae09f"
|
||||
integrity sha512-fkwiq0VIQTksNNA131rDOsVJcns0pfVUjHzLrNBiF/O/Xxb5lQyEXkhZWcJ7npWsYlvs+h0jFWXXy4X46Em1JA==
|
||||
|
|
Ładowanie…
Reference in New Issue