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 [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 }}
/>
);
})}

View File

@ -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();

View File

@ -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) {

View File

@ -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) {

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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 (

View File

@ -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 />

View File

@ -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;
}

View File

@ -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} />

View File

@ -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;

View File

@ -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" : ""}

View File

@ -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();
}}
/>
);

View File

@ -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>
</>

View File

@ -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(

View File

@ -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,
);
}
}

View File

@ -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);

View File

@ -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));
}
}
}

View File

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

View File

@ -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();
}
}

View File

@ -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>}

View File

@ -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);

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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),

View File

@ -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"

View File

@ -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",

View File

@ -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();