feat: show login dialog when relay requires auth
This commit is contained in:
parent
414389c317
commit
bed8df06e8
7 changed files with 59 additions and 36 deletions
|
|
@ -142,13 +142,13 @@ export function PageManager({
|
||||||
<div className="flex h-full">
|
<div className="flex h-full">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<ResizablePanelGroup direction="horizontal">
|
<ResizablePanelGroup direction="horizontal">
|
||||||
<ResizablePanel defaultSize={55} minSize={30}>
|
<ResizablePanel minSize={30}>
|
||||||
<div key={primaryPageKey} className="h-full">
|
<div key={primaryPageKey} className="h-full">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle />
|
<ResizableHandle />
|
||||||
<ResizablePanel defaultSize={45} minSize={30} className="relative">
|
<ResizablePanel minSize={30} className="relative">
|
||||||
{secondaryStack.length ? (
|
{secondaryStack.length ? (
|
||||||
secondaryStack.map((item, index) => (
|
secondaryStack.map((item, index) => (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,9 @@ export default function NoteList({
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { isReady, signEvent } = useNostr()
|
const { signEvent, checkLogin } = useNostr()
|
||||||
const { isFetching: isFetchingRelayInfo, areAlgoRelays } = useFetchRelayInfos(relayUrls)
|
const { isFetching: isFetchingRelayInfo, areAlgoRelays } = useFetchRelayInfos(relayUrls)
|
||||||
|
const [refreshCount, setRefreshCount] = useState(0)
|
||||||
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
|
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
|
||||||
const [events, setEvents] = useState<Event[]>([])
|
const [events, setEvents] = useState<Event[]>([])
|
||||||
const [newEvents, setNewEvents] = useState<Event[]>([])
|
const [newEvents, setNewEvents] = useState<Event[]>([])
|
||||||
|
|
@ -44,7 +45,7 @@ export default function NoteList({
|
||||||
}, [JSON.stringify(filter), areAlgoRelays])
|
}, [JSON.stringify(filter), areAlgoRelays])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isReady || isFetchingRelayInfo) return
|
if (isFetchingRelayInfo) return
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
setInitialized(false)
|
setInitialized(false)
|
||||||
|
|
@ -75,7 +76,13 @@ export default function NoteList({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ signer: signEvent, needSort: !areAlgoRelays }
|
{
|
||||||
|
signer: async (evt) => {
|
||||||
|
const signedEvt = await checkLogin(() => signEvent(evt))
|
||||||
|
return signedEvt ?? null
|
||||||
|
},
|
||||||
|
needSort: !areAlgoRelays
|
||||||
|
}
|
||||||
)
|
)
|
||||||
setTimelineKey(timelineKey)
|
setTimelineKey(timelineKey)
|
||||||
return closer
|
return closer
|
||||||
|
|
@ -88,9 +95,9 @@ export default function NoteList({
|
||||||
}, [
|
}, [
|
||||||
JSON.stringify(relayUrls),
|
JSON.stringify(relayUrls),
|
||||||
JSON.stringify(noteFilter),
|
JSON.stringify(noteFilter),
|
||||||
isReady,
|
|
||||||
isFetchingRelayInfo,
|
isFetchingRelayInfo,
|
||||||
areAlgoRelays
|
areAlgoRelays,
|
||||||
|
refreshCount
|
||||||
])
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -157,7 +164,17 @@ export default function NoteList({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center text-sm text-muted-foreground">
|
<div className="text-center text-sm text-muted-foreground">
|
||||||
{hasMore ? <div ref={bottomRef}>{t('loading...')}</div> : t('no more notes')}
|
{hasMore ? (
|
||||||
|
<div ref={bottomRef}>{t('loading...')}</div>
|
||||||
|
) : events.length ? (
|
||||||
|
t('no more notes')
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center w-full max-sm:mt-2">
|
||||||
|
<Button size="lg" onClick={() => setRefreshCount((pre) => pre + 1)}>
|
||||||
|
{t('reload notes')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ const LIMIT = 100
|
||||||
|
|
||||||
export default function ReplyNoteList({ event, className }: { event: NEvent; className?: string }) {
|
export default function ReplyNoteList({ event, className }: { event: NEvent; className?: string }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { isReady, pubkey } = useNostr()
|
const { pubkey } = useNostr()
|
||||||
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
|
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
|
||||||
const [until, setUntil] = useState<number | undefined>(() => dayjs().unix())
|
const [until, setUntil] = useState<number | undefined>(() => dayjs().unix())
|
||||||
const [replies, setReplies] = useState<NEvent[]>([])
|
const [replies, setReplies] = useState<NEvent[]>([])
|
||||||
|
|
@ -46,7 +46,7 @@ export default function ReplyNoteList({ event, className }: { event: NEvent; cla
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isReady || loading) return
|
if (loading) return
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
@ -87,7 +87,7 @@ export default function ReplyNoteList({ event, className }: { event: NEvent; cla
|
||||||
return () => {
|
return () => {
|
||||||
promise.then((closer) => closer?.())
|
promise.then((closer) => closer?.())
|
||||||
}
|
}
|
||||||
}, [isReady])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateNoteReplyCount(event.id, replies.length)
|
updateNoteReplyCount(event.id, replies.length)
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ export default {
|
||||||
'Using private key login is insecure. It is recommended to use a browser extension for login, such as alby, nostr-keyx or nos2x.',
|
'Using private key login is insecure. It is recommended to use a browser extension for login, such as alby, nostr-keyx or nos2x.',
|
||||||
'Login with Browser Extension': 'Login with Browser Extension',
|
'Login with Browser Extension': 'Login with Browser Extension',
|
||||||
'Login with Bunker': 'Login with Bunker',
|
'Login with Bunker': 'Login with Bunker',
|
||||||
'Login with Private Key': 'Login with Private Key'
|
'Login with Private Key': 'Login with Private Key',
|
||||||
|
'reload notes': 'reload notes'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ export default {
|
||||||
'使用私钥登录是不安全的。建议使用浏览器插件进行登录,例如 alby、nostr-keyx 或 nos2x',
|
'使用私钥登录是不安全的。建议使用浏览器插件进行登录,例如 alby、nostr-keyx 或 nos2x',
|
||||||
'Login with Browser Extension': '浏览器插件登录',
|
'Login with Browser Extension': '浏览器插件登录',
|
||||||
'Login with Bunker': 'Bunker 登录',
|
'Login with Bunker': 'Bunker 登录',
|
||||||
'Login with Private Key': '私钥登录'
|
'Login with Private Key': '私钥登录',
|
||||||
|
'reload notes': '重新加载笔记'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import { Nip07Signer } from './nip-07.signer'
|
||||||
import { NsecSigner } from './nsec.signer'
|
import { NsecSigner } from './nsec.signer'
|
||||||
|
|
||||||
type TNostrContext = {
|
type TNostrContext = {
|
||||||
isReady: boolean
|
|
||||||
pubkey: string | null
|
pubkey: string | null
|
||||||
setPubkey: (pubkey: string) => void
|
setPubkey: (pubkey: string) => void
|
||||||
nsecLogin: (nsec: string) => Promise<string>
|
nsecLogin: (nsec: string) => Promise<string>
|
||||||
|
|
@ -29,7 +28,7 @@ type TNostrContext = {
|
||||||
publish: (draftEvent: TDraftEvent, additionalRelayUrls?: string[]) => Promise<Event>
|
publish: (draftEvent: TDraftEvent, additionalRelayUrls?: string[]) => Promise<Event>
|
||||||
signHttpAuth: (url: string, method: string) => Promise<string>
|
signHttpAuth: (url: string, method: string) => Promise<string>
|
||||||
signEvent: (draftEvent: TDraftEvent) => Promise<Event>
|
signEvent: (draftEvent: TDraftEvent) => Promise<Event>
|
||||||
checkLogin: (cb?: () => void | Promise<void>) => void
|
checkLogin: <T>(cb?: () => T) => Promise<T | void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const NostrContext = createContext<TNostrContext | undefined>(undefined)
|
const NostrContext = createContext<TNostrContext | undefined>(undefined)
|
||||||
|
|
@ -44,7 +43,6 @@ export const useNostr = () => {
|
||||||
|
|
||||||
export function NostrProvider({ children }: { children: React.ReactNode }) {
|
export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
const [isReady, setIsReady] = useState(false)
|
|
||||||
const [pubkey, setPubkey] = useState<string | null>(null)
|
const [pubkey, setPubkey] = useState<string | null>(null)
|
||||||
const [signer, setSigner] = useState<ISigner | null>(null)
|
const [signer, setSigner] = useState<ISigner | null>(null)
|
||||||
const [openLoginDialog, setOpenLoginDialog] = useState(false)
|
const [openLoginDialog, setOpenLoginDialog] = useState(false)
|
||||||
|
|
@ -56,18 +54,17 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [account] = await storage.getAccounts()
|
const [account] = await storage.getAccounts()
|
||||||
if (!account) {
|
if (!account) {
|
||||||
if (isElectron(window) || !window.nostr) {
|
if (isElectron(window) || !window.nostr) {
|
||||||
return setIsReady(true)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// For browser env, attempt to login with nip-07
|
// For browser env, attempt to login with nip-07
|
||||||
const nip07Signer = new Nip07Signer()
|
const nip07Signer = new Nip07Signer()
|
||||||
const pubkey = await nip07Signer.getPublicKey()
|
const pubkey = await nip07Signer.getPublicKey()
|
||||||
if (!pubkey) {
|
if (!pubkey) {
|
||||||
return setIsReady(true)
|
return
|
||||||
}
|
}
|
||||||
setPubkey(pubkey)
|
setPubkey(pubkey)
|
||||||
setSigner(nip07Signer)
|
setSigner(nip07Signer)
|
||||||
setIsReady(true)
|
|
||||||
return await storage.setAccounts([{ pubkey, signerType: 'nip-07' }])
|
return await storage.setAccounts([{ pubkey, signerType: 'nip-07' }])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,24 +78,24 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
if (!pubkey) {
|
if (!pubkey) {
|
||||||
setPubkey(null)
|
setPubkey(null)
|
||||||
await storage.setAccounts([])
|
await storage.setAccounts([])
|
||||||
return setIsReady(true)
|
return
|
||||||
}
|
}
|
||||||
setPubkey(pubkey)
|
setPubkey(pubkey)
|
||||||
setSigner(nsecSigner)
|
setSigner(nsecSigner)
|
||||||
return setIsReady(true)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account.signerType === 'browser-nsec') {
|
if (account.signerType === 'browser-nsec') {
|
||||||
if (!account.nsec) {
|
if (!account.nsec) {
|
||||||
setPubkey(null)
|
setPubkey(null)
|
||||||
await storage.setAccounts([])
|
await storage.setAccounts([])
|
||||||
return setIsReady(true)
|
return
|
||||||
}
|
}
|
||||||
const browserNsecSigner = new BrowserNsecSigner()
|
const browserNsecSigner = new BrowserNsecSigner()
|
||||||
const pubkey = browserNsecSigner.login(account.nsec)
|
const pubkey = browserNsecSigner.login(account.nsec)
|
||||||
setPubkey(pubkey)
|
setPubkey(pubkey)
|
||||||
setSigner(browserNsecSigner)
|
setSigner(browserNsecSigner)
|
||||||
return setIsReady(true)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account.signerType === 'nip-07') {
|
if (account.signerType === 'nip-07') {
|
||||||
|
|
@ -107,33 +104,32 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
if (!pubkey) {
|
if (!pubkey) {
|
||||||
setPubkey(null)
|
setPubkey(null)
|
||||||
await storage.setAccounts([])
|
await storage.setAccounts([])
|
||||||
return setIsReady(true)
|
return
|
||||||
}
|
}
|
||||||
setPubkey(pubkey)
|
setPubkey(pubkey)
|
||||||
setSigner(nip07Signer)
|
setSigner(nip07Signer)
|
||||||
return setIsReady(true)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account.signerType === 'bunker') {
|
if (account.signerType === 'bunker') {
|
||||||
if (!account.bunker || !account.bunkerClientSecretKey) {
|
if (!account.bunker || !account.bunkerClientSecretKey) {
|
||||||
setPubkey(null)
|
setPubkey(null)
|
||||||
await storage.setAccounts([])
|
await storage.setAccounts([])
|
||||||
return setIsReady(true)
|
return
|
||||||
}
|
}
|
||||||
const bunkerSigner = new BunkerSigner(hexToBytes(account.bunkerClientSecretKey))
|
const bunkerSigner = new BunkerSigner(hexToBytes(account.bunkerClientSecretKey))
|
||||||
const pubkey = await bunkerSigner.login(account.bunker)
|
const pubkey = await bunkerSigner.login(account.bunker)
|
||||||
setPubkey(pubkey)
|
setPubkey(pubkey)
|
||||||
setSigner(bunkerSigner)
|
setSigner(bunkerSigner)
|
||||||
return setIsReady(true)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await storage.setAccounts([])
|
await storage.setAccounts([])
|
||||||
return setIsReady(true)
|
return
|
||||||
}
|
}
|
||||||
init().catch(() => {
|
init().catch(() => {
|
||||||
setPubkey(null)
|
setPubkey(null)
|
||||||
storage.setAccounts([])
|
storage.setAccounts([])
|
||||||
setIsReady(true)
|
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|
@ -238,8 +234,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
return 'Nostr ' + btoa(JSON.stringify(event))
|
return 'Nostr ' + btoa(JSON.stringify(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkLogin = async (cb?: () => void) => {
|
const checkLogin = async <T,>(cb?: () => T): Promise<T | void> => {
|
||||||
if (pubkey) {
|
if (signer) {
|
||||||
return cb && cb()
|
return cb && cb()
|
||||||
}
|
}
|
||||||
return setOpenLoginDialog(true)
|
return setOpenLoginDialog(true)
|
||||||
|
|
@ -248,7 +244,6 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<NostrContext.Provider
|
<NostrContext.Provider
|
||||||
value={{
|
value={{
|
||||||
isReady,
|
|
||||||
pubkey,
|
pubkey,
|
||||||
setPubkey,
|
setPubkey,
|
||||||
nsecLogin,
|
nsecLogin,
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ class ClientService extends EventTarget {
|
||||||
signer,
|
signer,
|
||||||
needSort = true
|
needSort = true
|
||||||
}: {
|
}: {
|
||||||
signer?: (evt: TDraftEvent) => Promise<NEvent>
|
signer?: (evt: TDraftEvent) => Promise<NEvent | null>
|
||||||
needSort?: boolean
|
needSort?: boolean
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
|
|
@ -221,12 +221,21 @@ class ClientService extends EventTarget {
|
||||||
if (reason.startsWith('auth-required:')) {
|
if (reason.startsWith('auth-required:')) {
|
||||||
if (!hasAuthed && signer) {
|
if (!hasAuthed && signer) {
|
||||||
relay
|
relay
|
||||||
.auth((authEvt: EventTemplate) => {
|
.auth(async (authEvt: EventTemplate) => {
|
||||||
return signer(authEvt) as Promise<VerifiedEvent>
|
const evt = await signer(authEvt)
|
||||||
|
if (!evt) {
|
||||||
|
throw new Error('sign event failed')
|
||||||
|
}
|
||||||
|
return evt as VerifiedEvent
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
hasAuthed = true
|
hasAuthed = true
|
||||||
|
if (!eosed) {
|
||||||
startSub()
|
startSub()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// ignore
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue