Browse Source

feat: show login dialog when relay requires auth

imwald
codytseng 1 year ago
parent
commit
bed8df06e8
  1. 4
      src/renderer/src/PageManager.tsx
  2. 29
      src/renderer/src/components/NoteList/index.tsx
  3. 6
      src/renderer/src/components/ReplyNoteList/index.tsx
  4. 3
      src/renderer/src/i18n/en.ts
  5. 3
      src/renderer/src/i18n/zh.ts
  6. 33
      src/renderer/src/providers/NostrProvider/index.tsx
  7. 17
      src/renderer/src/services/client.service.ts

4
src/renderer/src/PageManager.tsx

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

29
src/renderer/src/components/NoteList/index.tsx

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

6
src/renderer/src/components/ReplyNoteList/index.tsx

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

3
src/renderer/src/i18n/en.ts

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

3
src/renderer/src/i18n/zh.ts

@ -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': '重新加载笔记'
} }
} }

33
src/renderer/src/providers/NostrProvider/index.tsx

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

17
src/renderer/src/services/client.service.ts

@ -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
startSub() if (!eosed) {
startSub()
}
})
.catch(() => {
// ignore
}) })
} }
} }

Loading…
Cancel
Save