feat: multi-account system #514
@ -1,19 +1,17 @@
|
|||||||
import "./Zap.css";
|
import "./Zap.css";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import { HexKey, TaggedRawEvent } from "@snort/nostr";
|
import { HexKey, TaggedRawEvent } from "@snort/nostr";
|
||||||
|
|
||||||
import { decodeInvoice, InvoiceDetails, sha256, unwrap } from "Util";
|
import { decodeInvoice, InvoiceDetails, sha256, unwrap } from "Util";
|
||||||
import { formatShort } from "Number";
|
import { formatShort } from "Number";
|
||||||
import Text from "Element/Text";
|
import Text from "Element/Text";
|
||||||
import ProfileImage from "Element/ProfileImage";
|
import ProfileImage from "Element/ProfileImage";
|
||||||
import { RootState } from "State/Store";
|
|
||||||
import { findTag } from "Util";
|
import { findTag } from "Util";
|
||||||
import { UserCache } from "Cache/UserCache";
|
import { UserCache } from "Cache/UserCache";
|
||||||
|
import useLogin from "Hooks/useLogin";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
import useLogin from "Hooks/useLogin";
|
|
||||||
|
|
||||||
function getInvoice(zap: TaggedRawEvent): InvoiceDetails | undefined {
|
function getInvoice(zap: TaggedRawEvent): InvoiceDetails | undefined {
|
||||||
const bolt11 = findTag(zap, "bolt11");
|
const bolt11 = findTag(zap, "bolt11");
|
||||||
|
@ -20,7 +20,7 @@ import { SubscriptionEvent } from "Subscription";
|
|||||||
*/
|
*/
|
||||||
export default function useLoginFeed() {
|
export default function useLoginFeed() {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const { publicKey: pubKey, privateKey: privKey, readNotifications, muted: stateMuted } = login;
|
const { publicKey: pubKey, privateKey: privKey, readNotifications } = login;
|
||||||
const { isMuted } = useModeration();
|
const { isMuted } = useModeration();
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
|
|
||||||
|
@ -9,135 +9,135 @@ import { bech32ToHex, dedupeById, randomSample, sanitizeRelayUrl, unixNowMs } fr
|
|||||||
import { getCurrentSubscription, SubscriptionEvent } from "Subscription";
|
import { getCurrentSubscription, SubscriptionEvent } from "Subscription";
|
||||||
|
|
||||||
export function setRelays(state: LoginSession, relays: Record<string, RelaySettings>, createdAt: number) {
|
export function setRelays(state: LoginSession, relays: Record<string, RelaySettings>, createdAt: number) {
|
||||||
if (state.relays.timestamp > createdAt) {
|
if (state.relays.timestamp > createdAt) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter out non-websocket urls
|
// filter out non-websocket urls
|
||||||
const filtered = new Map<string, RelaySettings>();
|
const filtered = new Map<string, RelaySettings>();
|
||||||
for (const [k, v] of Object.entries(relays)) {
|
for (const [k, v] of Object.entries(relays)) {
|
||||||
if (k.startsWith("wss://") || k.startsWith("ws://")) {
|
if (k.startsWith("wss://") || k.startsWith("ws://")) {
|
||||||
const url = sanitizeRelayUrl(k);
|
const url = sanitizeRelayUrl(k);
|
||||||
if (url) {
|
if (url) {
|
||||||
filtered.set(url, v as RelaySettings);
|
filtered.set(url, v as RelaySettings);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
state.relays.item = Object.fromEntries(filtered.entries());
|
}
|
||||||
state.relays.timestamp = createdAt;
|
state.relays.item = Object.fromEntries(filtered.entries());
|
||||||
LoginStore.updateSession(state);
|
state.relays.timestamp = createdAt;
|
||||||
|
LoginStore.updateSession(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeRelay(state: LoginSession, addr: string) {
|
export function removeRelay(state: LoginSession, addr: string) {
|
||||||
delete state.relays.item[addr];
|
delete state.relays.item[addr];
|
||||||
LoginStore.updateSession(state);
|
LoginStore.updateSession(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updatePreferences(state: LoginSession, p: UserPreferences) {
|
export function updatePreferences(state: LoginSession, p: UserPreferences) {
|
||||||
state.preferences = p;
|
state.preferences = p;
|
||||||
LoginStore.updateSession(state);
|
LoginStore.updateSession(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logout(k: HexKey) {
|
export function logout(k: HexKey) {
|
||||||
LoginStore.removeSession(k);
|
LoginStore.removeSession(k);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markNotificationsRead(state: LoginSession) {
|
export function markNotificationsRead(state: LoginSession) {
|
||||||
state.readNotifications = unixNowMs();
|
state.readNotifications = unixNowMs();
|
||||||
LoginStore.updateSession(state);
|
LoginStore.updateSession(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearEntropy(state: LoginSession) {
|
export function clearEntropy(state: LoginSession) {
|
||||||
state.generatedEntropy = undefined;
|
state.generatedEntropy = undefined;
|
||||||
LoginStore.updateSession(state);
|
LoginStore.updateSession(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a new key and login with this generated key
|
* Generate a new key and login with this generated key
|
||||||
*/
|
*/
|
||||||
export async function generateNewLogin(publisher: EventPublisher) {
|
export async function generateNewLogin(publisher: EventPublisher) {
|
||||||
const ent = generateBip39Entropy();
|
const ent = generateBip39Entropy();
|
||||||
const entHex = secp.utils.bytesToHex(ent);
|
const entHex = secp.utils.bytesToHex(ent);
|
||||||
const newKeyHex = entropyToDerivedKey(ent);
|
const newKeyHex = entropyToDerivedKey(ent);
|
||||||
let newRelays: Record<string, RelaySettings> = {};
|
let newRelays: Record<string, RelaySettings> = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rsp = await fetch("https://api.nostr.watch/v1/online");
|
const rsp = await fetch("https://api.nostr.watch/v1/online");
|
||||||
if (rsp.ok) {
|
if (rsp.ok) {
|
||||||
const online: string[] = await rsp.json();
|
const online: string[] = await rsp.json();
|
||||||
const pickRandom = randomSample(online, 4);
|
const pickRandom = randomSample(online, 4);
|
||||||
const relayObjects = pickRandom.map(a => [a, { read: true, write: true }]);
|
const relayObjects = pickRandom.map(a => [a, { read: true, write: true }]);
|
||||||
newRelays = {
|
newRelays = {
|
||||||
...Object.fromEntries(relayObjects),
|
...Object.fromEntries(relayObjects),
|
||||||
...Object.fromEntries(DefaultRelays.entries()),
|
...Object.fromEntries(DefaultRelays.entries()),
|
||||||
};
|
};
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e);
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
|
||||||
const ev = await publisher.addFollow([bech32ToHex(SnortPubKey), newKeyHex], newRelays);
|
const ev = await publisher.addFollow([bech32ToHex(SnortPubKey), newKeyHex], newRelays);
|
||||||
publisher.broadcast(ev);
|
publisher.broadcast(ev);
|
||||||
|
|
||||||
LoginStore.loginWithPrivateKey(newKeyHex, entHex);
|
LoginStore.loginWithPrivateKey(newKeyHex, entHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setTags(state: LoginSession, tags: Array<string>, ts: number) {
|
export function setTags(state: LoginSession, tags: Array<string>, ts: number) {
|
||||||
if (state.tags.timestamp > ts) {
|
if (state.tags.timestamp > ts) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.tags.item = tags;
|
state.tags.item = tags;
|
||||||
state.tags.timestamp = ts;
|
state.tags.timestamp = ts;
|
||||||
LoginStore.updateSession(state);
|
LoginStore.updateSession(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setMuted(state: LoginSession, muted: Array<string>, ts: number) {
|
export function setMuted(state: LoginSession, muted: Array<string>, ts: number) {
|
||||||
if (state.muted.timestamp > ts) {
|
if (state.muted.timestamp > ts) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.muted.item = muted;
|
state.muted.item = muted;
|
||||||
state.muted.timestamp = ts;
|
state.muted.timestamp = ts;
|
||||||
LoginStore.updateSession(state);
|
LoginStore.updateSession(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setBlocked(state: LoginSession, blocked: Array<string>, ts: number) {
|
export function setBlocked(state: LoginSession, blocked: Array<string>, ts: number) {
|
||||||
if (state.blocked.timestamp > ts) {
|
if (state.blocked.timestamp > ts) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.blocked.item = blocked;
|
state.blocked.item = blocked;
|
||||||
state.blocked.timestamp = ts;
|
state.blocked.timestamp = ts;
|
||||||
LoginStore.updateSession(state);
|
LoginStore.updateSession(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setFollows(state: LoginSession, follows: Array<string>, ts: number) {
|
export function setFollows(state: LoginSession, follows: Array<string>, ts: number) {
|
||||||
if (state.follows.timestamp > ts) {
|
if (state.follows.timestamp > ts) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.follows.item = follows;
|
state.follows.item = follows;
|
||||||
state.follows.timestamp = ts;
|
state.follows.timestamp = ts;
|
||||||
LoginStore.updateSession(state);
|
LoginStore.updateSession(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setPinned(state: LoginSession, pinned: Array<string>, ts: number) {
|
export function setPinned(state: LoginSession, pinned: Array<string>, ts: number) {
|
||||||
if (state.pinned.timestamp > ts) {
|
if (state.pinned.timestamp > ts) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.pinned.item = pinned;
|
state.pinned.item = pinned;
|
||||||
state.pinned.timestamp = ts;
|
state.pinned.timestamp = ts;
|
||||||
LoginStore.updateSession(state);
|
LoginStore.updateSession(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setBookmarked(state: LoginSession, bookmarked: Array<string>, ts: number) {
|
export function setBookmarked(state: LoginSession, bookmarked: Array<string>, ts: number) {
|
||||||
if (state.bookmarked.timestamp > ts) {
|
if (state.bookmarked.timestamp > ts) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.bookmarked.item = bookmarked;
|
state.bookmarked.item = bookmarked;
|
||||||
state.bookmarked.timestamp = ts;
|
state.bookmarked.timestamp = ts;
|
||||||
LoginStore.updateSession(state);
|
LoginStore.updateSession(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addSubscription(state: LoginSession, ...subs: SubscriptionEvent[]) {
|
export function addSubscription(state: LoginSession, ...subs: SubscriptionEvent[]) {
|
||||||
state.subscriptions = dedupeById([...state.subscriptions, ...subs]);
|
state.subscriptions = dedupeById([...(state.subscriptions || []), ...subs]);
|
||||||
state.currentSubscription = getCurrentSubscription(state.subscriptions);
|
state.currentSubscription = getCurrentSubscription(state.subscriptions);
|
||||||
LoginStore.updateSession(state);
|
LoginStore.updateSession(state);
|
||||||
}
|
}
|
@ -1,18 +1,16 @@
|
|||||||
import "./Root.css";
|
import "./Root.css";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import { Link, Outlet, RouteObject, useLocation, useNavigate, useParams } from "react-router-dom";
|
import { Link, Outlet, RouteObject, useLocation, useNavigate, useParams } from "react-router-dom";
|
||||||
import { useIntl, FormattedMessage } from "react-intl";
|
import { useIntl, FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import Tabs, { Tab } from "Element/Tabs";
|
import Tabs, { Tab } from "Element/Tabs";
|
||||||
import { RootState } from "State/Store";
|
|
||||||
import Timeline from "Element/Timeline";
|
import Timeline from "Element/Timeline";
|
||||||
import { System } from "System";
|
import { System } from "System";
|
||||||
import { TimelineSubject } from "Feed/TimelineFeed";
|
import { TimelineSubject } from "Feed/TimelineFeed";
|
||||||
import { debounce, getRelayName, sha256, unixNow, unwrap } from "Util";
|
import { debounce, getRelayName, sha256, unixNow, unwrap } from "Util";
|
||||||
|
import useLogin from "Hooks/useLogin";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
import useLogin from "Hooks/useLogin";
|
|
||||||
|
|
||||||
interface RelayOption {
|
interface RelayOption {
|
||||||
url: string;
|
url: string;
|
||||||
|
@ -58,9 +58,10 @@ export function SubscribePage() {
|
|||||||
<h2>{mapPlanName(a.id)}</h2>
|
<h2>{mapPlanName(a.id)}</h2>
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Support Snort every month for {price} sats and receive the following rewards"
|
defaultMessage="Subscribe to Snort {plan} for {price} and receive the following rewards"
|
||||||
values={{
|
values={{
|
||||||
price: <b>{formatShort(a.price)}</b>,
|
plan: mapPlanName(a.id),
|
||||||
|
price: <b>{formatShort(a.price)} sats/mo</b>,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
@ -86,10 +87,7 @@ export function SubscribePage() {
|
|||||||
{a.disabled ? (
|
{a.disabled ? (
|
||||||
<FormattedMessage defaultMessage="Coming soon" />
|
<FormattedMessage defaultMessage="Coming soon" />
|
||||||
) : (
|
) : (
|
||||||
<FormattedMessage
|
<FormattedMessage defaultMessage="Subscribe" />
|
||||||
defaultMessage="Subscribe for {amount}/mo"
|
|
||||||
values={{ amount: formatShort(a.price) }}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,7 +18,7 @@ export const Plans = [
|
|||||||
{
|
{
|
||||||
id: SubscriptionType.Supporter,
|
id: SubscriptionType.Supporter,
|
||||||
price: 5_000,
|
price: 5_000,
|
||||||
disabled: true,
|
disabled: false,
|
||||||
unlocks: [LockedFeatures.MultiAccount, LockedFeatures.NostrAddress, LockedFeatures.Badge],
|
unlocks: [LockedFeatures.MultiAccount, LockedFeatures.NostrAddress, LockedFeatures.Badge],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user