Compare commits
16 Commits
8ec3d66570
...
311d325fd3
Author | SHA1 | Date | |
---|---|---|---|
311d325fd3 | |||
|
074694bfac | ||
db4f605404 | |||
|
662c1c26b3 | ||
|
278d9655f6 | ||
|
501c07d87b | ||
|
307f32bb7b | ||
|
425e7bc653 | ||
|
e82023f105 | ||
|
ba1219b790 | ||
|
2b9617232e | ||
|
5b1147da5d | ||
d2d82b19a4 | |||
|
b27b27f067 | ||
|
f041370c62 | ||
|
7be98978dd |
54
package-lock.json
generated
54
package-lock.json
generated
@ -21,6 +21,7 @@
|
|||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dnd-core": "16.0.1",
|
"dnd-core": "16.0.1",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
|
"idb": "8.0.0",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mui-file-input": "4.0.4",
|
"mui-file-input": "4.0.4",
|
||||||
@ -50,7 +51,8 @@
|
|||||||
"prettier": "3.2.5",
|
"prettier": "3.2.5",
|
||||||
"ts-css-modules-vite-plugin": "1.0.20",
|
"ts-css-modules-vite-plugin": "1.0.20",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.1.4"
|
"vite": "^5.1.4",
|
||||||
|
"vite-tsconfig-paths": "4.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aashutoshrathi/word-wrap": {
|
"node_modules/@aashutoshrathi/word-wrap": {
|
||||||
@ -3554,6 +3556,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/globrex": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/graphemer": {
|
"node_modules/graphemer": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
||||||
@ -3592,6 +3600,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/idb": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw=="
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
|
||||||
@ -4992,6 +5005,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tsconfck": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-CMjc5zMnyAjcS9sPLytrbFmj89st2g+JYtY/c02ug4Q+CZaAtCgbyviI0n1YvjZE/pzoc6FbNsINS13DOL1B9w==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"tsconfck": "bin/tsconfck.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18 || >=20"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tseep": {
|
"node_modules/tseep": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/tseep/-/tseep-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/tseep/-/tseep-1.2.1.tgz",
|
||||||
@ -5197,6 +5230,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite-tsconfig-paths": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"globrex": "^0.1.2",
|
||||||
|
"tsconfck": "^3.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vite": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/web-streams-polyfill": {
|
"node_modules/web-streams-polyfill": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dnd-core": "16.0.1",
|
"dnd-core": "16.0.1",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
|
"idb": "8.0.0",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mui-file-input": "4.0.4",
|
"mui-file-input": "4.0.4",
|
||||||
@ -56,6 +57,7 @@
|
|||||||
"prettier": "3.2.5",
|
"prettier": "3.2.5",
|
||||||
"ts-css-modules-vite-plugin": "1.0.20",
|
"ts-css-modules-vite-plugin": "1.0.20",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.1.4"
|
"vite": "^5.1.4",
|
||||||
|
"vite-tsconfig-paths": "4.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,13 @@ const App = () => {
|
|||||||
const authState = useSelector((state: State) => state.auth)
|
const authState = useSelector((state: State) => state.auth)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (window.location.hostname === '0.0.0.0') {
|
||||||
|
// A change of the host is needed to make library available in windows object
|
||||||
|
// the app can't encrypt files without the crypto library
|
||||||
|
// which is only available on https or localhost
|
||||||
|
window.location.hostname = 'localhost'
|
||||||
|
}
|
||||||
|
|
||||||
generateBunkerDelegatedKey()
|
generateBunkerDelegatedKey()
|
||||||
|
|
||||||
const authController = new AuthController()
|
const authController = new AuthController()
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
} from '../../routes'
|
} from '../../routes'
|
||||||
import {
|
import {
|
||||||
clearAuthToken,
|
clearAuthToken,
|
||||||
|
hexToNpub,
|
||||||
saveNsecBunkerDelegatedKey,
|
saveNsecBunkerDelegatedKey,
|
||||||
shorten
|
shorten
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
@ -60,13 +61,17 @@ export const AppBar = () => {
|
|||||||
setUserAvatar(picture || userRobotImage)
|
setUserAvatar(picture || userRobotImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
setUsername(shorten(display_name || name || '', 7))
|
const npub = authState.usersPubkey
|
||||||
|
? hexToNpub(authState.usersPubkey)
|
||||||
|
: ''
|
||||||
|
|
||||||
|
setUsername(shorten(display_name || name || npub, 7))
|
||||||
} else {
|
} else {
|
||||||
setUserAvatar(userRobotImage || '')
|
setUserAvatar(userRobotImage || '')
|
||||||
setUsername('')
|
setUsername('')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [metadataState, userRobotImage])
|
}, [metadataState, userRobotImage, authState.usersPubkey])
|
||||||
|
|
||||||
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
|
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
setAnchorElUser(event.currentTarget)
|
setAnchorElUser(event.currentTarget)
|
||||||
@ -172,13 +177,13 @@ export const AppBar = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAnchorElUser(null)
|
setAnchorElUser(null)
|
||||||
|
|
||||||
navigate(appPrivateRoutes.relays)
|
navigate(appPrivateRoutes.settings)
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Relays
|
Settings
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Link
|
<Link
|
||||||
to={appPublicRoutes.source}
|
to={appPublicRoutes.source}
|
||||||
|
@ -14,71 +14,130 @@ import { NostrController } from '.'
|
|||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { queryNip05 } from '../utils'
|
import { queryNip05 } from '../utils'
|
||||||
import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'
|
import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'
|
||||||
|
import { EventEmitter } from 'tseep'
|
||||||
|
import { localCache } from '../services'
|
||||||
|
|
||||||
export class MetadataController {
|
export class MetadataController extends EventEmitter {
|
||||||
private nostrController: NostrController
|
private nostrController: NostrController
|
||||||
private specialMetadataRelay = 'wss://purplepag.es'
|
private specialMetadataRelay = 'wss://purplepag.es'
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
super()
|
||||||
this.nostrController = NostrController.getInstance()
|
this.nostrController = NostrController.getInstance()
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEmptyMetadataEvent = (): Event => {
|
/**
|
||||||
return {
|
* Asynchronously checks for more recent metadata events authored by a specific key.
|
||||||
content: '',
|
* If a more recent metadata event is found, it is handled and returned.
|
||||||
created_at: new Date().valueOf(),
|
* If no more recent event is found, the current event is returned.
|
||||||
id: '',
|
* @param hexKey The hexadecimal key of the author to filter metadata events.
|
||||||
kind: 0,
|
* @param currentEvent The current metadata event, if any, to compare with newer events.
|
||||||
pubkey: '',
|
* @returns A promise resolving to the most recent metadata event found, or null if none is found.
|
||||||
sig: '',
|
*/
|
||||||
tags: []
|
private async checkForMoreRecentMetadata(
|
||||||
}
|
hexKey: string,
|
||||||
}
|
currentEvent: Event | null
|
||||||
|
): Promise<Event | null> {
|
||||||
public findMetadata = async (hexKey: string) => {
|
// Define the event filter to only include metadata events authored by the given key
|
||||||
const eventFilter: Filter = {
|
const eventFilter: Filter = {
|
||||||
kinds: [kinds.Metadata],
|
kinds: [kinds.Metadata], // Only metadata events
|
||||||
authors: [hexKey]
|
authors: [hexKey] // Authored by the specified key
|
||||||
}
|
}
|
||||||
|
|
||||||
const pool = new SimplePool()
|
const pool = new SimplePool()
|
||||||
|
|
||||||
|
// Try to get the metadata event from a special relay (wss://purplepag.es)
|
||||||
const metadataEvent = await pool
|
const metadataEvent = await pool
|
||||||
.get([this.specialMetadataRelay], eventFilter)
|
.get([this.specialMetadataRelay], eventFilter)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err)
|
console.error(err) // Log any errors
|
||||||
return null
|
return null // Return null if an error occurs
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// If a valid metadata event is found from the special relay
|
||||||
if (
|
if (
|
||||||
metadataEvent &&
|
metadataEvent &&
|
||||||
validateEvent(metadataEvent) &&
|
validateEvent(metadataEvent) && // Validate the event
|
||||||
verifyEvent(metadataEvent)
|
verifyEvent(metadataEvent) // Verify the event's authenticity
|
||||||
) {
|
) {
|
||||||
|
// If there's no current event or the new metadata event is more recent
|
||||||
|
if (!currentEvent || metadataEvent.created_at > currentEvent.created_at) {
|
||||||
|
// Handle the new metadata event
|
||||||
|
this.handleNewMetadataEvent(metadataEvent)
|
||||||
return metadataEvent
|
return metadataEvent
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no valid metadata event is found from the special relay, get the most popular relays
|
||||||
const mostPopularRelays = await this.nostrController.getMostPopularRelays()
|
const mostPopularRelays = await this.nostrController.getMostPopularRelays()
|
||||||
|
|
||||||
|
// Query the most popular relays for metadata events
|
||||||
const events = await pool
|
const events = await pool
|
||||||
.querySync(mostPopularRelays, eventFilter)
|
.querySync(mostPopularRelays, eventFilter)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err)
|
console.error(err) // Log any errors
|
||||||
|
return null // Return null if an error occurs
|
||||||
return null
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// If events are found from the popular relays
|
||||||
if (events && events.length) {
|
if (events && events.length) {
|
||||||
events.sort((a, b) => b.created_at - a.created_at)
|
events.sort((a, b) => b.created_at - a.created_at) // Sort events by creation date (descending)
|
||||||
|
|
||||||
|
// Iterate through the events
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
if (validateEvent(event) && verifyEvent(event)) {
|
// If the event is valid, authentic, and more recent than the current event
|
||||||
|
if (
|
||||||
|
validateEvent(event) &&
|
||||||
|
verifyEvent(event) &&
|
||||||
|
(!currentEvent || event.created_at > currentEvent.created_at)
|
||||||
|
) {
|
||||||
|
// Handle the new metadata event
|
||||||
|
this.handleNewMetadataEvent(event)
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Mo metadata found.')
|
return currentEvent // Return the current event if no newer event is found
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle new metadata events and emit them to subscribers
|
||||||
|
*/
|
||||||
|
private async handleNewMetadataEvent(event: VerifiedEvent) {
|
||||||
|
// update the event in local cache
|
||||||
|
localCache.addUserMetadata(event)
|
||||||
|
// Emit the event to subscribers.
|
||||||
|
this.emit(event.pubkey, event.kind, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds metadata for a given hexadecimal key.
|
||||||
|
*
|
||||||
|
* @param hexKey - The hexadecimal key to search for metadata.
|
||||||
|
* @returns A promise that resolves to the metadata event.
|
||||||
|
*/
|
||||||
|
public findMetadata = async (hexKey: string): Promise<Event | null> => {
|
||||||
|
// Attempt to retrieve the metadata event from the local cache
|
||||||
|
const cachedMetadataEvent = await localCache.getUserMetadata(hexKey)
|
||||||
|
|
||||||
|
// If cached metadata is found, check its validity
|
||||||
|
if (cachedMetadataEvent) {
|
||||||
|
const oneDayInMS = 24 * 60 * 60 * 1000 // Number of milliseconds in one day
|
||||||
|
|
||||||
|
// Check if the cached metadata is older than one day
|
||||||
|
if (Date.now() - cachedMetadataEvent.cachedAt > oneDayInMS) {
|
||||||
|
// If older than one day, find the metadata from relays in background
|
||||||
|
|
||||||
|
this.checkForMoreRecentMetadata(hexKey, cachedMetadataEvent.event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the cached metadata event
|
||||||
|
return cachedMetadataEvent.event
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no cached metadata is found, retrieve it from relays
|
||||||
|
return this.checkForMoreRecentMetadata(hexKey, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
public findRelayListMetadata = async (hexKey: string) => {
|
public findRelayListMetadata = async (hexKey: string) => {
|
||||||
@ -142,7 +201,7 @@ export class MetadataController {
|
|||||||
throw new Error('No relay list metadata found.')
|
throw new Error('No relay list metadata found.')
|
||||||
}
|
}
|
||||||
|
|
||||||
public extractProfileMetadataContent = (event: VerifiedEvent) => {
|
public extractProfileMetadataContent = (event: Event) => {
|
||||||
try {
|
try {
|
||||||
if (!event.content) return {}
|
if (!event.content) return {}
|
||||||
return JSON.parse(event.content) as ProfileMetadata
|
return JSON.parse(event.content) as ProfileMetadata
|
||||||
@ -175,6 +234,7 @@ export class MetadataController {
|
|||||||
.publishEvent(signedMetadataEvent, [this.specialMetadataRelay])
|
.publishEvent(signedMetadataEvent, [this.specialMetadataRelay])
|
||||||
.then((relays) => {
|
.then((relays) => {
|
||||||
toast.success(`Metadata event published on: ${relays.join('\n')}`)
|
toast.success(`Metadata event published on: ${relays.join('\n')}`)
|
||||||
|
this.handleNewMetadataEvent(signedMetadataEvent as VerifiedEvent)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
toast.error(err.message)
|
toast.error(err.message)
|
||||||
@ -193,6 +253,8 @@ export class MetadataController {
|
|||||||
userRelays.push(...relaySet.write)
|
userRelays.push(...relaySet.write)
|
||||||
} else {
|
} else {
|
||||||
const metadata = await this.findMetadata(hexKey)
|
const metadata = await this.findMetadata(hexKey)
|
||||||
|
if (!metadata) return null
|
||||||
|
|
||||||
const metadataContent = this.extractProfileMetadataContent(metadata)
|
const metadataContent = this.extractProfileMetadataContent(metadata)
|
||||||
|
|
||||||
if (metadataContent?.nip05) {
|
if (metadataContent?.nip05) {
|
||||||
@ -306,4 +368,16 @@ export class MetadataController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public validate = (event: Event) => validateEvent(event) && verifyEvent(event)
|
public validate = (event: Event) => validateEvent(event) && verifyEvent(event)
|
||||||
|
|
||||||
|
public getEmptyMetadataEvent = (): Event => {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
created_at: new Date().valueOf(),
|
||||||
|
id: '',
|
||||||
|
kind: 0,
|
||||||
|
pubkey: '',
|
||||||
|
sig: '',
|
||||||
|
tags: []
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,8 +18,7 @@ import { MetadataController, NostrController } from '../controllers'
|
|||||||
import { LoginMethods } from '../store/auth/types'
|
import { LoginMethods } from '../store/auth/types'
|
||||||
import { setUserRobotImage } from '../store/userRobotImage/action'
|
import { setUserRobotImage } from '../store/userRobotImage/action'
|
||||||
import { State } from '../store/rootReducer'
|
import { State } from '../store/rootReducer'
|
||||||
|
import { Event, kinds } from 'nostr-tools'
|
||||||
const metadataController = new MetadataController()
|
|
||||||
|
|
||||||
export const MainLayout = () => {
|
export const MainLayout = () => {
|
||||||
const dispatch: Dispatch = useDispatch()
|
const dispatch: Dispatch = useDispatch()
|
||||||
@ -27,6 +26,8 @@ export const MainLayout = () => {
|
|||||||
const authState = useSelector((state: State) => state.auth)
|
const authState = useSelector((state: State) => state.auth)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const metadataController = new MetadataController()
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
dispatch(
|
dispatch(
|
||||||
setAuthState({
|
setAuthState({
|
||||||
@ -68,6 +69,20 @@ export const MainLayout = () => {
|
|||||||
nostrController.createNsecBunkerSigner(usersPubkey)
|
nostrController.createNsecBunkerSigner(usersPubkey)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleMetadataEvent = (event: Event) => {
|
||||||
|
dispatch(setMetadataEvent(event))
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataController.on(usersPubkey, (kind: number, event: Event) => {
|
||||||
|
if (kind === kinds.Metadata) {
|
||||||
|
handleMetadataEvent(event)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
metadataController.findMetadata(usersPubkey).then((metadataEvent) => {
|
||||||
|
if (metadataEvent) handleMetadataEvent(metadataEvent)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ import {
|
|||||||
generateEncryptionKey,
|
generateEncryptionKey,
|
||||||
getHash,
|
getHash,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
|
isOnline,
|
||||||
npubToHex,
|
npubToHex,
|
||||||
queryNip05,
|
queryNip05,
|
||||||
sendDM,
|
sendDM,
|
||||||
@ -49,6 +50,7 @@ import type { Identifier, XYCoord } from 'dnd-core'
|
|||||||
import { useDrag, useDrop } from 'react-dnd'
|
import { useDrag, useDrop } from 'react-dnd'
|
||||||
import saveAs from 'file-saver'
|
import saveAs from 'file-saver'
|
||||||
import CopyModal from '../../components/copyModal'
|
import CopyModal from '../../components/copyModal'
|
||||||
|
import { Event, kinds } from 'nostr-tools'
|
||||||
|
|
||||||
export const CreatePage = () => {
|
export const CreatePage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -337,7 +339,7 @@ export const CreatePage = () => {
|
|||||||
|
|
||||||
const blob = new Blob([encryptedArrayBuffer])
|
const blob = new Blob([encryptedArrayBuffer])
|
||||||
|
|
||||||
if (navigator.onLine) {
|
if (await isOnline()) {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setLoadingSpinnerDesc('Uploading zip file to file storage.')
|
setLoadingSpinnerDesc('Uploading zip file to file storage.')
|
||||||
const fileUrl = await uploadToFileStorage(blob, nostrController)
|
const fileUrl = await uploadToFileStorage(blob, nostrController)
|
||||||
@ -517,16 +519,26 @@ const DisplayUser = ({
|
|||||||
if (!(user.pubkey in metadata)) {
|
if (!(user.pubkey in metadata)) {
|
||||||
const metadataController = new MetadataController()
|
const metadataController = new MetadataController()
|
||||||
|
|
||||||
metadataController
|
const handleMetadataEvent = (event: Event) => {
|
||||||
.findMetadata(user.pubkey)
|
|
||||||
.then((metadataEvent) => {
|
|
||||||
const metadataContent =
|
const metadataContent =
|
||||||
metadataController.extractProfileMetadataContent(metadataEvent)
|
metadataController.extractProfileMetadataContent(event)
|
||||||
if (metadataContent)
|
if (metadataContent)
|
||||||
setMetadata((prev) => ({
|
setMetadata((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[user.pubkey]: metadataContent
|
[user.pubkey]: metadataContent
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataController.on(user.pubkey, (kind: number, event: Event) => {
|
||||||
|
if (kind === kinds.Metadata) {
|
||||||
|
handleMetadataEvent(event)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
metadataController
|
||||||
|
.findMetadata(user.pubkey)
|
||||||
|
.then((metadataEvent) => {
|
||||||
|
if (metadataEvent) handleMetadataEvent(metadataEvent)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(
|
console.error(
|
||||||
|
@ -2,7 +2,7 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
|||||||
import EditIcon from '@mui/icons-material/Edit'
|
import EditIcon from '@mui/icons-material/Edit'
|
||||||
import { Box, IconButton, SxProps, Theme, Typography } from '@mui/material'
|
import { Box, IconButton, SxProps, Theme, Typography } from '@mui/material'
|
||||||
import { truncate } from 'lodash'
|
import { truncate } from 'lodash'
|
||||||
import { VerifiedEvent, nip19 } from 'nostr-tools'
|
import { Event, VerifiedEvent, kinds, nip19 } from 'nostr-tools'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||||
@ -75,6 +75,20 @@ export const ProfilePage = () => {
|
|||||||
|
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
const getMetadata = async (pubkey: string) => {
|
const getMetadata = async (pubkey: string) => {
|
||||||
|
const handleMetadataEvent = (event: Event) => {
|
||||||
|
const metadataContent =
|
||||||
|
metadataController.extractProfileMetadataContent(event)
|
||||||
|
if (metadataContent) {
|
||||||
|
setProfileMetadata(metadataContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataController.on(pubkey, (kind: number, event: Event) => {
|
||||||
|
if (kind === kinds.Metadata) {
|
||||||
|
handleMetadataEvent(event)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const metadataEvent = await metadataController
|
const metadataEvent = await metadataController
|
||||||
.findMetadata(pubkey)
|
.findMetadata(pubkey)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -82,13 +96,7 @@ export const ProfilePage = () => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (metadataEvent) {
|
if (metadataEvent) handleMetadataEvent(metadataEvent)
|
||||||
const metadataContent =
|
|
||||||
metadataController.extractProfileMetadataContent(metadataEvent)
|
|
||||||
if (metadataContent) {
|
|
||||||
setProfileMetadata(metadataContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
96
src/pages/settings/Settings.tsx
Normal file
96
src/pages/settings/Settings.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import AccountCircleIcon from '@mui/icons-material/AccountCircle'
|
||||||
|
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'
|
||||||
|
import CachedIcon from '@mui/icons-material/Cached'
|
||||||
|
import RouterIcon from '@mui/icons-material/Router'
|
||||||
|
import { useTheme } from '@mui/material'
|
||||||
|
import List from '@mui/material/List'
|
||||||
|
import ListItemButton from '@mui/material/ListItemButton'
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon'
|
||||||
|
import ListItemText from '@mui/material/ListItemText'
|
||||||
|
import ListSubheader from '@mui/material/ListSubheader'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { appPrivateRoutes, getProfileSettingsRoute } from '../../routes'
|
||||||
|
import { State } from '../../store/rootReducer'
|
||||||
|
|
||||||
|
export const SettingsPage = () => {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
||||||
|
|
||||||
|
const listItem = (label: string, disabled = false) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ListItemText
|
||||||
|
primary={label}
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.primary
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!disabled && (
|
||||||
|
<ArrowForwardIosIcon
|
||||||
|
style={{
|
||||||
|
color: theme.palette.action.active,
|
||||||
|
marginRight: -10
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
marginTop: 2
|
||||||
|
}}
|
||||||
|
subheader={
|
||||||
|
<ListSubheader
|
||||||
|
sx={{
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
borderBottom: '0.5px solid',
|
||||||
|
paddingBottom: 2,
|
||||||
|
paddingTop: 2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItemButton
|
||||||
|
onClick={() => {
|
||||||
|
navigate(getProfileSettingsRoute(usersPubkey!))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<AccountCircleIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
{listItem('Profile')}
|
||||||
|
</ListItemButton>
|
||||||
|
<ListItemButton
|
||||||
|
onClick={() => {
|
||||||
|
navigate(appPrivateRoutes.relays)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<RouterIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
{listItem('Relays')}
|
||||||
|
</ListItemButton>
|
||||||
|
<ListItemButton
|
||||||
|
onClick={() => {
|
||||||
|
navigate(appPrivateRoutes.cacheSettings)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<CachedIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
{listItem('Local Cache')}
|
||||||
|
</ListItemButton>
|
||||||
|
</List>
|
||||||
|
)
|
||||||
|
}
|
96
src/pages/settings/cache/index.tsx
vendored
Normal file
96
src/pages/settings/cache/index.tsx
vendored
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import ClearIcon from '@mui/icons-material/Clear'
|
||||||
|
import InputIcon from '@mui/icons-material/Input'
|
||||||
|
import IosShareIcon from '@mui/icons-material/IosShare'
|
||||||
|
import {
|
||||||
|
List,
|
||||||
|
ListItemButton,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
ListSubheader,
|
||||||
|
useTheme
|
||||||
|
} from '@mui/material'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { localCache } from '../../../services'
|
||||||
|
import { LoadingSpinner } from '../../../components/LoadingSpinner'
|
||||||
|
|
||||||
|
export const CacheSettingsPage = () => {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
|
|
||||||
|
const handleClearData = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
setLoadingSpinnerDesc('Clearing cache data')
|
||||||
|
localCache
|
||||||
|
.clearCacheData()
|
||||||
|
.then(() => {
|
||||||
|
toast.success('cleared cached data')
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('An error occurred in clearing cache data', err)
|
||||||
|
toast.error(err.message || 'An error occurred in clearing cache data')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const listItem = (label: string) => {
|
||||||
|
return (
|
||||||
|
<ListItemText
|
||||||
|
primary={label}
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.primary
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
|
<List
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
marginTop: 2
|
||||||
|
}}
|
||||||
|
subheader={
|
||||||
|
<ListSubheader
|
||||||
|
sx={{
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
borderBottom: '0.5px solid',
|
||||||
|
paddingBottom: 2,
|
||||||
|
paddingTop: 2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cache Setting
|
||||||
|
</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItemButton disabled>
|
||||||
|
<ListItemIcon>
|
||||||
|
<IosShareIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
{listItem('Export (coming soon)')}
|
||||||
|
</ListItemButton>
|
||||||
|
|
||||||
|
<ListItemButton disabled>
|
||||||
|
<ListItemIcon>
|
||||||
|
<InputIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
{listItem('Import (coming soon)')}
|
||||||
|
</ListItemButton>
|
||||||
|
|
||||||
|
<ListItemButton onClick={handleClearData}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<ClearIcon sx={{ color: theme.palette.error.main }} />
|
||||||
|
</ListItemIcon>
|
||||||
|
{listItem('Clear Cache')}
|
||||||
|
</ListItemButton>
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -11,7 +11,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
useTheme
|
useTheme
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { UnsignedEvent, nip19, kinds, VerifiedEvent } from 'nostr-tools'
|
import { UnsignedEvent, nip19, kinds, VerifiedEvent, Event } from 'nostr-tools'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Link, useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
@ -95,6 +95,20 @@ export const ProfileSettingsPage = () => {
|
|||||||
|
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
const getMetadata = async (pubkey: string) => {
|
const getMetadata = async (pubkey: string) => {
|
||||||
|
const handleMetadataEvent = (event: Event) => {
|
||||||
|
const metadataContent =
|
||||||
|
metadataController.extractProfileMetadataContent(event)
|
||||||
|
if (metadataContent) {
|
||||||
|
setProfileMetadata(metadataContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataController.on(pubkey, (kind: number, event: Event) => {
|
||||||
|
if (kind === kinds.Metadata) {
|
||||||
|
handleMetadataEvent(event)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const metadataEvent = await metadataController
|
const metadataEvent = await metadataController
|
||||||
.findMetadata(pubkey)
|
.findMetadata(pubkey)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -102,13 +116,7 @@ export const ProfileSettingsPage = () => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (metadataEvent) {
|
if (metadataEvent) handleMetadataEvent(metadataEvent)
|
||||||
const metadataContent =
|
|
||||||
metadataController.extractProfileMetadataContent(metadataEvent)
|
|
||||||
if (metadataContent) {
|
|
||||||
setProfileMetadata(metadataContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,36 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
||||||
import { Box, List, ListItem, TextField } from '@mui/material'
|
import ElectricBoltIcon from '@mui/icons-material/ElectricBolt'
|
||||||
import RouterIcon from '@mui/icons-material/Router'
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
|
||||||
import styles from './style.module.scss'
|
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'
|
||||||
import Switch from '@mui/material/Switch'
|
|
||||||
import ListItemText from '@mui/material/ListItemText'
|
|
||||||
import Divider from '@mui/material/Divider'
|
|
||||||
import { NostrController } from '../../controllers'
|
|
||||||
import {
|
|
||||||
RelayMap,
|
|
||||||
RelayInfoObject,
|
|
||||||
RelayFee,
|
|
||||||
RelayConnectionState
|
|
||||||
} from '../../types'
|
|
||||||
import LogoutIcon from '@mui/icons-material/Logout'
|
import LogoutIcon from '@mui/icons-material/Logout'
|
||||||
import { useAppSelector, useAppDispatch } from '../../hooks'
|
import RouterIcon from '@mui/icons-material/Router'
|
||||||
import {
|
import { Box, List, ListItem, TextField, Tooltip } from '@mui/material'
|
||||||
compareObjects,
|
import Button from '@mui/material/Button'
|
||||||
shorten,
|
import Divider from '@mui/material/Divider'
|
||||||
hexToNpub,
|
import InputAdornment from '@mui/material/InputAdornment'
|
||||||
capitalizeFirstLetter
|
import ListItemText from '@mui/material/ListItemText'
|
||||||
} from '../../utils'
|
import Switch from '@mui/material/Switch'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { NostrController } from '../../../controllers'
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../../hooks'
|
||||||
import {
|
import {
|
||||||
setRelayMapAction,
|
setRelayMapAction,
|
||||||
setRelayMapUpdatedAction
|
setRelayMapUpdatedAction
|
||||||
} from '../../store/actions'
|
} from '../../../store/actions'
|
||||||
import { toast } from 'react-toastify'
|
import {
|
||||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
|
RelayConnectionState,
|
||||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'
|
RelayFee,
|
||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
RelayInfoObject,
|
||||||
import ElectricBoltIcon from '@mui/icons-material/ElectricBolt'
|
RelayMap
|
||||||
import { Tooltip } from '@mui/material'
|
} from '../../../types'
|
||||||
import InputAdornment from '@mui/material/InputAdornment'
|
import {
|
||||||
import Button from '@mui/material/Button'
|
capitalizeFirstLetter,
|
||||||
|
compareObjects,
|
||||||
|
hexToNpub,
|
||||||
|
shorten
|
||||||
|
} from '../../../utils'
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
|
||||||
export const RelaysPage = () => {
|
export const RelaysPage = () => {
|
||||||
const nostrController = NostrController.getInstance()
|
const nostrController = NostrController.getInstance()
|
@ -1,4 +1,4 @@
|
|||||||
@import '../../colors.scss';
|
@import '../../../colors.scss';
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
margin-top: 25px;
|
margin-top: 25px;
|
@ -20,7 +20,7 @@ import saveAs from 'file-saver'
|
|||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { MuiFileInput } from 'mui-file-input'
|
import { MuiFileInput } from 'mui-file-input'
|
||||||
import { Event, verifyEvent } from 'nostr-tools'
|
import { Event, kinds, verifyEvent } from 'nostr-tools'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
@ -50,7 +50,8 @@ import {
|
|||||||
sendDM,
|
sendDM,
|
||||||
shorten,
|
shorten,
|
||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
uploadToFileStorage
|
uploadToFileStorage,
|
||||||
|
isOnline
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import {
|
import {
|
||||||
@ -433,7 +434,7 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
const blob = new Blob([encryptedArrayBuffer])
|
const blob = new Blob([encryptedArrayBuffer])
|
||||||
|
|
||||||
if (navigator.onLine) {
|
if (await isOnline()) {
|
||||||
setLoadingSpinnerDesc('Uploading zip file to file storage.')
|
setLoadingSpinnerDesc('Uploading zip file to file storage.')
|
||||||
const fileUrl = await uploadToFileStorage(blob, nostrController)
|
const fileUrl = await uploadToFileStorage(blob, nostrController)
|
||||||
.then((url) => {
|
.then((url) => {
|
||||||
@ -846,17 +847,27 @@ const DisplayMeta = ({
|
|||||||
|
|
||||||
hexKeys.forEach((key) => {
|
hexKeys.forEach((key) => {
|
||||||
if (!(key in metadata)) {
|
if (!(key in metadata)) {
|
||||||
metadataController
|
const handleMetadataEvent = (event: Event) => {
|
||||||
.findMetadata(key)
|
|
||||||
.then((metadataEvent) => {
|
|
||||||
const metadataContent =
|
const metadataContent =
|
||||||
metadataController.extractProfileMetadataContent(metadataEvent)
|
metadataController.extractProfileMetadataContent(event)
|
||||||
|
|
||||||
if (metadataContent)
|
if (metadataContent)
|
||||||
setMetadata((prev) => ({
|
setMetadata((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[key]: metadataContent
|
[key]: metadataContent
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataController.on(key, (kind: number, event: Event) => {
|
||||||
|
if (kind === kinds.Metadata) {
|
||||||
|
handleMetadataEvent(event)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
metadataController
|
||||||
|
.findMetadata(key)
|
||||||
|
.then((metadataEvent) => {
|
||||||
|
if (metadataEvent) handleMetadataEvent(metadataEvent)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(`error occurred in finding metadata for: ${key}`, err)
|
console.error(`error occurred in finding metadata for: ${key}`, err)
|
||||||
@ -902,7 +913,9 @@ const DisplayMeta = ({
|
|||||||
<UserComponent
|
<UserComponent
|
||||||
pubkey={submittedBy}
|
pubkey={submittedBy}
|
||||||
name={
|
name={
|
||||||
profile?.display_name || profile?.name || shorten(submittedBy)
|
profile?.display_name ||
|
||||||
|
profile?.name ||
|
||||||
|
shorten(hexToNpub(submittedBy))
|
||||||
}
|
}
|
||||||
image={profile?.picture}
|
image={profile?.picture}
|
||||||
/>
|
/>
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import { MuiFileInput } from 'mui-file-input'
|
import { MuiFileInput } from 'mui-file-input'
|
||||||
import { Event, verifyEvent } from 'nostr-tools'
|
import { Event, kinds, verifyEvent } from 'nostr-tools'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
@ -107,16 +107,26 @@ export const VerifyPage = () => {
|
|||||||
const pubkey = npubToHex(user)!
|
const pubkey = npubToHex(user)!
|
||||||
|
|
||||||
if (!(pubkey in metadata)) {
|
if (!(pubkey in metadata)) {
|
||||||
metadataController
|
const handleMetadataEvent = (event: Event) => {
|
||||||
.findMetadata(pubkey)
|
|
||||||
.then((metadataEvent) => {
|
|
||||||
const metadataContent =
|
const metadataContent =
|
||||||
metadataController.extractProfileMetadataContent(metadataEvent)
|
metadataController.extractProfileMetadataContent(event)
|
||||||
if (metadataContent)
|
if (metadataContent)
|
||||||
setMetadata((prev) => ({
|
setMetadata((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[pubkey]: metadataContent
|
[pubkey]: metadataContent
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataController.on(pubkey, (kind: number, event: Event) => {
|
||||||
|
if (kind === kinds.Metadata) {
|
||||||
|
handleMetadataEvent(event)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
metadataController
|
||||||
|
.findMetadata(pubkey)
|
||||||
|
.then((metadataEvent) => {
|
||||||
|
if (metadataEvent) handleMetadataEvent(metadataEvent)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(
|
console.error(
|
||||||
|
@ -1,21 +1,25 @@
|
|||||||
import { HomePage } from '../pages/home'
|
|
||||||
import { CreatePage } from '../pages/create'
|
import { CreatePage } from '../pages/create'
|
||||||
|
import { HomePage } from '../pages/home'
|
||||||
import { LandingPage } from '../pages/landing/LandingPage'
|
import { LandingPage } from '../pages/landing/LandingPage'
|
||||||
import { Login } from '../pages/login'
|
import { Login } from '../pages/login'
|
||||||
import { ProfilePage } from '../pages/profile'
|
import { ProfilePage } from '../pages/profile'
|
||||||
import { hexToNpub } from '../utils'
|
import { SettingsPage } from '../pages/settings/Settings'
|
||||||
|
import { CacheSettingsPage } from '../pages/settings/cache'
|
||||||
|
import { ProfileSettingsPage } from '../pages/settings/profile'
|
||||||
|
import { RelaysPage } from '../pages/settings/relays'
|
||||||
import { SignPage } from '../pages/sign'
|
import { SignPage } from '../pages/sign'
|
||||||
import { VerifyPage } from '../pages/verify'
|
import { VerifyPage } from '../pages/verify'
|
||||||
import { ProfileSettingsPage } from '../pages/settings/profile'
|
import { hexToNpub } from '../utils'
|
||||||
import { RelaysPage } from '../pages/relays'
|
|
||||||
|
|
||||||
export const appPrivateRoutes = {
|
export const appPrivateRoutes = {
|
||||||
homePage: '/',
|
homePage: '/',
|
||||||
create: '/create',
|
create: '/create',
|
||||||
sign: '/sign',
|
sign: '/sign',
|
||||||
verify: '/verify',
|
verify: '/verify',
|
||||||
|
settings: '/settings',
|
||||||
profileSettings: '/settings/profile/:npub',
|
profileSettings: '/settings/profile/:npub',
|
||||||
relays: '/relays'
|
cacheSettings: '/settings/cache',
|
||||||
|
relays: '/settings/relays'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appPublicRoutes = {
|
export const appPublicRoutes = {
|
||||||
@ -65,10 +69,18 @@ export const privateRoutes = [
|
|||||||
path: appPrivateRoutes.verify,
|
path: appPrivateRoutes.verify,
|
||||||
element: <VerifyPage />
|
element: <VerifyPage />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: appPrivateRoutes.settings,
|
||||||
|
element: <SettingsPage />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: appPrivateRoutes.profileSettings,
|
path: appPrivateRoutes.profileSettings,
|
||||||
element: <ProfileSettingsPage />
|
element: <ProfileSettingsPage />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: appPrivateRoutes.cacheSettings,
|
||||||
|
element: <CacheSettingsPage />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: appPrivateRoutes.relays,
|
path: appPrivateRoutes.relays,
|
||||||
element: <RelaysPage />
|
element: <RelaysPage />
|
||||||
|
63
src/services/cache/index.ts
vendored
Normal file
63
src/services/cache/index.ts
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { IDBPDatabase, openDB } from 'idb'
|
||||||
|
import { Event } from 'nostr-tools'
|
||||||
|
import { CachedMetadataEvent } from '../../types'
|
||||||
|
import { SchemaV1 } from './schema'
|
||||||
|
|
||||||
|
class LocalCache {
|
||||||
|
// Static property to hold the single instance of LocalCache
|
||||||
|
private static instance: LocalCache | null = null
|
||||||
|
private db!: IDBPDatabase<SchemaV1>
|
||||||
|
|
||||||
|
// Private constructor to prevent direct instantiation
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
// Method to initialize the database
|
||||||
|
private async init() {
|
||||||
|
this.db = await openDB<SchemaV1>('sigit-cache', 1, {
|
||||||
|
upgrade(db) {
|
||||||
|
db.createObjectStore('userMetadata', { keyPath: 'event.pubkey' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static method to get the single instance of LocalCache
|
||||||
|
public static async getInstance(): Promise<LocalCache> {
|
||||||
|
// If the instance doesn't exist, create it
|
||||||
|
if (!LocalCache.instance) {
|
||||||
|
LocalCache.instance = new LocalCache()
|
||||||
|
await LocalCache.instance.init()
|
||||||
|
}
|
||||||
|
// Return the single instance of LocalCache
|
||||||
|
return LocalCache.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to add user metadata
|
||||||
|
public async addUserMetadata(event: Event) {
|
||||||
|
await this.db.put('userMetadata', { event, cachedAt: Date.now() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to get user metadata by key
|
||||||
|
public async getUserMetadata(
|
||||||
|
key: string
|
||||||
|
): Promise<CachedMetadataEvent | null> {
|
||||||
|
const data = await this.db.get('userMetadata', key)
|
||||||
|
return data || null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to delete user metadata by key
|
||||||
|
public async deleteUserMetadata(key: string) {
|
||||||
|
await this.db.delete('userMetadata', key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to clear cache data
|
||||||
|
public async clearCacheData() {
|
||||||
|
// Clear the 'userMetadata' store in the IndexedDB database
|
||||||
|
await this.db.clear('userMetadata')
|
||||||
|
|
||||||
|
// Reload the current page to ensure any cached data is reset
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the single instance of LocalCache
|
||||||
|
export const localCache = await LocalCache.getInstance()
|
9
src/services/cache/schema.ts
vendored
Normal file
9
src/services/cache/schema.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { DBSchema } from 'idb'
|
||||||
|
import { CachedMetadataEvent } from '../../types'
|
||||||
|
|
||||||
|
export interface SchemaV1 extends DBSchema {
|
||||||
|
userMetadata: {
|
||||||
|
key: string
|
||||||
|
value: CachedMetadataEvent
|
||||||
|
}
|
||||||
|
}
|
1
src/services/index.ts
Normal file
1
src/services/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './cache'
|
6
src/types/cache.ts
Normal file
6
src/types/cache.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { Event } from 'nostr-tools'
|
||||||
|
|
||||||
|
export interface CachedMetadataEvent {
|
||||||
|
event: Event
|
||||||
|
cachedAt: number
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
|
export * from './cache'
|
||||||
export * from './core'
|
export * from './core'
|
||||||
export * from './nostr'
|
export * from './nostr'
|
||||||
export * from './profile'
|
export * from './profile'
|
||||||
export * from './zip'
|
|
||||||
export * from './relay'
|
export * from './relay'
|
||||||
|
export * from './zip'
|
||||||
|
@ -11,3 +11,41 @@ export const compareObjects = (
|
|||||||
|
|
||||||
return JSON.stringify(obj1) === JSON.stringify(obj2)
|
return JSON.stringify(obj1) === JSON.stringify(obj2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to check if the system is online by making a network request
|
||||||
|
export const isOnline = async () => {
|
||||||
|
// First, check the navigator's online status
|
||||||
|
if (!navigator.onLine) return false // If navigator reports offline, return false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If navigator.onLine is true, it can be false positive.
|
||||||
|
* In other words, navigator.onLine being true does not necessarily mean that there is a working internet connection.
|
||||||
|
* Most implementations seem to only be checking if there's a connected network adapter.
|
||||||
|
* There are a number of circumstances where that's the case, but the internet is still unreachable. For example:
|
||||||
|
* The user is only connected to an internal network
|
||||||
|
* The user is inside a virtual machine, and the virtual network adapter is connected, but the host system is offline
|
||||||
|
* The user uses a VPN which has installed a virtual network adapter that is always connected
|
||||||
|
*
|
||||||
|
* To overcome the above problem we'll have to make a http request
|
||||||
|
*/
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Define a URL to check the online status
|
||||||
|
const url = 'https://www.google.com'
|
||||||
|
|
||||||
|
// Make a HEAD request to the URL with 'no-cors' mode
|
||||||
|
// This mode is used to handle opaque responses which do not expose their content
|
||||||
|
const response = await fetch(url, { method: 'HEAD', mode: 'no-cors' })
|
||||||
|
|
||||||
|
// Check if the response is OK or if the response type is 'opaque'
|
||||||
|
// An 'opaque' response type means that the request succeeded but the response content is not available
|
||||||
|
if (response.ok || response.type === 'opaque') {
|
||||||
|
return true // If the request is successful, return true
|
||||||
|
} else {
|
||||||
|
return false // If the request fails, return false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Catch any errors that occur during the fetch request
|
||||||
|
return false // If an error occurs, return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2022", // Updated to ES2022 for better top-level await support
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"], // Updated to ES2022
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react(), tsconfigPaths()],
|
||||||
|
build: {
|
||||||
|
target: 'ES2022'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user