readonly login sessions

This commit is contained in:
Kieran 2023-09-23 22:21:37 +01:00
parent 3efb5321f6
commit 94da60ebfa
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
30 changed files with 217 additions and 119 deletions

View File

@ -16,7 +16,7 @@ interface BookmarksProps {
const Bookmarks = ({ pubkey, bookmarks, related }: BookmarksProps) => { const Bookmarks = ({ pubkey, bookmarks, related }: BookmarksProps) => {
const [onlyPubkey, setOnlyPubkey] = useState<HexKey | "all">("all"); const [onlyPubkey, setOnlyPubkey] = useState<HexKey | "all">("all");
const loginPubKey = useLogin().publicKey; const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
const ps = useMemo(() => { const ps = useMemo(() => {
return [...new Set(bookmarks.map(ev => ev.pubkey))]; return [...new Set(bookmarks.map(ev => ev.pubkey))];
}, [bookmarks]); }, [bookmarks]);
@ -47,7 +47,7 @@ const Bookmarks = ({ pubkey, bookmarks, related }: BookmarksProps) => {
key={n.id} key={n.id}
data={n} data={n}
related={related} related={related}
options={{ showTime: false, showBookmarked: true, canUnbookmark: loginPubKey === pubkey }} options={{ showTime: false, showBookmarked: true, canUnbookmark: publicKey === pubkey }}
/> />
); );
})} })}

View File

@ -17,8 +17,8 @@ interface Token {
} }
export default function CashuNuts({ token }: { token: string }) { export default function CashuNuts({ token }: { token: string }) {
const login = useLogin(); const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
const profile = useUserProfile(login.publicKey); const profile = useUserProfile(publicKey);
async function copyToken(e: React.MouseEvent<HTMLButtonElement>, token: string) { async function copyToken(e: React.MouseEvent<HTMLButtonElement>, token: string) {
e.stopPropagation(); e.stopPropagation();

View File

@ -18,14 +18,14 @@ export interface DMProps {
} }
export default function DM(props: DMProps) { export default function DM(props: DMProps) {
const pubKey = useLogin().publicKey; const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const msg = props.data; const msg = props.data;
const [content, setContent] = useState<string>(); const [content, setContent] = useState<string>();
const { ref, inView } = useInView({ triggerOnce: true }); const { ref, inView } = useInView({ triggerOnce: true });
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const isMe = msg.from === pubKey; const isMe = msg.from === publicKey;
const otherPubkey = isMe ? pubKey : msg.from; const otherPubkey = isMe ? publicKey : msg.from;
async function decrypt() { async function decrypt() {
if (publisher) { if (publisher) {

View File

@ -10,12 +10,12 @@ import { Chat, ChatParticipant, createEmptyChatObject, useChatSystem } from "cha
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
export default function DmWindow({ id }: { id: string }) { export default function DmWindow({ id }: { id: string }) {
const pubKey = useLogin().publicKey; const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
const dms = useChatSystem(); const dms = useChatSystem();
const chat = dms.find(a => a.id === id) ?? createEmptyChatObject(id); const chat = dms.find(a => a.id === id) ?? createEmptyChatObject(id);
function participant(p: ChatParticipant) { function participant(p: ChatParticipant) {
if (p.id === pubKey) { if (p.id === publicKey) {
return <NoteToSelf className="f-grow mb-10" pubkey={p.id} />; return <NoteToSelf className="f-grow mb-10" pubkey={p.id} />;
} }
if (p.type === "pubkey") { if (p.type === "pubkey") {
@ -56,7 +56,7 @@ export default function DmWindow({ id }: { id: string }) {
} }
function DmChatSelected({ chat }: { chat: Chat }) { function DmChatSelected({ chat }: { chat: Chat }) {
const { publicKey: myPubKey } = useLogin(); const { publicKey: myPubKey } = useLogin(s => ({ publicKey: s.publicKey }));
const sortedDms = useMemo(() => { const sortedDms = useMemo(() => {
const myDms = chat?.messages; const myDms = chat?.messages;
if (myPubKey && myDms) { if (myPubKey && myDms) {

View File

@ -18,9 +18,9 @@ export interface FollowButtonProps {
export default function FollowButton(props: FollowButtonProps) { export default function FollowButton(props: FollowButtonProps) {
const pubkey = parseId(props.pubkey); const pubkey = parseId(props.pubkey);
const publisher = useEventPublisher(); 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 isFollowing = follows.item.includes(pubkey);
const baseClassname = `${props.className} follow-button`; const baseClassname = `${props.className ? ` ${props.className}` : ""}follow-button`;
async function follow(pubkey: HexKey) { async function follow(pubkey: HexKey) {
if (publisher) { if (publisher) {
@ -43,6 +43,7 @@ export default function FollowButton(props: FollowButtonProps) {
return ( return (
<AsyncButton <AsyncButton
className={isFollowing ? `${baseClassname} secondary` : baseClassname} className={isFollowing ? `${baseClassname} secondary` : baseClassname}
disabled={readonly}
onClick={() => (isFollowing ? unfollow(pubkey) : follow(pubkey))}> onClick={() => (isFollowing ? unfollow(pubkey) : follow(pubkey))}>
{isFollowing ? <FormattedMessage {...messages.Unfollow} /> : <FormattedMessage {...messages.Follow} />} {isFollowing ? <FormattedMessage {...messages.Unfollow} /> : <FormattedMessage {...messages.Follow} />}
</AsyncButton> </AsyncButton>

View File

@ -51,7 +51,7 @@ export default function FollowListBase({
<div className="flex mt10 mb10"> <div className="flex mt10 mb10">
<div className="f-grow bold">{title}</div> <div className="f-grow bold">{title}</div>
{actions} {actions}
<AsyncButton className="transparent" type="button" onClick={() => followAll()}> <AsyncButton className="transparent" type="button" onClick={() => followAll()} disabled={login.readonly}>
<FormattedMessage {...messages.FollowAll} /> <FormattedMessage {...messages.FollowAll} />
</AsyncButton> </AsyncButton>
</div> </div>

View File

@ -4,7 +4,7 @@ import Icon from "Icons/Icon";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
export function FollowingMark({ pubkey }: { pubkey: string }) { export function FollowingMark({ pubkey }: { pubkey: string }) {
const { follows } = useLogin(); const { follows } = useLogin(s => ({ follows: s.follows }));
const doesFollow = follows.item.includes(pubkey); const doesFollow = follows.item.includes(pubkey);
if (!doesFollow) return; if (!doesFollow) return;

View File

@ -7,7 +7,7 @@ import messages from "./messages";
export default function LogoutButton() { export default function LogoutButton() {
const navigate = useNavigate(); const navigate = useNavigate();
const login = useLogin(); const login = useLogin(s => ({ publicKey: s.publicKey, id: s.id }));
if (!login.publicKey) return; if (!login.publicKey) return;
return ( return (

View File

@ -4,8 +4,8 @@ import useLogin from "Hooks/useLogin";
const MixCloudEmbed = ({ link }: { link: string }) => { const MixCloudEmbed = ({ link }: { link: string }) => {
const feedPath = (MixCloudRegex.test(link) && RegExp.$1) + "%2F" + (MixCloudRegex.test(link) && RegExp.$2); const feedPath = (MixCloudRegex.test(link) && RegExp.$1) + "%2F" + (MixCloudRegex.test(link) && RegExp.$2);
const lightTheme = useLogin().preferences.theme === "light"; const { theme } = useLogin(s => ({ theme: s.preferences.theme }));
const lightParams = lightTheme ? "light=1" : "light=0"; const lightParams = theme === "light" ? "light=1" : "light=0";
return ( return (
<> <>
<br /> <br />

View File

@ -43,8 +43,8 @@ export default function Nip5Service(props: Nip05ServiceProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const { helpText = true } = props; const { helpText = true } = props;
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const pubkey = useLogin().publicKey; const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
const user = useUserProfile(pubkey); const user = useUserProfile(publicKey);
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const svc = useMemo(() => new ServiceProvider(props.service), [props.service]); const svc = useMemo(() => new ServiceProvider(props.service), [props.service]);
const [serviceConfig, setServiceConfig] = useState<ServiceConfig>(); const [serviceConfig, setServiceConfig] = useState<ServiceConfig>();
@ -179,11 +179,11 @@ export default function Nip5Service(props: Nip05ServiceProps) {
} }
async function startBuy(handle: string, domain: string) { async function startBuy(handle: string, domain: string) {
if (!pubkey) { if (!publicKey) {
return; return;
} }
const rsp = await svc.RegisterHandle(handle, domain, pubkey); const rsp = await svc.RegisterHandle(handle, domain, publicKey);
if ("error" in rsp) { if ("error" in rsp) {
setError(rsp); setError(rsp);
} else { } else {
@ -193,7 +193,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
} }
async function claimForSubscription(handle: string, domain: string, sub: string) { async function claimForSubscription(handle: string, domain: string, sub: string) {
if (!pubkey || !publisher) { if (!publicKey || !publisher) {
return; return;
} }

View File

@ -29,7 +29,6 @@ interface NosteContextMenuProps {
export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) { export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const login = useLogin(); const login = useLogin();
const { pinned, bookmarked, publicKey, preferences: prefs } = login;
const { mute, block } = useModeration(); const { mute, block } = useModeration();
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const [showBroadcast, setShowBroadcast] = useState(false); const [showBroadcast, setShowBroadcast] = useState(false);
@ -37,7 +36,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
const langNames = new Intl.DisplayNames([...window.navigator.languages], { const langNames = new Intl.DisplayNames([...window.navigator.languages], {
type: "language", type: "language",
}); });
const isMine = ev.pubkey === publicKey; const isMine = ev.pubkey === login.publicKey;
async function deleteEvent() { async function deleteEvent() {
if (window.confirm(formatMessage(messages.ConfirmDeletion, { id: ev.id.substring(0, 8) })) && publisher) { 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) { async function pin(id: HexKey) {
if (publisher) { if (publisher) {
const es = [...pinned.item, id]; const es = [...login.pinned.item, id];
const ev = await publisher.noteList(es, Lists.Pinned); const ev = await publisher.noteList(es, Lists.Pinned);
System.BroadcastEvent(ev); System.BroadcastEvent(ev);
setPinned(login, es, ev.created_at * 1000); setPinned(login, es, ev.created_at * 1000);
@ -98,7 +97,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
async function bookmark(id: HexKey) { async function bookmark(id: HexKey) {
if (publisher) { if (publisher) {
const es = [...bookmarked.item, id]; const es = [...login.bookmarked.item, id];
const ev = await publisher.noteList(es, Lists.Bookmarked); const ev = await publisher.noteList(es, Lists.Bookmarked);
System.BroadcastEvent(ev); System.BroadcastEvent(ev);
setBookmarked(login, es, ev.created_at * 1000); setBookmarked(login, es, ev.created_at * 1000);
@ -131,13 +130,13 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
<Icon name="share" /> <Icon name="share" />
<FormattedMessage {...messages.Share} /> <FormattedMessage {...messages.Share} />
</MenuItem> </MenuItem>
{!pinned.item.includes(ev.id) && ( {!login.pinned.item.includes(ev.id) && !login.readonly && (
<MenuItem onClick={() => pin(ev.id)}> <MenuItem onClick={() => pin(ev.id)}>
<Icon name="pin" /> <Icon name="pin" />
<FormattedMessage {...messages.Pin} /> <FormattedMessage {...messages.Pin} />
</MenuItem> </MenuItem>
)} )}
{!bookmarked.item.includes(ev.id) && ( {!login.bookmarked.item.includes(ev.id) && !login.readonly && (
<MenuItem onClick={() => bookmark(ev.id)}> <MenuItem onClick={() => bookmark(ev.id)}>
<Icon name="bookmark" /> <Icon name="bookmark" />
<FormattedMessage {...messages.Bookmark} /> <FormattedMessage {...messages.Bookmark} />
@ -147,23 +146,23 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
<Icon name="copy" /> <Icon name="copy" />
<FormattedMessage {...messages.CopyID} /> <FormattedMessage {...messages.CopyID} />
</MenuItem> </MenuItem>
<MenuItem onClick={() => mute(ev.pubkey)}> {!login.readonly && (
<Icon name="mute" /> <MenuItem onClick={() => mute(ev.pubkey)}>
<FormattedMessage {...messages.Mute} /> <Icon name="mute" />
</MenuItem> <FormattedMessage {...messages.Mute} />
{prefs.enableReactions && ( </MenuItem>
)}
{login.preferences.enableReactions && !login.readonly && (
<MenuItem onClick={() => props.react("-")}> <MenuItem onClick={() => props.react("-")}>
<Icon name="dislike" /> <Icon name="dislike" />
<FormattedMessage {...messages.DislikeAction} /> <FormattedMessage {...messages.DislikeAction} />
</MenuItem> </MenuItem>
)} )}
{ev.pubkey === publicKey && ( <MenuItem onClick={handleReBroadcastButtonClick}>
<MenuItem onClick={handleReBroadcastButtonClick}> <Icon name="relay" />
<Icon name="relay" /> <FormattedMessage defaultMessage="Broadcast Event" />
<FormattedMessage {...messages.ReBroadcast} /> </MenuItem>
</MenuItem> {ev.pubkey !== login.publicKey && !login.readonly && (
)}
{ev.pubkey !== publicKey && (
<MenuItem onClick={() => block(ev.pubkey)}> <MenuItem onClick={() => block(ev.pubkey)}>
<Icon name="block" /> <Icon name="block" />
<FormattedMessage {...messages.Block} /> <FormattedMessage {...messages.Block} />
@ -173,13 +172,13 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
<Icon name="translate" /> <Icon name="translate" />
<FormattedMessage {...messages.TranslateTo} values={{ lang: langNames.of(lang.split("-")[0]) }} /> <FormattedMessage {...messages.TranslateTo} values={{ lang: langNames.of(lang.split("-")[0]) }} />
</MenuItem> </MenuItem>
{prefs.showDebugMenus && ( {login.preferences.showDebugMenus && (
<MenuItem onClick={() => copyEvent()}> <MenuItem onClick={() => copyEvent()}>
<Icon name="json" /> <Icon name="json" />
<FormattedMessage {...messages.CopyJSON} /> <FormattedMessage {...messages.CopyJSON} />
</MenuItem> </MenuItem>
)} )}
{isMine && ( {isMine && !login.readonly && (
<MenuItem onClick={() => deleteEvent()}> <MenuItem onClick={() => deleteEvent()}>
<Icon name="trash" className="red" /> <Icon name="trash" className="red" />
<FormattedMessage {...messages.Delete} /> <FormattedMessage {...messages.Delete} />

View File

@ -32,7 +32,7 @@ export function NoteCreator() {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const uploader = useFileUpload(); const uploader = useFileUpload();
const login = useLogin(); const login = useLogin(s => ({ relays: s.relays, publicKey: s.publicKey }));
const note = useNoteCreator(); const note = useNoteCreator();
const relays = login.relays; const relays = login.relays;

View File

@ -47,8 +47,11 @@ export default function NoteFooter(props: NoteFooterProps) {
const { ev, positive, reposts, zaps } = props; const { ev, positive, reposts, zaps } = props;
const system = useContext(SnortContext); const system = useContext(SnortContext);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const login = useLogin(); const {
const { publicKey, preferences: prefs } = login; publicKey,
preferences: prefs,
readonly,
} = useLogin(s => ({ preferences: s.preferences, publicKey: s.publicKey, readonly: s.readonly }));
const author = useUserProfile(ev.pubkey); const author = useUserProfile(ev.pubkey);
const interactionCache = useInteractionCache(publicKey, ev.id); const interactionCache = useInteractionCache(publicKey, ev.id);
const publisher = useEventPublisher(); const publisher = useEventPublisher();
@ -59,6 +62,7 @@ export default function NoteFooter(props: NoteFooterProps) {
const walletState = useWallet(); const walletState = useWallet();
const wallet = walletState.wallet; const wallet = walletState.wallet;
const canFastZap = wallet?.isReady() && !readonly;
const isMine = ev.pubkey === publicKey; const isMine = ev.pubkey === publicKey;
const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0); const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
const didZap = interactionCache.data.zapped || zaps.some(a => a.sender === publicKey); 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; if (zapping || e?.isPropagationStopped()) return;
const lnurl = getZapTarget(); const lnurl = getZapTarget();
if (wallet?.isReady() && lnurl) { if (canFastZap && lnurl) {
setZapping(true); setZapping(true);
try { try {
await fastZapInner(lnurl, prefs.defaultZapAmount); await fastZapInner(lnurl, prefs.defaultZapAmount);
@ -194,7 +198,7 @@ export default function NoteFooter(props: NoteFooterProps) {
className={didZap ? "reacted" : ""} className={didZap ? "reacted" : ""}
{...longPress()} {...longPress()}
title={formatMessage({ defaultMessage: "Zap" })} title={formatMessage({ defaultMessage: "Zap" })}
iconName={wallet?.isReady() ? "zapFast" : "zap"} iconName={canFastZap ? "zapFast" : "zap"}
value={zapTotal} value={zapTotal}
onClick={e => fastZap(e)} onClick={e => fastZap(e)}
/> />
@ -204,13 +208,17 @@ export default function NoteFooter(props: NoteFooterProps) {
} }
function repostIcon() { function repostIcon() {
if (readonly) return;
return ( return (
<AsyncFooterIcon <AsyncFooterIcon
className={hasReposted() ? "reacted" : ""} className={hasReposted() ? "reacted" : ""}
iconName="repeat" iconName="repeat"
title={formatMessage({ defaultMessage: "Repost" })} title={formatMessage({ defaultMessage: "Repost" })}
value={reposts.length} 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"} iconName={reacted ? "heart-solid" : "heart"}
title={formatMessage({ defaultMessage: "Like" })} title={formatMessage({ defaultMessage: "Like" })}
value={positive.length} value={positive.length}
onClick={() => react(prefs.reactionEmoji)} onClick={async () => {
if (readonly) return;
await react(prefs.reactionEmoji);
}}
/> />
); );
} }
function replyIcon() { function replyIcon() {
if (readonly) return;
return ( return (
<AsyncFooterIcon <AsyncFooterIcon
className={note.show ? "reacted" : ""} className={note.show ? "reacted" : ""}

View File

@ -96,6 +96,7 @@ export function LoginUnlock() {
LoginStore.setPublisher(login.id, pub); LoginStore.setPublisher(login.id, pub);
LoginStore.updateSession({ LoginStore.updateSession({
...login, ...login,
readonly: false,
privateKeyData: newPin, privateKeyData: newPin,
privateKey: undefined, privateKey: undefined,
}); });
@ -112,12 +113,20 @@ export function LoginUnlock() {
LoginStore.setPublisher(login.id, pub); LoginStore.setPublisher(login.id, pub);
LoginStore.updateSession({ LoginStore.updateSession({
...login, ...login,
readonly: false,
privateKeyData: key, 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) { if (login.privateKey !== undefined) {
return ( return (
<PinPrompt <PinPrompt
@ -142,7 +151,7 @@ export function LoginUnlock() {
} }
onResult={unlockSession} onResult={unlockSession}
onCancel={() => { onCancel={() => {
//nothing makeSessionReadonly();
}} }}
/> />
); );

View File

@ -1,45 +1,34 @@
import { useState } from "react"; import { useContext, useState } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { TaggedNostrEvent } from "@snort/system"; import { TaggedNostrEvent } from "@snort/system";
import { SnortContext } from "@snort/system-react";
import useEventPublisher from "Hooks/useEventPublisher";
import Modal from "Element/Modal"; import Modal from "Element/Modal";
import messages from "./messages"; import messages from "./messages";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import { System } from "index"; import AsyncButton from "./AsyncButton";
export function ReBroadcaster({ onClose, ev }: { onClose: () => void; ev: TaggedNostrEvent }) { export function ReBroadcaster({ onClose, ev }: { onClose: () => void; ev: TaggedNostrEvent }) {
const [selected, setSelected] = useState<Array<string>>(); const [selected, setSelected] = useState<Array<string>>();
const publisher = useEventPublisher(); const system = useContext(SnortContext);
const { relays } = useLogin(s => ({ relays: s.relays }));
async function sendReBroadcast() { async function sendReBroadcast() {
if (publisher) { if (selected) {
if (selected) { await Promise.all(selected.map(r => system.WriteOnceToRelay(r, ev)));
await Promise.all(selected.map(r => System.WriteOnceToRelay(r, ev))); } else {
} else { system.BroadcastEvent(ev);
System.BroadcastEvent(ev);
}
} }
} }
function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) {
ev.stopPropagation();
sendReBroadcast().catch(console.warn);
}
const login = useLogin();
const relays = login.relays;
function renderRelayCustomisation() { function renderRelayCustomisation() {
return ( return (
<div> <div className="flex-column g8">
{Object.keys(relays.item || {}) {Object.keys(relays.item || {})
.filter(el => relays.item[el].write) .filter(el => relays.item[el].write)
.map((r, i, a) => ( .map((r, i, a) => (
<div className="card flex"> <div className="card flex f-space">
<div className="flex f-col f-grow"> <div>{r}</div>
<div>{r}</div>
</div>
<div> <div>
<input <input
type="checkbox" type="checkbox"
@ -63,13 +52,13 @@ export function ReBroadcaster({ onClose, ev }: { onClose: () => void; ev: Tagged
<> <>
<Modal id="broadcaster" className="note-creator-modal" onClose={onClose}> <Modal id="broadcaster" className="note-creator-modal" onClose={onClose}>
{renderRelayCustomisation()} {renderRelayCustomisation()}
<div className="note-creator-actions"> <div className="flex g8">
<button className="secondary" onClick={onClose}> <button className="secondary" onClick={onClose}>
<FormattedMessage {...messages.Cancel} /> <FormattedMessage {...messages.Cancel} />
</button> </button>
<button onClick={onSubmit}> <AsyncButton onClick={sendReBroadcast}>
<FormattedMessage {...messages.ReBroadcast} /> <FormattedMessage {...messages.ReBroadcast} />
</button> </AsyncButton>
</div> </div>
</Modal> </Modal>
</> </>

View File

@ -248,9 +248,11 @@ function SendSatsInput(props: {
onChange?: (v: SendSatsInputSelection) => void; onChange?: (v: SendSatsInputSelection) => void;
onNextStage: (v: SendSatsInputSelection) => Promise<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 { formatMessage } = useIntl();
const defaultZapAmount = login.preferences.defaultZapAmount;
const amounts: Record<string, string> = { const amounts: Record<string, string> = {
[defaultZapAmount.toString()]: "", [defaultZapAmount.toString()]: "",
"1000": "👍", "1000": "👍",
@ -264,7 +266,7 @@ function SendSatsInput(props: {
const [comment, setComment] = useState<string>(); const [comment, setComment] = useState<string>();
const [amount, setAmount] = useState<number>(defaultZapAmount); const [amount, setAmount] = useState<number>(defaultZapAmount);
const [customAmount, setCustomAmount] = 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() { function getValue() {
return { return {
@ -358,6 +360,7 @@ function SendSatsInput(props: {
} }
function SendSatsZapTypeSelector({ zapType, setZapType }: { zapType: ZapType; setZapType: (t: ZapType) => void }) { function SendSatsZapTypeSelector({ zapType, setZapType }: { zapType: ZapType; setZapType: (t: ZapType) => void }) {
const { readonly } = useLogin(s => ({ readonly: s.readonly }));
const makeTab = (t: ZapType, n: React.ReactNode) => ( const makeTab = (t: ZapType, n: React.ReactNode) => (
<button type="button" className={zapType === t ? "" : "secondary"} onClick={() => setZapType(t)}> <button type="button" className={zapType === t ? "" : "secondary"} onClick={() => setZapType(t)}>
{n} {n}
@ -369,7 +372,7 @@ function SendSatsZapTypeSelector({ zapType, setZapType }: { zapType: ZapType; se
<FormattedMessage defaultMessage="Zap Type" /> <FormattedMessage defaultMessage="Zap Type" />
</h3> </h3>
<div className="flex g8"> <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.PrivateZap, "Private")*/}
{makeTab(ZapType.AnonZap, <FormattedMessage defaultMessage="Anon" description="Anonymous Zap" />)} {makeTab(ZapType.AnonZap, <FormattedMessage defaultMessage="Anon" description="Anonymous Zap" />)}
{makeTab( {makeTab(

View File

@ -1,9 +1,19 @@
import { LoginStore } from "Login"; import { LoginSession, LoginStore } from "Login";
import { useSyncExternalStore } from "react"; import { useSyncExternalStore } from "react";
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector";
export default function useLogin() { export default function useLogin<T extends object = LoginSession>(selector?: (v: LoginSession) => T) {
return useSyncExternalStore( if (selector) {
s => LoginStore.hook(s), return useSyncExternalStoreWithSelector(
() => LoginStore.snapshot(), s => LoginStore.hook(s),
); () => LoginStore.snapshot(),
undefined,
selector,
);
} else {
return useSyncExternalStore<T>(
s => LoginStore.hook(s),
() => LoginStore.snapshot() as T,
);
}
} }

View File

@ -81,7 +81,7 @@ const getMessages = (locale: string) => {
}; };
export const IntlProvider = ({ children }: { children: ReactNode }) => { export const IntlProvider = ({ children }: { children: ReactNode }) => {
const { language } = useLogin().preferences; const { language } = useLogin(s => ({ language: s.preferences.language }));
const locale = language ?? getLocale(); const locale = language ?? getLocale();
const [messages, setMessages] = useState<Record<string, string>>(enMessages); const [messages, setMessages] = useState<Record<string, string>>(enMessages);

View File

@ -188,10 +188,8 @@ export function createPublisher(l: LoginSession, pin?: PinEncrypted) {
case LoginSessionType.Nip7os: { case LoginSessionType.Nip7os: {
return new EventPublisher(new Nip7OsSigner(), unwrap(l.publicKey)); return new EventPublisher(new Nip7OsSigner(), unwrap(l.publicKey));
} }
default: { case LoginSessionType.Nip7: {
if (l.publicKey) { return new EventPublisher(new Nip7Signer(), unwrap(l.publicKey));
return new EventPublisher(new Nip7Signer(), l.publicKey);
}
} }
} }
} }

View File

@ -35,6 +35,11 @@ export interface LoginSession {
*/ */
privateKey?: HexKey; privateKey?: HexKey;
/**
* If this session cannot sign events
*/
readonly: boolean;
/** /**
* Encrypted private key * Encrypted private key
*/ */

View File

@ -14,6 +14,7 @@ const LoggedOut = {
id: "default", id: "default",
type: "public_key", type: "public_key",
preferences: DefaultPreferences, preferences: DefaultPreferences,
readonly: true,
tags: { tags: {
item: [], item: [],
timestamp: 0, timestamp: 0,
@ -65,6 +66,12 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
if (!this.#activeAccount) { if (!this.#activeAccount) {
this.#activeAccount = this.#accounts.keys().next().value; 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() { getSessions() {
@ -108,6 +115,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
const newSession = { const newSession = {
...LoggedOut, ...LoggedOut,
id: uuid(), id: uuid(),
readonly: type === LoginSessionType.PublicKey,
type, type,
publicKey: key, publicKey: key,
relays: { relays: {
@ -146,6 +154,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
...LoggedOut, ...LoggedOut,
id: uuid(), id: uuid(),
type: LoginSessionType.PrivateKey, type: LoginSessionType.PrivateKey,
readonly: false,
privateKeyData: key, privateKeyData: key,
publicKey: pubKey, publicKey: pubKey,
generatedEntropy: entropy, 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) { if (didMigrate) {
console.debug("Finished migration to MultiAccountStore"); console.debug("Finished migration in MultiAccountStore");
this.#save(); this.#save();
} }
} }

View File

@ -66,13 +66,14 @@ export default function Layout() {
const NoteCreatorButton = () => { const NoteCreatorButton = () => {
const location = useLocation(); 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 { show, replyTo, update } = useNoteCreator(v => ({ show: v.show, replyTo: v.replyTo, update: v.update }));
const shouldHideNoteCreator = useMemo(() => { const shouldHideNoteCreator = useMemo(() => {
const isReplyNoteCreatorShowing = replyTo && show; const isReplyNoteCreatorShowing = replyTo && show;
const hideOn = ["/settings", "/messages", "/new", "/login", "/donate", "/e", "/subscribe"]; const hideOn = ["/settings", "/messages", "/new", "/login", "/donate", "/e", "/subscribe"];
return isReplyNoteCreatorShowing || hideOn.some(a => location.pathname.startsWith(a)); return readonly || isReplyNoteCreatorShowing || hideOn.some(a => location.pathname.startsWith(a));
}, [location]); }, [location, readonly]);
if (shouldHideNoteCreator) return; if (shouldHideNoteCreator) return;
return ( return (
@ -96,7 +97,12 @@ const AccountHeader = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { formatMessage } = useIntl(); 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 profile = useUserProfile(publicKey);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [searching, setSearching] = useState(false); const [searching, setSearching] = useState(false);
@ -174,10 +180,12 @@ const AccountHeader = () => {
)} )}
</div> </div>
)} )}
<Link className="btn" to="/messages"> {!readonly && (
<Icon name="mail" size={24} /> <Link className="btn" to="/messages">
{unreadDms > 0 && <span className="has-unread"></span>} <Icon name="mail" size={24} />
</Link> {unreadDms > 0 && <span className="has-unread"></span>}
</Link>
)}
<Link className="btn" to="/notifications" onClick={goToNotifications}> <Link className="btn" to="/notifications" onClick={goToNotifications}>
<Icon name="bell-02" size={24} /> <Icon name="bell-02" size={24} />
{hasNotifications && <span className="has-unread"></span>} {hasNotifications && <span className="has-unread"></span>}

View File

@ -97,6 +97,7 @@ export default function LoginPage() {
async function doLogin(pin?: string) { async function doLogin(pin?: string) {
try { try {
await loginHandler.doLogin(key, pin); await loginHandler.doLogin(key, pin);
navigate("/");
} catch (e) { } catch (e) {
if (e instanceof PinRequiredError) { if (e instanceof PinRequiredError) {
setPin(true); setPin(true);

View File

@ -466,7 +466,7 @@ export default function ProfilePage() {
<Icon name="zap" size={16} /> <Icon name="zap" size={16} />
</IconButton> </IconButton>
)} )}
{loginPubKey && ( {loginPubKey && !login.readonly && (
<> <>
<IconButton <IconButton
onClick={() => onClick={() =>

View File

@ -22,7 +22,7 @@ export interface ProfileSettingsProps {
export default function ProfileSettings(props: ProfileSettingsProps) { export default function ProfileSettings(props: ProfileSettingsProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const { publicKey: id } = useLogin(); const { publicKey: id, readonly } = useLogin(s => ({ publicKey: s.publicKey, readonly: s.readonly }));
const user = useUserProfile(id ?? ""); const user = useUserProfile(id ?? "");
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const uploader = useFileUpload(); const uploader = useFileUpload();
@ -113,26 +113,48 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
<h4> <h4>
<FormattedMessage defaultMessage="Name" /> <FormattedMessage defaultMessage="Name" />
</h4> </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>
<div className="flex f-col w-max g8"> <div className="flex f-col w-max g8">
<h4> <h4>
<FormattedMessage defaultMessage="About" /> <FormattedMessage defaultMessage="About" />
</h4> </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>
<div className="flex f-col w-max g8"> <div className="flex f-col w-max g8">
<h4> <h4>
<FormattedMessage defaultMessage="Website" /> <FormattedMessage defaultMessage="Website" />
</h4> </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>
<div className="flex f-col w-max g8"> <div className="flex f-col w-max g8">
<h4> <h4>
<FormattedMessage defaultMessage="Nostr Address" /> <FormattedMessage defaultMessage="Nostr Address" />
</h4> </h4>
<div className="flex f-col g8 w-max"> <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> <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." /> <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> </small>
@ -150,9 +172,15 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
<h4> <h4>
<FormattedMessage defaultMessage="Lightning Address" /> <FormattedMessage defaultMessage="Lightning Address" />
</h4> </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> </div>
<AsyncButton className="primary" onClick={() => saveProfile()}> <AsyncButton className="primary" onClick={() => saveProfile()} disabled={readonly}>
<FormattedMessage defaultMessage="Save" /> <FormattedMessage defaultMessage="Save" />
</AsyncButton> </AsyncButton>
</div> </div>
@ -170,7 +198,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
background: (banner?.length ?? 0) > 0 ? `no-repeat center/cover url("${banner}")` : undefined, background: (banner?.length ?? 0) > 0 ? `no-repeat center/cover url("${banner}")` : undefined,
}} }}
className="banner"> className="banner">
<AsyncButton type="button" onClick={() => setNewBanner()}> <AsyncButton type="button" onClick={() => setNewBanner()} disabled={readonly}>
<FormattedMessage defaultMessage="Upload" /> <FormattedMessage defaultMessage="Upload" />
</AsyncButton> </AsyncButton>
</div> </div>
@ -178,7 +206,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
{(props.avatar ?? true) && ( {(props.avatar ?? true) && (
<div className="avatar-stack"> <div className="avatar-stack">
<Avatar pubkey={id} user={user} image={picture} /> <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" /> <Icon name="upload-01" />
</AsyncButton> </AsyncButton>
</div> </div>

View File

@ -8,6 +8,7 @@ import useEventPublisher from "Hooks/useEventPublisher";
import { System } from "index"; import { System } from "index";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import { setRelays } from "Login"; import { setRelays } from "Login";
import AsyncButton from "Element/AsyncButton";
import messages from "./messages"; import messages from "./messages";
@ -91,9 +92,9 @@ const RelaySettingsPage = () => {
</div> </div>
<div className="flex mt10"> <div className="flex mt10">
<div className="f-grow"></div> <div className="f-grow"></div>
<button type="button" onClick={() => saveRelays()}> <AsyncButton type="button" onClick={() => saveRelays()} disabled={login.readonly}>
<FormattedMessage {...messages.Save} /> <FormattedMessage {...messages.Save} />
</button> </AsyncButton>
</div> </div>
{addRelay()} {addRelay()}
<h3> <h3>

View File

@ -158,7 +158,7 @@ export function createEmptyChatObject(id: string) {
} }
export function useNip4Chat() { export function useNip4Chat() {
const { publicKey } = useLogin(); const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
return useSyncExternalStore( return useSyncExternalStore(
c => Nip4Chats.hook(c), c => Nip4Chats.hook(c),
() => Nip4Chats.snapshot(publicKey), () => Nip4Chats.snapshot(publicKey),
@ -173,7 +173,7 @@ export function useNip29Chat() {
} }
export function useNip24Chat() { export function useNip24Chat() {
const { publicKey } = useLogin(); const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
return useSyncExternalStore( return useSyncExternalStore(
c => Nip24Chats.hook(c), c => Nip24Chats.hook(c),
() => Nip24Chats.snapshot(publicKey), () => Nip24Chats.snapshot(publicKey),

View File

@ -458,6 +458,9 @@
"GspYR7": { "GspYR7": {
"defaultMessage": "{n} Dislike" "defaultMessage": "{n} Dislike"
}, },
"Gxcr08": {
"defaultMessage": "Broadcast Event"
},
"H+vHiz": { "H+vHiz": {
"defaultMessage": "Hex Key..", "defaultMessage": "Hex Key..",
"description": "Hexidecimal 'key' input for improxy" "description": "Hexidecimal 'key' input for improxy"
@ -564,6 +567,9 @@
"LF5kYT": { "LF5kYT": {
"defaultMessage": "Other Connections" "defaultMessage": "Other Connections"
}, },
"LR1XjT": {
"defaultMessage": "Pin too short"
},
"LXxsbk": { "LXxsbk": {
"defaultMessage": "Anonymous" "defaultMessage": "Anonymous"
}, },
@ -1344,6 +1350,9 @@
"defaultMessage": "Your key", "defaultMessage": "Your key",
"description": "Label for key input" "description": "Label for key input"
}, },
"wSZR47": {
"defaultMessage": "Submit"
},
"wWLwvh": { "wWLwvh": {
"defaultMessage": "Anon", "defaultMessage": "Anon",
"description": "Anonymous Zap" "description": "Anonymous Zap"

View File

@ -150,6 +150,7 @@
"GUlSVG": "Claim your included Snort nostr address", "GUlSVG": "Claim your included Snort nostr address",
"Gcn9NQ": "Magnet Link", "Gcn9NQ": "Magnet Link",
"GspYR7": "{n} Dislike", "GspYR7": "{n} Dislike",
"Gxcr08": "Broadcast Event",
"H+vHiz": "Hex Key..", "H+vHiz": "Hex Key..",
"H0JBH6": "Log Out", "H0JBH6": "Log Out",
"H6/kLh": "Order Paid!", "H6/kLh": "Order Paid!",
@ -185,6 +186,7 @@
"KoFlZg": "Enter mint URL", "KoFlZg": "Enter mint URL",
"KtsyO0": "Enter Pin", "KtsyO0": "Enter Pin",
"LF5kYT": "Other Connections", "LF5kYT": "Other Connections",
"LR1XjT": "Pin too short",
"LXxsbk": "Anonymous", "LXxsbk": "Anonymous",
"LgbKvU": "Comment", "LgbKvU": "Comment",
"Lu5/Bj": "Open on Zapstr", "Lu5/Bj": "Open on Zapstr",
@ -440,6 +442,7 @@
"vxwnbh": "Amount of work to apply to all published events", "vxwnbh": "Amount of work to apply to all published events",
"wEQDC6": "Edit", "wEQDC6": "Edit",
"wLtRCF": "Your key", "wLtRCF": "Your key",
"wSZR47": "Submit",
"wWLwvh": "Anon", "wWLwvh": "Anon",
"wYSD2L": "Nostr Adddress", "wYSD2L": "Nostr Adddress",
"wih7iJ": "name is blocked", "wih7iJ": "name is blocked",

View File

@ -384,12 +384,12 @@ export class Connection extends ExternalStore<ConnectionStateSnapshot> {
} }
this.AwaitingAuth.set(challenge, true); this.AwaitingAuth.set(challenge, true);
const authEvent = await this.Auth(challenge, this.Address); const authEvent = await this.Auth(challenge, this.Address);
return new Promise(resolve => { if (!authEvent) {
if (!authEvent) { authCleanup();
authCleanup(); throw new Error("No auth event");
return Promise.reject("no event"); }
}
return await new Promise(resolve => {
const t = setTimeout(() => { const t = setTimeout(() => {
authCleanup(); authCleanup();
resolve(); resolve();