readonly login sessions
This commit is contained in:
parent
3efb5321f6
commit
94da60ebfa
@ -16,7 +16,7 @@ interface BookmarksProps {
|
||||
|
||||
const Bookmarks = ({ pubkey, bookmarks, related }: BookmarksProps) => {
|
||||
const [onlyPubkey, setOnlyPubkey] = useState<HexKey | "all">("all");
|
||||
const loginPubKey = useLogin().publicKey;
|
||||
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
|
||||
const ps = useMemo(() => {
|
||||
return [...new Set(bookmarks.map(ev => ev.pubkey))];
|
||||
}, [bookmarks]);
|
||||
@ -47,7 +47,7 @@ const Bookmarks = ({ pubkey, bookmarks, related }: BookmarksProps) => {
|
||||
key={n.id}
|
||||
data={n}
|
||||
related={related}
|
||||
options={{ showTime: false, showBookmarked: true, canUnbookmark: loginPubKey === pubkey }}
|
||||
options={{ showTime: false, showBookmarked: true, canUnbookmark: publicKey === pubkey }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -17,8 +17,8 @@ interface Token {
|
||||
}
|
||||
|
||||
export default function CashuNuts({ token }: { token: string }) {
|
||||
const login = useLogin();
|
||||
const profile = useUserProfile(login.publicKey);
|
||||
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
|
||||
const profile = useUserProfile(publicKey);
|
||||
|
||||
async function copyToken(e: React.MouseEvent<HTMLButtonElement>, token: string) {
|
||||
e.stopPropagation();
|
||||
|
@ -18,14 +18,14 @@ export interface DMProps {
|
||||
}
|
||||
|
||||
export default function DM(props: DMProps) {
|
||||
const pubKey = useLogin().publicKey;
|
||||
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
|
||||
const publisher = useEventPublisher();
|
||||
const msg = props.data;
|
||||
const [content, setContent] = useState<string>();
|
||||
const { ref, inView } = useInView({ triggerOnce: true });
|
||||
const { formatMessage } = useIntl();
|
||||
const isMe = msg.from === pubKey;
|
||||
const otherPubkey = isMe ? pubKey : msg.from;
|
||||
const isMe = msg.from === publicKey;
|
||||
const otherPubkey = isMe ? publicKey : msg.from;
|
||||
|
||||
async function decrypt() {
|
||||
if (publisher) {
|
||||
|
@ -10,12 +10,12 @@ import { Chat, ChatParticipant, createEmptyChatObject, useChatSystem } from "cha
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
export default function DmWindow({ id }: { id: string }) {
|
||||
const pubKey = useLogin().publicKey;
|
||||
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
|
||||
const dms = useChatSystem();
|
||||
const chat = dms.find(a => a.id === id) ?? createEmptyChatObject(id);
|
||||
|
||||
function participant(p: ChatParticipant) {
|
||||
if (p.id === pubKey) {
|
||||
if (p.id === publicKey) {
|
||||
return <NoteToSelf className="f-grow mb-10" pubkey={p.id} />;
|
||||
}
|
||||
if (p.type === "pubkey") {
|
||||
@ -56,7 +56,7 @@ export default function DmWindow({ id }: { id: string }) {
|
||||
}
|
||||
|
||||
function DmChatSelected({ chat }: { chat: Chat }) {
|
||||
const { publicKey: myPubKey } = useLogin();
|
||||
const { publicKey: myPubKey } = useLogin(s => ({ publicKey: s.publicKey }));
|
||||
const sortedDms = useMemo(() => {
|
||||
const myDms = chat?.messages;
|
||||
if (myPubKey && myDms) {
|
||||
|
@ -18,9 +18,9 @@ export interface FollowButtonProps {
|
||||
export default function FollowButton(props: FollowButtonProps) {
|
||||
const pubkey = parseId(props.pubkey);
|
||||
const publisher = useEventPublisher();
|
||||
const { follows, relays } = useLogin();
|
||||
const { follows, relays, readonly } = useLogin(s => ({ follows: s.follows, relays: s.relays, readonly: s.readonly }));
|
||||
const isFollowing = follows.item.includes(pubkey);
|
||||
const baseClassname = `${props.className} follow-button`;
|
||||
const baseClassname = `${props.className ? ` ${props.className}` : ""}follow-button`;
|
||||
|
||||
async function follow(pubkey: HexKey) {
|
||||
if (publisher) {
|
||||
@ -43,6 +43,7 @@ export default function FollowButton(props: FollowButtonProps) {
|
||||
return (
|
||||
<AsyncButton
|
||||
className={isFollowing ? `${baseClassname} secondary` : baseClassname}
|
||||
disabled={readonly}
|
||||
onClick={() => (isFollowing ? unfollow(pubkey) : follow(pubkey))}>
|
||||
{isFollowing ? <FormattedMessage {...messages.Unfollow} /> : <FormattedMessage {...messages.Follow} />}
|
||||
</AsyncButton>
|
||||
|
@ -51,7 +51,7 @@ export default function FollowListBase({
|
||||
<div className="flex mt10 mb10">
|
||||
<div className="f-grow bold">{title}</div>
|
||||
{actions}
|
||||
<AsyncButton className="transparent" type="button" onClick={() => followAll()}>
|
||||
<AsyncButton className="transparent" type="button" onClick={() => followAll()} disabled={login.readonly}>
|
||||
<FormattedMessage {...messages.FollowAll} />
|
||||
</AsyncButton>
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@ import Icon from "Icons/Icon";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
export function FollowingMark({ pubkey }: { pubkey: string }) {
|
||||
const { follows } = useLogin();
|
||||
const { follows } = useLogin(s => ({ follows: s.follows }));
|
||||
const doesFollow = follows.item.includes(pubkey);
|
||||
if (!doesFollow) return;
|
||||
|
||||
|
@ -7,7 +7,7 @@ import messages from "./messages";
|
||||
|
||||
export default function LogoutButton() {
|
||||
const navigate = useNavigate();
|
||||
const login = useLogin();
|
||||
const login = useLogin(s => ({ publicKey: s.publicKey, id: s.id }));
|
||||
|
||||
if (!login.publicKey) return;
|
||||
return (
|
||||
|
@ -4,8 +4,8 @@ import useLogin from "Hooks/useLogin";
|
||||
const MixCloudEmbed = ({ link }: { link: string }) => {
|
||||
const feedPath = (MixCloudRegex.test(link) && RegExp.$1) + "%2F" + (MixCloudRegex.test(link) && RegExp.$2);
|
||||
|
||||
const lightTheme = useLogin().preferences.theme === "light";
|
||||
const lightParams = lightTheme ? "light=1" : "light=0";
|
||||
const { theme } = useLogin(s => ({ theme: s.preferences.theme }));
|
||||
const lightParams = theme === "light" ? "light=1" : "light=0";
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
|
@ -43,8 +43,8 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
const navigate = useNavigate();
|
||||
const { helpText = true } = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const pubkey = useLogin().publicKey;
|
||||
const user = useUserProfile(pubkey);
|
||||
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
|
||||
const user = useUserProfile(publicKey);
|
||||
const publisher = useEventPublisher();
|
||||
const svc = useMemo(() => new ServiceProvider(props.service), [props.service]);
|
||||
const [serviceConfig, setServiceConfig] = useState<ServiceConfig>();
|
||||
@ -179,11 +179,11 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
}
|
||||
|
||||
async function startBuy(handle: string, domain: string) {
|
||||
if (!pubkey) {
|
||||
if (!publicKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rsp = await svc.RegisterHandle(handle, domain, pubkey);
|
||||
const rsp = await svc.RegisterHandle(handle, domain, publicKey);
|
||||
if ("error" in rsp) {
|
||||
setError(rsp);
|
||||
} else {
|
||||
@ -193,7 +193,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
}
|
||||
|
||||
async function claimForSubscription(handle: string, domain: string, sub: string) {
|
||||
if (!pubkey || !publisher) {
|
||||
if (!publicKey || !publisher) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,6 @@ interface NosteContextMenuProps {
|
||||
export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const login = useLogin();
|
||||
const { pinned, bookmarked, publicKey, preferences: prefs } = login;
|
||||
const { mute, block } = useModeration();
|
||||
const publisher = useEventPublisher();
|
||||
const [showBroadcast, setShowBroadcast] = useState(false);
|
||||
@ -37,7 +36,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
|
||||
const langNames = new Intl.DisplayNames([...window.navigator.languages], {
|
||||
type: "language",
|
||||
});
|
||||
const isMine = ev.pubkey === publicKey;
|
||||
const isMine = ev.pubkey === login.publicKey;
|
||||
|
||||
async function deleteEvent() {
|
||||
if (window.confirm(formatMessage(messages.ConfirmDeletion, { id: ev.id.substring(0, 8) })) && publisher) {
|
||||
@ -89,7 +88,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
|
||||
|
||||
async function pin(id: HexKey) {
|
||||
if (publisher) {
|
||||
const es = [...pinned.item, id];
|
||||
const es = [...login.pinned.item, id];
|
||||
const ev = await publisher.noteList(es, Lists.Pinned);
|
||||
System.BroadcastEvent(ev);
|
||||
setPinned(login, es, ev.created_at * 1000);
|
||||
@ -98,7 +97,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
|
||||
|
||||
async function bookmark(id: HexKey) {
|
||||
if (publisher) {
|
||||
const es = [...bookmarked.item, id];
|
||||
const es = [...login.bookmarked.item, id];
|
||||
const ev = await publisher.noteList(es, Lists.Bookmarked);
|
||||
System.BroadcastEvent(ev);
|
||||
setBookmarked(login, es, ev.created_at * 1000);
|
||||
@ -131,13 +130,13 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
|
||||
<Icon name="share" />
|
||||
<FormattedMessage {...messages.Share} />
|
||||
</MenuItem>
|
||||
{!pinned.item.includes(ev.id) && (
|
||||
{!login.pinned.item.includes(ev.id) && !login.readonly && (
|
||||
<MenuItem onClick={() => pin(ev.id)}>
|
||||
<Icon name="pin" />
|
||||
<FormattedMessage {...messages.Pin} />
|
||||
</MenuItem>
|
||||
)}
|
||||
{!bookmarked.item.includes(ev.id) && (
|
||||
{!login.bookmarked.item.includes(ev.id) && !login.readonly && (
|
||||
<MenuItem onClick={() => bookmark(ev.id)}>
|
||||
<Icon name="bookmark" />
|
||||
<FormattedMessage {...messages.Bookmark} />
|
||||
@ -147,23 +146,23 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
|
||||
<Icon name="copy" />
|
||||
<FormattedMessage {...messages.CopyID} />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => mute(ev.pubkey)}>
|
||||
<Icon name="mute" />
|
||||
<FormattedMessage {...messages.Mute} />
|
||||
</MenuItem>
|
||||
{prefs.enableReactions && (
|
||||
{!login.readonly && (
|
||||
<MenuItem onClick={() => mute(ev.pubkey)}>
|
||||
<Icon name="mute" />
|
||||
<FormattedMessage {...messages.Mute} />
|
||||
</MenuItem>
|
||||
)}
|
||||
{login.preferences.enableReactions && !login.readonly && (
|
||||
<MenuItem onClick={() => props.react("-")}>
|
||||
<Icon name="dislike" />
|
||||
<FormattedMessage {...messages.DislikeAction} />
|
||||
</MenuItem>
|
||||
)}
|
||||
{ev.pubkey === publicKey && (
|
||||
<MenuItem onClick={handleReBroadcastButtonClick}>
|
||||
<Icon name="relay" />
|
||||
<FormattedMessage {...messages.ReBroadcast} />
|
||||
</MenuItem>
|
||||
)}
|
||||
{ev.pubkey !== publicKey && (
|
||||
<MenuItem onClick={handleReBroadcastButtonClick}>
|
||||
<Icon name="relay" />
|
||||
<FormattedMessage defaultMessage="Broadcast Event" />
|
||||
</MenuItem>
|
||||
{ev.pubkey !== login.publicKey && !login.readonly && (
|
||||
<MenuItem onClick={() => block(ev.pubkey)}>
|
||||
<Icon name="block" />
|
||||
<FormattedMessage {...messages.Block} />
|
||||
@ -173,13 +172,13 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
|
||||
<Icon name="translate" />
|
||||
<FormattedMessage {...messages.TranslateTo} values={{ lang: langNames.of(lang.split("-")[0]) }} />
|
||||
</MenuItem>
|
||||
{prefs.showDebugMenus && (
|
||||
{login.preferences.showDebugMenus && (
|
||||
<MenuItem onClick={() => copyEvent()}>
|
||||
<Icon name="json" />
|
||||
<FormattedMessage {...messages.CopyJSON} />
|
||||
</MenuItem>
|
||||
)}
|
||||
{isMine && (
|
||||
{isMine && !login.readonly && (
|
||||
<MenuItem onClick={() => deleteEvent()}>
|
||||
<Icon name="trash" className="red" />
|
||||
<FormattedMessage {...messages.Delete} />
|
||||
|
@ -32,7 +32,7 @@ export function NoteCreator() {
|
||||
const { formatMessage } = useIntl();
|
||||
const publisher = useEventPublisher();
|
||||
const uploader = useFileUpload();
|
||||
const login = useLogin();
|
||||
const login = useLogin(s => ({ relays: s.relays, publicKey: s.publicKey }));
|
||||
const note = useNoteCreator();
|
||||
const relays = login.relays;
|
||||
|
||||
|
@ -47,8 +47,11 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
const { ev, positive, reposts, zaps } = props;
|
||||
const system = useContext(SnortContext);
|
||||
const { formatMessage } = useIntl();
|
||||
const login = useLogin();
|
||||
const { publicKey, preferences: prefs } = login;
|
||||
const {
|
||||
publicKey,
|
||||
preferences: prefs,
|
||||
readonly,
|
||||
} = useLogin(s => ({ preferences: s.preferences, publicKey: s.publicKey, readonly: s.readonly }));
|
||||
const author = useUserProfile(ev.pubkey);
|
||||
const interactionCache = useInteractionCache(publicKey, ev.id);
|
||||
const publisher = useEventPublisher();
|
||||
@ -59,6 +62,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
const walletState = useWallet();
|
||||
const wallet = walletState.wallet;
|
||||
|
||||
const canFastZap = wallet?.isReady() && !readonly;
|
||||
const isMine = ev.pubkey === publicKey;
|
||||
const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
||||
const didZap = interactionCache.data.zapped || zaps.some(a => a.sender === publicKey);
|
||||
@ -127,7 +131,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
if (zapping || e?.isPropagationStopped()) return;
|
||||
|
||||
const lnurl = getZapTarget();
|
||||
if (wallet?.isReady() && lnurl) {
|
||||
if (canFastZap && lnurl) {
|
||||
setZapping(true);
|
||||
try {
|
||||
await fastZapInner(lnurl, prefs.defaultZapAmount);
|
||||
@ -194,7 +198,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
className={didZap ? "reacted" : ""}
|
||||
{...longPress()}
|
||||
title={formatMessage({ defaultMessage: "Zap" })}
|
||||
iconName={wallet?.isReady() ? "zapFast" : "zap"}
|
||||
iconName={canFastZap ? "zapFast" : "zap"}
|
||||
value={zapTotal}
|
||||
onClick={e => fastZap(e)}
|
||||
/>
|
||||
@ -204,13 +208,17 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
}
|
||||
|
||||
function repostIcon() {
|
||||
if (readonly) return;
|
||||
return (
|
||||
<AsyncFooterIcon
|
||||
className={hasReposted() ? "reacted" : ""}
|
||||
iconName="repeat"
|
||||
title={formatMessage({ defaultMessage: "Repost" })}
|
||||
value={reposts.length}
|
||||
onClick={() => repost()}
|
||||
onClick={async () => {
|
||||
if (readonly) return;
|
||||
await repost();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -226,12 +234,16 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
iconName={reacted ? "heart-solid" : "heart"}
|
||||
title={formatMessage({ defaultMessage: "Like" })}
|
||||
value={positive.length}
|
||||
onClick={() => react(prefs.reactionEmoji)}
|
||||
onClick={async () => {
|
||||
if (readonly) return;
|
||||
await react(prefs.reactionEmoji);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function replyIcon() {
|
||||
if (readonly) return;
|
||||
return (
|
||||
<AsyncFooterIcon
|
||||
className={note.show ? "reacted" : ""}
|
||||
|
@ -96,6 +96,7 @@ export function LoginUnlock() {
|
||||
LoginStore.setPublisher(login.id, pub);
|
||||
LoginStore.updateSession({
|
||||
...login,
|
||||
readonly: false,
|
||||
privateKeyData: newPin,
|
||||
privateKey: undefined,
|
||||
});
|
||||
@ -112,12 +113,20 @@ export function LoginUnlock() {
|
||||
LoginStore.setPublisher(login.id, pub);
|
||||
LoginStore.updateSession({
|
||||
...login,
|
||||
readonly: false,
|
||||
privateKeyData: key,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (login.publicKey && !publisher && sessionNeedsPin(login)) {
|
||||
function makeSessionReadonly() {
|
||||
LoginStore.updateSession({
|
||||
...login,
|
||||
readonly: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (login.publicKey && !publisher && sessionNeedsPin(login) && !login.readonly) {
|
||||
if (login.privateKey !== undefined) {
|
||||
return (
|
||||
<PinPrompt
|
||||
@ -142,7 +151,7 @@ export function LoginUnlock() {
|
||||
}
|
||||
onResult={unlockSession}
|
||||
onCancel={() => {
|
||||
//nothing
|
||||
makeSessionReadonly();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -1,45 +1,34 @@
|
||||
import { useState } from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { TaggedNostrEvent } from "@snort/system";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import Modal from "Element/Modal";
|
||||
import messages from "./messages";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { System } from "index";
|
||||
import AsyncButton from "./AsyncButton";
|
||||
|
||||
export function ReBroadcaster({ onClose, ev }: { onClose: () => void; ev: TaggedNostrEvent }) {
|
||||
const [selected, setSelected] = useState<Array<string>>();
|
||||
const publisher = useEventPublisher();
|
||||
const system = useContext(SnortContext);
|
||||
const { relays } = useLogin(s => ({ relays: s.relays }));
|
||||
|
||||
async function sendReBroadcast() {
|
||||
if (publisher) {
|
||||
if (selected) {
|
||||
await Promise.all(selected.map(r => System.WriteOnceToRelay(r, ev)));
|
||||
} else {
|
||||
System.BroadcastEvent(ev);
|
||||
}
|
||||
if (selected) {
|
||||
await Promise.all(selected.map(r => system.WriteOnceToRelay(r, ev)));
|
||||
} else {
|
||||
system.BroadcastEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) {
|
||||
ev.stopPropagation();
|
||||
sendReBroadcast().catch(console.warn);
|
||||
}
|
||||
|
||||
const login = useLogin();
|
||||
const relays = login.relays;
|
||||
|
||||
function renderRelayCustomisation() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex-column g8">
|
||||
{Object.keys(relays.item || {})
|
||||
.filter(el => relays.item[el].write)
|
||||
.map((r, i, a) => (
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>{r}</div>
|
||||
</div>
|
||||
<div className="card flex f-space">
|
||||
<div>{r}</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -63,13 +52,13 @@ export function ReBroadcaster({ onClose, ev }: { onClose: () => void; ev: Tagged
|
||||
<>
|
||||
<Modal id="broadcaster" className="note-creator-modal" onClose={onClose}>
|
||||
{renderRelayCustomisation()}
|
||||
<div className="note-creator-actions">
|
||||
<div className="flex g8">
|
||||
<button className="secondary" onClick={onClose}>
|
||||
<FormattedMessage {...messages.Cancel} />
|
||||
</button>
|
||||
<button onClick={onSubmit}>
|
||||
<AsyncButton onClick={sendReBroadcast}>
|
||||
<FormattedMessage {...messages.ReBroadcast} />
|
||||
</button>
|
||||
</AsyncButton>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
|
@ -248,9 +248,11 @@ function SendSatsInput(props: {
|
||||
onChange?: (v: SendSatsInputSelection) => void;
|
||||
onNextStage: (v: SendSatsInputSelection) => Promise<void>;
|
||||
}) {
|
||||
const login = useLogin();
|
||||
const { defaultZapAmount, readonly } = useLogin(s => ({
|
||||
defaultZapAmount: s.preferences.defaultZapAmount,
|
||||
readonly: s.readonly,
|
||||
}));
|
||||
const { formatMessage } = useIntl();
|
||||
const defaultZapAmount = login.preferences.defaultZapAmount;
|
||||
const amounts: Record<string, string> = {
|
||||
[defaultZapAmount.toString()]: "",
|
||||
"1000": "👍",
|
||||
@ -264,7 +266,7 @@ function SendSatsInput(props: {
|
||||
const [comment, setComment] = useState<string>();
|
||||
const [amount, setAmount] = useState<number>(defaultZapAmount);
|
||||
const [customAmount, setCustomAmount] = useState<number>(defaultZapAmount);
|
||||
const [zapType, setZapType] = useState(ZapType.PublicZap);
|
||||
const [zapType, setZapType] = useState(readonly ? ZapType.AnonZap : ZapType.PublicZap);
|
||||
|
||||
function getValue() {
|
||||
return {
|
||||
@ -358,6 +360,7 @@ function SendSatsInput(props: {
|
||||
}
|
||||
|
||||
function SendSatsZapTypeSelector({ zapType, setZapType }: { zapType: ZapType; setZapType: (t: ZapType) => void }) {
|
||||
const { readonly } = useLogin(s => ({ readonly: s.readonly }));
|
||||
const makeTab = (t: ZapType, n: React.ReactNode) => (
|
||||
<button type="button" className={zapType === t ? "" : "secondary"} onClick={() => setZapType(t)}>
|
||||
{n}
|
||||
@ -369,7 +372,7 @@ function SendSatsZapTypeSelector({ zapType, setZapType }: { zapType: ZapType; se
|
||||
<FormattedMessage defaultMessage="Zap Type" />
|
||||
</h3>
|
||||
<div className="flex g8">
|
||||
{makeTab(ZapType.PublicZap, <FormattedMessage defaultMessage="Public" description="Public Zap" />)}
|
||||
{!readonly && makeTab(ZapType.PublicZap, <FormattedMessage defaultMessage="Public" description="Public Zap" />)}
|
||||
{/*makeTab(ZapType.PrivateZap, "Private")*/}
|
||||
{makeTab(ZapType.AnonZap, <FormattedMessage defaultMessage="Anon" description="Anonymous Zap" />)}
|
||||
{makeTab(
|
||||
|
@ -1,9 +1,19 @@
|
||||
import { LoginStore } from "Login";
|
||||
import { LoginSession, LoginStore } from "Login";
|
||||
import { useSyncExternalStore } from "react";
|
||||
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector";
|
||||
|
||||
export default function useLogin() {
|
||||
return useSyncExternalStore(
|
||||
s => LoginStore.hook(s),
|
||||
() => LoginStore.snapshot(),
|
||||
);
|
||||
export default function useLogin<T extends object = LoginSession>(selector?: (v: LoginSession) => T) {
|
||||
if (selector) {
|
||||
return useSyncExternalStoreWithSelector(
|
||||
s => LoginStore.hook(s),
|
||||
() => LoginStore.snapshot(),
|
||||
undefined,
|
||||
selector,
|
||||
);
|
||||
} else {
|
||||
return useSyncExternalStore<T>(
|
||||
s => LoginStore.hook(s),
|
||||
() => LoginStore.snapshot() as T,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ const getMessages = (locale: string) => {
|
||||
};
|
||||
|
||||
export const IntlProvider = ({ children }: { children: ReactNode }) => {
|
||||
const { language } = useLogin().preferences;
|
||||
const { language } = useLogin(s => ({ language: s.preferences.language }));
|
||||
const locale = language ?? getLocale();
|
||||
const [messages, setMessages] = useState<Record<string, string>>(enMessages);
|
||||
|
||||
|
@ -188,10 +188,8 @@ export function createPublisher(l: LoginSession, pin?: PinEncrypted) {
|
||||
case LoginSessionType.Nip7os: {
|
||||
return new EventPublisher(new Nip7OsSigner(), unwrap(l.publicKey));
|
||||
}
|
||||
default: {
|
||||
if (l.publicKey) {
|
||||
return new EventPublisher(new Nip7Signer(), l.publicKey);
|
||||
}
|
||||
case LoginSessionType.Nip7: {
|
||||
return new EventPublisher(new Nip7Signer(), unwrap(l.publicKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,11 @@ export interface LoginSession {
|
||||
*/
|
||||
privateKey?: HexKey;
|
||||
|
||||
/**
|
||||
* If this session cannot sign events
|
||||
*/
|
||||
readonly: boolean;
|
||||
|
||||
/**
|
||||
* Encrypted private key
|
||||
*/
|
||||
|
@ -14,6 +14,7 @@ const LoggedOut = {
|
||||
id: "default",
|
||||
type: "public_key",
|
||||
preferences: DefaultPreferences,
|
||||
readonly: true,
|
||||
tags: {
|
||||
item: [],
|
||||
timestamp: 0,
|
||||
@ -65,6 +66,12 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
if (!this.#activeAccount) {
|
||||
this.#activeAccount = this.#accounts.keys().next().value;
|
||||
}
|
||||
// reset readonly on load
|
||||
for (const [, v] of this.#accounts) {
|
||||
if (v.type === LoginSessionType.PrivateKey && v.readonly) {
|
||||
v.readonly = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getSessions() {
|
||||
@ -108,6 +115,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
const newSession = {
|
||||
...LoggedOut,
|
||||
id: uuid(),
|
||||
readonly: type === LoginSessionType.PublicKey,
|
||||
type,
|
||||
publicKey: key,
|
||||
relays: {
|
||||
@ -146,6 +154,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
...LoggedOut,
|
||||
id: uuid(),
|
||||
type: LoginSessionType.PrivateKey,
|
||||
readonly: false,
|
||||
privateKeyData: key,
|
||||
publicKey: pubKey,
|
||||
generatedEntropy: entropy,
|
||||
@ -221,8 +230,21 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
}
|
||||
}
|
||||
|
||||
// mark readonly
|
||||
for (const [, v] of this.#accounts) {
|
||||
if (v.type === LoginSessionType.PublicKey && !v.readonly) {
|
||||
v.readonly = true;
|
||||
didMigrate = true;
|
||||
}
|
||||
// reset readonly on load
|
||||
if (v.type === LoginSessionType.PrivateKey && v.readonly) {
|
||||
v.readonly = false;
|
||||
didMigrate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (didMigrate) {
|
||||
console.debug("Finished migration to MultiAccountStore");
|
||||
console.debug("Finished migration in MultiAccountStore");
|
||||
this.#save();
|
||||
}
|
||||
}
|
||||
|
@ -66,13 +66,14 @@ export default function Layout() {
|
||||
|
||||
const NoteCreatorButton = () => {
|
||||
const location = useLocation();
|
||||
const { readonly } = useLogin(s => ({ readonly: s.readonly }));
|
||||
const { show, replyTo, update } = useNoteCreator(v => ({ show: v.show, replyTo: v.replyTo, update: v.update }));
|
||||
|
||||
const shouldHideNoteCreator = useMemo(() => {
|
||||
const isReplyNoteCreatorShowing = replyTo && show;
|
||||
const hideOn = ["/settings", "/messages", "/new", "/login", "/donate", "/e", "/subscribe"];
|
||||
return isReplyNoteCreatorShowing || hideOn.some(a => location.pathname.startsWith(a));
|
||||
}, [location]);
|
||||
return readonly || isReplyNoteCreatorShowing || hideOn.some(a => location.pathname.startsWith(a));
|
||||
}, [location, readonly]);
|
||||
|
||||
if (shouldHideNoteCreator) return;
|
||||
return (
|
||||
@ -96,7 +97,12 @@ const AccountHeader = () => {
|
||||
const navigate = useNavigate();
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const { publicKey, latestNotification, readNotifications } = useLogin();
|
||||
const { publicKey, latestNotification, readNotifications, readonly } = useLogin(s => ({
|
||||
publicKey: s.publicKey,
|
||||
latestNotification: s.latestNotification,
|
||||
readNotifications: s.readNotifications,
|
||||
readonly: s.readonly,
|
||||
}));
|
||||
const profile = useUserProfile(publicKey);
|
||||
const [search, setSearch] = useState("");
|
||||
const [searching, setSearching] = useState(false);
|
||||
@ -174,10 +180,12 @@ const AccountHeader = () => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Link className="btn" to="/messages">
|
||||
<Icon name="mail" size={24} />
|
||||
{unreadDms > 0 && <span className="has-unread"></span>}
|
||||
</Link>
|
||||
{!readonly && (
|
||||
<Link className="btn" to="/messages">
|
||||
<Icon name="mail" size={24} />
|
||||
{unreadDms > 0 && <span className="has-unread"></span>}
|
||||
</Link>
|
||||
)}
|
||||
<Link className="btn" to="/notifications" onClick={goToNotifications}>
|
||||
<Icon name="bell-02" size={24} />
|
||||
{hasNotifications && <span className="has-unread"></span>}
|
||||
|
@ -97,6 +97,7 @@ export default function LoginPage() {
|
||||
async function doLogin(pin?: string) {
|
||||
try {
|
||||
await loginHandler.doLogin(key, pin);
|
||||
navigate("/");
|
||||
} catch (e) {
|
||||
if (e instanceof PinRequiredError) {
|
||||
setPin(true);
|
||||
|
@ -466,7 +466,7 @@ export default function ProfilePage() {
|
||||
<Icon name="zap" size={16} />
|
||||
</IconButton>
|
||||
)}
|
||||
{loginPubKey && (
|
||||
{loginPubKey && !login.readonly && (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
|
@ -22,7 +22,7 @@ export interface ProfileSettingsProps {
|
||||
|
||||
export default function ProfileSettings(props: ProfileSettingsProps) {
|
||||
const navigate = useNavigate();
|
||||
const { publicKey: id } = useLogin();
|
||||
const { publicKey: id, readonly } = useLogin(s => ({ publicKey: s.publicKey, readonly: s.readonly }));
|
||||
const user = useUserProfile(id ?? "");
|
||||
const publisher = useEventPublisher();
|
||||
const uploader = useFileUpload();
|
||||
@ -113,26 +113,48 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Name" />
|
||||
</h4>
|
||||
<input className="w-max" type="text" value={name} onChange={e => setName(e.target.value)} />
|
||||
<input
|
||||
className="w-max"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex f-col w-max g8">
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="About" />
|
||||
</h4>
|
||||
<textarea className="w-max" onChange={e => setAbout(e.target.value)} value={about}></textarea>
|
||||
<textarea
|
||||
className="w-max"
|
||||
onChange={e => setAbout(e.target.value)}
|
||||
value={about}
|
||||
disabled={readonly}></textarea>
|
||||
</div>
|
||||
<div className="flex f-col w-max g8">
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Website" />
|
||||
</h4>
|
||||
<input className="w-max" type="text" value={website} onChange={e => setWebsite(e.target.value)} />
|
||||
<input
|
||||
className="w-max"
|
||||
type="text"
|
||||
value={website}
|
||||
onChange={e => setWebsite(e.target.value)}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex f-col w-max g8">
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Nostr Address" />
|
||||
</h4>
|
||||
<div className="flex f-col g8 w-max">
|
||||
<input type="text" className="w-max" value={nip05} onChange={e => setNip05(e.target.value)} />
|
||||
<input
|
||||
type="text"
|
||||
className="w-max"
|
||||
value={nip05}
|
||||
onChange={e => setNip05(e.target.value)}
|
||||
disabled={readonly}
|
||||
/>
|
||||
<small>
|
||||
<FormattedMessage defaultMessage="Usernames are not unique on Nostr. The nostr address is your unique human-readable address that is unique to you upon registration." />
|
||||
</small>
|
||||
@ -150,9 +172,15 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Lightning Address" />
|
||||
</h4>
|
||||
<input className="w-max" type="text" value={lud16} onChange={e => setLud16(e.target.value)} />
|
||||
<input
|
||||
className="w-max"
|
||||
type="text"
|
||||
value={lud16}
|
||||
onChange={e => setLud16(e.target.value)}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
<AsyncButton className="primary" onClick={() => saveProfile()}>
|
||||
<AsyncButton className="primary" onClick={() => saveProfile()} disabled={readonly}>
|
||||
<FormattedMessage defaultMessage="Save" />
|
||||
</AsyncButton>
|
||||
</div>
|
||||
@ -170,7 +198,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
||||
background: (banner?.length ?? 0) > 0 ? `no-repeat center/cover url("${banner}")` : undefined,
|
||||
}}
|
||||
className="banner">
|
||||
<AsyncButton type="button" onClick={() => setNewBanner()}>
|
||||
<AsyncButton type="button" onClick={() => setNewBanner()} disabled={readonly}>
|
||||
<FormattedMessage defaultMessage="Upload" />
|
||||
</AsyncButton>
|
||||
</div>
|
||||
@ -178,7 +206,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
||||
{(props.avatar ?? true) && (
|
||||
<div className="avatar-stack">
|
||||
<Avatar pubkey={id} user={user} image={picture} />
|
||||
<AsyncButton type="button" className="btn-rnd" onClick={() => setNewAvatar()}>
|
||||
<AsyncButton type="button" className="btn-rnd" onClick={() => setNewAvatar()} disabled={readonly}>
|
||||
<Icon name="upload-01" />
|
||||
</AsyncButton>
|
||||
</div>
|
||||
|
@ -8,6 +8,7 @@ import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import { System } from "index";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { setRelays } from "Login";
|
||||
import AsyncButton from "Element/AsyncButton";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
@ -91,9 +92,9 @@ const RelaySettingsPage = () => {
|
||||
</div>
|
||||
<div className="flex mt10">
|
||||
<div className="f-grow"></div>
|
||||
<button type="button" onClick={() => saveRelays()}>
|
||||
<AsyncButton type="button" onClick={() => saveRelays()} disabled={login.readonly}>
|
||||
<FormattedMessage {...messages.Save} />
|
||||
</button>
|
||||
</AsyncButton>
|
||||
</div>
|
||||
{addRelay()}
|
||||
<h3>
|
||||
|
@ -158,7 +158,7 @@ export function createEmptyChatObject(id: string) {
|
||||
}
|
||||
|
||||
export function useNip4Chat() {
|
||||
const { publicKey } = useLogin();
|
||||
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
|
||||
return useSyncExternalStore(
|
||||
c => Nip4Chats.hook(c),
|
||||
() => Nip4Chats.snapshot(publicKey),
|
||||
@ -173,7 +173,7 @@ export function useNip29Chat() {
|
||||
}
|
||||
|
||||
export function useNip24Chat() {
|
||||
const { publicKey } = useLogin();
|
||||
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
|
||||
return useSyncExternalStore(
|
||||
c => Nip24Chats.hook(c),
|
||||
() => Nip24Chats.snapshot(publicKey),
|
||||
|
@ -458,6 +458,9 @@
|
||||
"GspYR7": {
|
||||
"defaultMessage": "{n} Dislike"
|
||||
},
|
||||
"Gxcr08": {
|
||||
"defaultMessage": "Broadcast Event"
|
||||
},
|
||||
"H+vHiz": {
|
||||
"defaultMessage": "Hex Key..",
|
||||
"description": "Hexidecimal 'key' input for improxy"
|
||||
@ -564,6 +567,9 @@
|
||||
"LF5kYT": {
|
||||
"defaultMessage": "Other Connections"
|
||||
},
|
||||
"LR1XjT": {
|
||||
"defaultMessage": "Pin too short"
|
||||
},
|
||||
"LXxsbk": {
|
||||
"defaultMessage": "Anonymous"
|
||||
},
|
||||
@ -1344,6 +1350,9 @@
|
||||
"defaultMessage": "Your key",
|
||||
"description": "Label for key input"
|
||||
},
|
||||
"wSZR47": {
|
||||
"defaultMessage": "Submit"
|
||||
},
|
||||
"wWLwvh": {
|
||||
"defaultMessage": "Anon",
|
||||
"description": "Anonymous Zap"
|
||||
|
@ -150,6 +150,7 @@
|
||||
"GUlSVG": "Claim your included Snort nostr address",
|
||||
"Gcn9NQ": "Magnet Link",
|
||||
"GspYR7": "{n} Dislike",
|
||||
"Gxcr08": "Broadcast Event",
|
||||
"H+vHiz": "Hex Key..",
|
||||
"H0JBH6": "Log Out",
|
||||
"H6/kLh": "Order Paid!",
|
||||
@ -185,6 +186,7 @@
|
||||
"KoFlZg": "Enter mint URL",
|
||||
"KtsyO0": "Enter Pin",
|
||||
"LF5kYT": "Other Connections",
|
||||
"LR1XjT": "Pin too short",
|
||||
"LXxsbk": "Anonymous",
|
||||
"LgbKvU": "Comment",
|
||||
"Lu5/Bj": "Open on Zapstr",
|
||||
@ -440,6 +442,7 @@
|
||||
"vxwnbh": "Amount of work to apply to all published events",
|
||||
"wEQDC6": "Edit",
|
||||
"wLtRCF": "Your key",
|
||||
"wSZR47": "Submit",
|
||||
"wWLwvh": "Anon",
|
||||
"wYSD2L": "Nostr Adddress",
|
||||
"wih7iJ": "name is blocked",
|
||||
|
@ -384,12 +384,12 @@ export class Connection extends ExternalStore<ConnectionStateSnapshot> {
|
||||
}
|
||||
this.AwaitingAuth.set(challenge, true);
|
||||
const authEvent = await this.Auth(challenge, this.Address);
|
||||
return new Promise(resolve => {
|
||||
if (!authEvent) {
|
||||
authCleanup();
|
||||
return Promise.reject("no event");
|
||||
}
|
||||
if (!authEvent) {
|
||||
authCleanup();
|
||||
throw new Error("No auth event");
|
||||
}
|
||||
|
||||
return await new Promise(resolve => {
|
||||
const t = setTimeout(() => {
|
||||
authCleanup();
|
||||
resolve();
|
||||
|
Loading…
x
Reference in New Issue
Block a user