feat(design): home page new design and functionality #135
@ -5,15 +5,15 @@ commit_message=$(cat "$1")
|
|||||||
|
|
||||||
if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9 \-]+\))?!?: .+$") then
|
if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9 \-]+\))?!?: .+$") then
|
||||||
tput setaf 2;
|
tput setaf 2;
|
||||||
echo -e "${GREEN} ✔ Commit message meets Conventional Commit standards"
|
echo "✔ Commit message meets Conventional Commit standards"
|
||||||
tput sgr0;
|
tput sgr0;
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
tput setaf 1;
|
tput setaf 1;
|
||||||
echo -e "${RED}❌ Commit message does not meet the Conventional Commit standard!"
|
echo "❌ Commit message does not meet the Conventional Commit standard!"
|
||||||
tput sgr0;
|
tput sgr0;
|
||||||
echo "An example of a valid message is:"
|
echo "An example of a valid message is:"
|
||||||
echo " feat(login): add the 'remember me' button"
|
echo " feat(login): add the 'remember me' button"
|
||||||
echo "ℹ More details at: https://www.conventionalcommits.org/en/v1.0.0/#summary"
|
echo "📝 More details at: https://www.conventionalcommits.org/en/v1.0.0/#summary"
|
||||||
exit 1
|
exit 1
|
56
package-lock.json
generated
56
package-lock.json
generated
@ -21,7 +21,7 @@
|
|||||||
"@noble/hashes": "^1.4.0",
|
"@noble/hashes": "^1.4.0",
|
||||||
"@nostr-dev-kit/ndk": "2.5.0",
|
"@nostr-dev-kit/ndk": "2.5.0",
|
||||||
"@reduxjs/toolkit": "2.2.1",
|
"@reduxjs/toolkit": "2.2.1",
|
||||||
"axios": "1.6.7",
|
"axios": "^1.7.4",
|
||||||
"crypto-hash": "3.0.0",
|
"crypto-hash": "3.0.0",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dnd-core": "16.0.1",
|
"dnd-core": "16.0.1",
|
||||||
@ -37,6 +37,7 @@
|
|||||||
"react-dnd": "16.0.1",
|
"react-dnd": "16.0.1",
|
||||||
"react-dnd-html5-backend": "16.0.1",
|
"react-dnd-html5-backend": "16.0.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-dropzone": "^14.2.3",
|
||||||
"react-redux": "9.1.0",
|
"react-redux": "9.1.0",
|
||||||
"react-router-dom": "6.22.1",
|
"react-router-dom": "6.22.1",
|
||||||
"react-toastify": "10.0.4",
|
"react-toastify": "10.0.4",
|
||||||
@ -2680,12 +2681,22 @@
|
|||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
},
|
},
|
||||||
|
"node_modules/attr-accept": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.6.7",
|
"version": "1.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
|
||||||
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
|
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.4",
|
"follow-redirects": "^1.15.6",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
@ -3857,6 +3868,24 @@
|
|||||||
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||||
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/file-selector": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/file-selector/node_modules/tslib": {
|
||||||
|
"version": "2.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||||
|
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
@ -5716,6 +5745,23 @@
|
|||||||
"react": "^18.2.0"
|
"react": "^18.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-dropzone": {
|
||||||
|
"version": "14.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz",
|
||||||
|
"integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"attr-accept": "^2.2.2",
|
||||||
|
"file-selector": "^0.6.0",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.13"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16.8 || 18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "18.2.0",
|
"version": "18.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 32",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 25",
|
||||||
"lint:fix": "eslint . --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint:fix": "eslint . --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"lint:staged": "eslint --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint:staged": "eslint --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"formatter:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"formatter:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
@ -31,7 +31,7 @@
|
|||||||
"@noble/hashes": "^1.4.0",
|
"@noble/hashes": "^1.4.0",
|
||||||
"@nostr-dev-kit/ndk": "2.5.0",
|
"@nostr-dev-kit/ndk": "2.5.0",
|
||||||
"@reduxjs/toolkit": "2.2.1",
|
"@reduxjs/toolkit": "2.2.1",
|
||||||
"axios": "1.6.7",
|
"axios": "^1.7.4",
|
||||||
"crypto-hash": "3.0.0",
|
"crypto-hash": "3.0.0",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dnd-core": "16.0.1",
|
"dnd-core": "16.0.1",
|
||||||
@ -47,6 +47,7 @@
|
|||||||
"react-dnd": "16.0.1",
|
"react-dnd": "16.0.1",
|
||||||
"react-dnd-html5-backend": "16.0.1",
|
"react-dnd-html5-backend": "16.0.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-dropzone": "^14.2.3",
|
||||||
"react-redux": "9.1.0",
|
"react-redux": "9.1.0",
|
||||||
"react-router-dom": "6.22.1",
|
"react-router-dom": "6.22.1",
|
||||||
"react-toastify": "10.0.4",
|
"react-toastify": "10.0.4",
|
||||||
|
@ -56,10 +56,16 @@ a {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-decoration-color: inherit;
|
text-decoration-color: inherit;
|
||||||
transition: ease 0.4s;
|
transition: ease 0.4s;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $primary-light;
|
color: $primary-light;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-decoration-color: inherit;
|
text-decoration-color: inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
@ -6,6 +6,18 @@ interface ContainerProps {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container component with pre-defined width, padding and margins for top level layout.
|
||||||
|
*
|
||||||
|
* **Important:** To avoid conflicts with `defaultStyle` (changing the `width`, `max-width`, `padding-inline`, and/or `margin-inline`) make sure to either:
|
||||||
|
* - When using *className* override, that styles are imported after the actual `Container` component
|
||||||
|
* ```
|
||||||
|
* import { Container } from './components/Container'
|
||||||
|
* import styles from './style.module.scss'
|
||||||
|
* ```
|
||||||
|
* - or add *!important* to imported styles
|
||||||
|
* - or override styles with *CSSProperties* object
|
||||||
|
*/
|
||||||
export const Container = ({
|
export const Container = ({
|
||||||
style = {},
|
style = {},
|
||||||
className = '',
|
className = '',
|
||||||
|
208
src/components/DisplaySigit/index.tsx
Normal file
208
src/components/DisplaySigit/index.tsx
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Meta, ProfileMetadata } from '../../types'
|
||||||
|
import { SigitCardDisplayInfo, SigitStatus } from '../../utils'
|
||||||
|
import { Event, kinds } from 'nostr-tools'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { MetadataController } from '../../controllers'
|
||||||
|
import { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils'
|
||||||
|
import { appPublicRoutes, appPrivateRoutes } from '../../routes'
|
||||||
|
import { Button, Divider, Tooltip } from '@mui/material'
|
||||||
|
import { DisplaySigner } from '../DisplaySigner'
|
||||||
|
import {
|
||||||
|
faArchive,
|
||||||
|
faCalendar,
|
||||||
|
faCopy,
|
||||||
|
faEye,
|
||||||
|
faFile
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { UserAvatar } from '../UserAvatar'
|
||||||
|
import { UserAvatarGroup } from '../UserAvatarGroup'
|
||||||
|
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
import { TooltipChild } from '../TooltipChild'
|
||||||
|
import { getExtensionIconLabel } from '../getExtensionIconLabel'
|
||||||
|
|
||||||
|
type SigitProps = {
|
||||||
|
meta: Meta
|
||||||
|
parsedMeta: SigitCardDisplayInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
createdAt,
|
||||||
|
submittedBy,
|
||||||
|
signers,
|
||||||
|
signedStatus,
|
||||||
|
fileExtensions
|
||||||
|
} = parsedMeta
|
||||||
|
|
||||||
|
const [profiles, setProfiles] = useState<{ [key: string]: ProfileMetadata }>(
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const hexKeys = new Set<string>([
|
||||||
|
...signers.map((signer) => npubToHex(signer)!)
|
||||||
|
])
|
||||||
|
|
||||||
|
if (submittedBy) {
|
||||||
|
hexKeys.add(npubToHex(submittedBy)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataController = new MetadataController()
|
||||||
|
|
||||||
|
const handleMetadataEvent = (key: string) => (event: Event) => {
|
||||||
|
const metadataContent =
|
||||||
|
metadataController.extractProfileMetadataContent(event)
|
||||||
|
|
||||||
|
if (metadataContent) {
|
||||||
|
setProfiles((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: metadataContent
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEventListener =
|
||||||
|
(key: string) => (kind: number, event: Event) => {
|
||||||
|
if (kind === kinds.Metadata) {
|
||||||
|
handleMetadataEvent(key)(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hexKeys.forEach((key) => {
|
||||||
|
if (!(key in profiles)) {
|
||||||
|
metadataController.on(key, handleEventListener(key))
|
||||||
|
|
||||||
|
metadataController
|
||||||
|
.findMetadata(key)
|
||||||
|
.then((metadataEvent) => {
|
||||||
|
if (metadataEvent) handleMetadataEvent(key)(metadataEvent)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`error occurred in finding metadata for: ${key}`, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
hexKeys.forEach((key) => {
|
||||||
|
metadataController.off(key, handleEventListener(key))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [submittedBy, signers, profiles])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.itemWrapper}>
|
||||||
|
<Link
|
||||||
|
to={
|
||||||
|
signedStatus === SigitStatus.Complete
|
||||||
|
? appPublicRoutes.verify
|
||||||
|
: appPrivateRoutes.sign
|
||||||
|
}
|
||||||
|
state={{ meta }}
|
||||||
|
className={styles.insetLink}
|
||||||
|
></Link>
|
||||||
|
<p className={`line-clamp-2 ${styles.title}`}>{title}</p>
|
||||||
|
<div className={styles.users}>
|
||||||
|
{submittedBy &&
|
||||||
|
(function () {
|
||||||
|
const profile = profiles[submittedBy]
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
profile?.display_name ||
|
||||||
|
profile?.name ||
|
||||||
|
shorten(hexToNpub(submittedBy))
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
>
|
||||||
|
<TooltipChild>
|
||||||
|
<UserAvatar pubkey={submittedBy} image={profile?.picture} />
|
||||||
|
</TooltipChild>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
{submittedBy && signers.length ? (
|
||||||
|
<Divider orientation="vertical" flexItem />
|
||||||
|
) : null}
|
||||||
|
<UserAvatarGroup className={styles.signers} max={7}>
|
||||||
|
{signers.map((signer) => {
|
||||||
|
const pubkey = npubToHex(signer)!
|
||||||
|
const profile = profiles[pubkey]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
key={signer}
|
||||||
|
title={
|
||||||
|
profile?.display_name || profile?.name || shorten(pubkey)
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
>
|
||||||
|
<TooltipChild>
|
||||||
|
<DisplaySigner
|
||||||
|
meta={meta}
|
||||||
|
profile={profile}
|
||||||
|
pubkey={pubkey}
|
||||||
|
/>
|
||||||
|
</TooltipChild>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</UserAvatarGroup>
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.details} ${styles.date} ${styles.iconLabel}`}>
|
||||||
|
<FontAwesomeIcon icon={faCalendar} />
|
||||||
|
{createdAt ? formatTimestamp(createdAt) : null}
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.details} ${styles.status}`}>
|
||||||
|
<span className={styles.iconLabel}>
|
||||||
|
<FontAwesomeIcon icon={faEye} /> {signedStatus}
|
||||||
|
</span>
|
||||||
|
{fileExtensions.length > 0 ? (
|
||||||
|
<span className={styles.iconLabel}>
|
||||||
|
{fileExtensions.length > 1 ? (
|
||||||
|
<>
|
||||||
|
<FontAwesomeIcon icon={faFile} /> Multiple File Types
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
getExtensionIconLabel(fileExtensions[0])
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className={styles.itemActions}>
|
||||||
|
<Tooltip title="Duplicate" arrow placement="top" disableInteractive>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
color: 'var(--primary-main)',
|
||||||
|
minWidth: '34px',
|
||||||
|
padding: '10px'
|
||||||
|
}}
|
||||||
|
variant={'text'}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCopy} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Archive" arrow placement="top" disableInteractive>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
color: 'var(--primary-main)',
|
||||||
|
minWidth: '34px',
|
||||||
|
padding: '10px'
|
||||||
|
}}
|
||||||
|
variant={'text'}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faArchive} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
134
src/components/DisplaySigit/style.module.scss
Normal file
134
src/components/DisplaySigit/style.module.scss
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
|
.itemWrapper {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: $overlay-background-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
padding: 15px;
|
||||||
|
gap: 15px;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&:only-child {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
transition: opacity ease 0.2s;
|
||||||
|
opacity: 0;
|
||||||
|
width: 4px;
|
||||||
|
background-color: $primary-main;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-within {
|
||||||
|
&::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemActions {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.insetLink {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemActions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
transition: ease 0.2s;
|
||||||
|
transform: translateX(100%);
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
flex-direction: column;
|
||||||
|
background: $overlay-background-color;
|
||||||
|
border-left: solid 1px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-within {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: none) {
|
||||||
|
border-top: solid 1px rgba(0, 0, 0, 0.1);
|
||||||
|
padding-top: 10px;
|
||||||
|
margin-inline: -15px;
|
||||||
|
margin-bottom: -15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 20px;
|
||||||
|
color: $text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.users {
|
||||||
|
margin-top: auto;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
grid-gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signers {
|
||||||
|
padding: 0 0 0 10px;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
transition: margin ease 0.2s;
|
||||||
|
margin: 0 0 0 -10px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
&:first-child {
|
||||||
|
margin-left: -10px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:hover,
|
||||||
|
> *:focus-within {
|
||||||
|
margin: 0 15px 0 5px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
color: rgba(0, 0, 0, 0.3);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconLabel {
|
||||||
|
display: flex;
|
||||||
|
grid-gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
display: flex;
|
||||||
|
grid-gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.itemWrapper:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
78
src/components/DisplaySigner/index.tsx
Normal file
78
src/components/DisplaySigner/index.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { Badge } from '@mui/material'
|
||||||
|
import { Event, verifyEvent } from 'nostr-tools'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { Meta, ProfileMetadata } from '../../types'
|
||||||
|
import { hexToNpub, parseJson } from '../../utils'
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
import { UserAvatar } from '../UserAvatar'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faCheck, faExclamation } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
enum SignStatus {
|
||||||
|
Signed = 'Signed',
|
||||||
|
Pending = 'Pending',
|
||||||
|
Invalid = 'Invalid Sign'
|
||||||
|
}
|
||||||
|
|
||||||
|
type DisplaySignerProps = {
|
||||||
|
meta: Meta
|
||||||
|
profile: ProfileMetadata
|
||||||
|
pubkey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisplaySigner = ({
|
||||||
|
meta,
|
||||||
|
profile,
|
||||||
|
pubkey
|
||||||
|
}: DisplaySignerProps) => {
|
||||||
|
const [signStatus, setSignedStatus] = useState<SignStatus>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!meta) return
|
||||||
|
|
||||||
|
const updateSignStatus = async () => {
|
||||||
|
const npub = hexToNpub(pubkey)
|
||||||
|
if (npub in meta.docSignatures) {
|
||||||
|
parseJson<Event>(meta.docSignatures[npub])
|
||||||
|
.then((event) => {
|
||||||
|
const isValidSignature = verifyEvent(event)
|
||||||
|
if (isValidSignature) {
|
||||||
|
setSignedStatus(SignStatus.Signed)
|
||||||
|
} else {
|
||||||
|
setSignedStatus(SignStatus.Invalid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(`err in parsing the docSignatures for ${npub}:>> `, err)
|
||||||
|
setSignedStatus(SignStatus.Invalid)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setSignedStatus(SignStatus.Pending)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSignStatus()
|
||||||
|
}, [meta, pubkey])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
className={styles.signer}
|
||||||
|
overlap="circular"
|
||||||
|
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||||
|
badgeContent={
|
||||||
|
signStatus !== SignStatus.Pending && (
|
||||||
|
<div className={styles.statusBadge}>
|
||||||
|
{signStatus === SignStatus.Signed && (
|
||||||
|
<FontAwesomeIcon icon={faCheck} />
|
||||||
|
)}
|
||||||
|
{signStatus === SignStatus.Invalid && (
|
||||||
|
<FontAwesomeIcon icon={faExclamation} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<UserAvatar pubkey={pubkey} image={profile?.picture} />
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
23
src/components/DisplaySigner/style.module.scss
Normal file
23
src/components/DisplaySigner/style.module.scss
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
|
.statusBadge {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
font-size: 10px;
|
||||||
|
|
||||||
|
background-color: $primary-main;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signer {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
@ -30,6 +30,7 @@
|
|||||||
border: 1px solid rgba(0, 0, 0, 0.137);
|
border: 1px solid rgba(0, 0, 0, 0.137);
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
-webkit-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
@ -42,13 +43,13 @@
|
|||||||
border-color: #01aaad79;
|
border-color: #01aaad79;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdfImageWrapper {
|
.pdfImageWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
-webkit-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
&.drawing {
|
&.drawing {
|
||||||
@ -94,7 +95,7 @@
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border: 1px solid rgb(160, 160, 160);
|
border: 1px solid rgb(160, 160, 160);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
color: #E74C3C;
|
color: #e74c3c;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
113
src/components/Select/index.tsx
Normal file
113
src/components/Select/index.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
MenuItem,
|
||||||
|
Select as SelectMui,
|
||||||
|
SelectChangeEvent,
|
||||||
|
styled,
|
||||||
|
SelectProps as SelectMuiProps,
|
||||||
|
MenuItemProps
|
||||||
|
} from '@mui/material'
|
||||||
|
|
||||||
|
const SelectCustomized = styled(SelectMui)<SelectMuiProps>(() => ({
|
||||||
|
backgroundColor: 'var(--primary-main)',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '500',
|
||||||
|
color: 'white',
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: 'var(--primary-light)'
|
||||||
|
},
|
||||||
|
'& .MuiSelect-select:focus': {
|
||||||
|
backgroundColor: 'var(--primary-light)'
|
||||||
|
},
|
||||||
|
'& .MuiSvgIcon-root': {
|
||||||
|
color: 'white'
|
||||||
|
},
|
||||||
|
'& .MuiOutlinedInput-notchedOutline': {
|
||||||
|
border: 'none'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const MenuItemCustomized = styled(MenuItem)<MenuItemProps>(() => ({
|
||||||
|
marginInline: '5px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
'&:hover': {
|
||||||
|
background: 'var(--primary-light)',
|
||||||
|
color: 'white'
|
||||||
|
},
|
||||||
|
'&.Mui-selected': {
|
||||||
|
background: 'var(--primary-dark)',
|
||||||
|
color: 'white'
|
||||||
|
},
|
||||||
|
'&.Mui-selected:hover': {
|
||||||
|
background: 'var(--primary-light)'
|
||||||
|
},
|
||||||
|
'&.Mui-selected.Mui-focusVisible': {
|
||||||
|
background: 'var(--primary-light)',
|
||||||
|
color: 'white'
|
||||||
|
},
|
||||||
|
'&.Mui-focusVisible': {
|
||||||
|
background: 'var(--primary-light)',
|
||||||
|
color: 'white'
|
||||||
|
},
|
||||||
|
'& + *': {
|
||||||
|
marginTop: '5px'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
interface SelectItemProps<T extends string> {
|
||||||
|
value: T
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectProps<T extends string> {
|
||||||
|
value: T
|
||||||
|
setValue: React.Dispatch<React.SetStateAction<T>>
|
||||||
|
options: SelectItemProps<T>[]
|
||||||
|
name?: string
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Select<T extends string>({
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
|
options,
|
||||||
|
name,
|
||||||
|
id
|
||||||
|
}: SelectProps<T>) {
|
||||||
|
const handleChange = (event: SelectChangeEvent<unknown>) => {
|
||||||
|
setValue(event.target.value as T)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl>
|
||||||
|
<SelectCustomized
|
||||||
|
id={id}
|
||||||
|
name={name}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
MenuProps={{
|
||||||
|
MenuListProps: {
|
||||||
|
sx: {
|
||||||
|
paddingBlock: '5px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PaperProps: {
|
||||||
|
sx: {
|
||||||
|
boxShadow: '0 0 4px 0 rgb(0, 0, 0, 0.1)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{options.map((o) => {
|
||||||
|
return (
|
||||||
|
<MenuItemCustomized key={o.label} value={o.value as string}>
|
||||||
|
{o.label}
|
||||||
|
</MenuItemCustomized>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</SelectCustomized>
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
}
|
16
src/components/TooltipChild.tsx
Normal file
16
src/components/TooltipChild.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { forwardRef, PropsWithChildren } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper wrapper for custom child components when using `@mui/material/tooltips`.
|
||||||
|
* Mui Tooltip works out-the-box with other `@mui` components but when using custom they require ref.
|
||||||
|
* @source https://mui.com/material-ui/react-tooltip/#custom-child-element
|
||||||
|
*/
|
||||||
|
export const TooltipChild = forwardRef<HTMLSpanElement, PropsWithChildren>(
|
||||||
|
({ children, ...rest }, ref) => {
|
||||||
|
return (
|
||||||
|
<span ref={ref} {...rest}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
@ -1,12 +1,11 @@
|
|||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import { getProfileRoute } from '../../routes'
|
import { getProfileRoute } from '../../routes'
|
||||||
|
|
||||||
import styles from './styles.module.scss'
|
import styles from './styles.module.scss'
|
||||||
import React from 'react'
|
|
||||||
import { AvatarIconButton } from '../UserAvatarIconButton'
|
import { AvatarIconButton } from '../UserAvatarIconButton'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
interface UserAvatarProps {
|
interface UserAvatarProps {
|
||||||
name: string
|
name?: string
|
||||||
pubkey: string
|
pubkey: string
|
||||||
image?: string
|
image?: string
|
||||||
}
|
}
|
||||||
@ -16,27 +15,22 @@ interface UserAvatarProps {
|
|||||||
* Clicking will navigate to the user's profile.
|
* Clicking will navigate to the user's profile.
|
||||||
*/
|
*/
|
||||||
export const UserAvatar = ({ pubkey, name, image }: UserAvatarProps) => {
|
export const UserAvatar = ({ pubkey, name, image }: UserAvatarProps) => {
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
navigate(getProfileRoute(pubkey))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<Link
|
||||||
|
to={getProfileRoute(pubkey)}
|
||||||
|
className={styles.container}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
<AvatarIconButton
|
<AvatarIconButton
|
||||||
src={image}
|
src={image}
|
||||||
hexKey={pubkey}
|
hexKey={pubkey}
|
||||||
aria-label={`account of user ${name}`}
|
aria-label={`account of user ${name || pubkey}`}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={handleClick}
|
sx={{
|
||||||
|
padding: 0
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{name ? (
|
{name ? <label className={styles.username}>{name}</label> : null}
|
||||||
<label onClick={handleClick} className={styles.username}>
|
</Link>
|
||||||
{name}
|
|
||||||
</label>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
flex-grow: 1;
|
// flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
|
38
src/components/UserAvatarGroup/index.tsx
Normal file
38
src/components/UserAvatarGroup/index.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Children, PropsWithChildren } from 'react'
|
||||||
|
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
|
||||||
|
interface UserAvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
max: number
|
||||||
|
renderSurplus?: ((surplus: number) => React.ReactNode) | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSurplus = (surplus: number) => {
|
||||||
|
return <span className={styles.icon}>+{surplus}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders children with the `max` limit (including surplus if available).
|
||||||
|
* The children are wrapped with a `div` (accepts standard `HTMLDivElement` attributes)
|
||||||
|
* @param max The maximum number of children rendered in a div.
|
||||||
|
* @param renderSurplus Custom render for surplus children (accepts surplus number).
|
||||||
|
*/
|
||||||
|
export const UserAvatarGroup = ({
|
||||||
|
max,
|
||||||
|
renderSurplus = defaultSurplus,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}: PropsWithChildren<UserAvatarGroupProps>) => {
|
||||||
|
const total = Children.count(children)
|
||||||
|
const surplus = total - max + 1
|
||||||
|
|
||||||
|
const childrenArray = Children.toArray(children)
|
||||||
|
return (
|
||||||
|
<div {...rest}>
|
||||||
|
{surplus > 1
|
||||||
|
? childrenArray.slice(0, surplus * -1).map((c) => c)
|
||||||
|
: children}
|
||||||
|
{surplus > 1 && renderSurplus(surplus)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
19
src/components/UserAvatarGroup/style.module.scss
Normal file
19
src/components/UserAvatarGroup/style.module.scss
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-width: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
background: white;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
border: solid 2px $primary-main;
|
||||||
|
}
|
@ -2,6 +2,6 @@
|
|||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border-width: 3px;
|
border-width: 2px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
78
src/components/getExtensionIconLabel.tsx
Normal file
78
src/components/getExtensionIconLabel.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
faFilePdf,
|
||||||
|
faFileExcel,
|
||||||
|
faFileWord,
|
||||||
|
faFilePowerpoint,
|
||||||
|
faFileZipper,
|
||||||
|
faFileCsv,
|
||||||
|
faFileLines,
|
||||||
|
faFileImage,
|
||||||
|
faFile,
|
||||||
|
IconDefinition
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
|
||||||
|
export const getExtensionIconLabel = (extension: string) => {
|
||||||
|
let icon: IconDefinition
|
||||||
|
switch (extension.toLowerCase()) {
|
||||||
|
case 'pdf':
|
||||||
|
icon = faFilePdf
|
||||||
|
break
|
||||||
|
case 'json':
|
||||||
|
icon = faFilePdf
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'xlsx':
|
||||||
|
case 'xls':
|
||||||
|
case 'xlsb':
|
||||||
|
case 'xlsm':
|
||||||
|
icon = faFileExcel
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'doc':
|
||||||
|
case 'docx':
|
||||||
|
icon = faFileWord
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'ppt':
|
||||||
|
case 'pptx':
|
||||||
|
icon = faFilePowerpoint
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'zip':
|
||||||
|
case '7z':
|
||||||
|
case 'rar':
|
||||||
|
case 'tar':
|
||||||
|
case 'gz':
|
||||||
|
icon = faFileZipper
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'csv':
|
||||||
|
icon = faFileCsv
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'txt':
|
||||||
|
icon = faFileLines
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'png':
|
||||||
|
case 'jpg':
|
||||||
|
case 'jpeg':
|
||||||
|
case 'gif':
|
||||||
|
case 'svg':
|
||||||
|
case 'bmp':
|
||||||
|
case 'ico':
|
||||||
|
icon = faFileImage
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
icon = faFile
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FontAwesomeIcon icon={icon} /> {extension.toUpperCase()}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -12,7 +12,8 @@ import {
|
|||||||
getAuthToken,
|
getAuthToken,
|
||||||
getVisitedLink,
|
getVisitedLink,
|
||||||
saveAuthToken,
|
saveAuthToken,
|
||||||
compareObjects
|
compareObjects,
|
||||||
|
unixNow
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { appPrivateRoutes } from '../routes'
|
import { appPrivateRoutes } from '../routes'
|
||||||
import { SignedEvent } from '../types'
|
import { SignedEvent } from '../types'
|
||||||
@ -54,7 +55,7 @@ export class AuthController {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Nostr uses unix timestamps
|
// Nostr uses unix timestamps
|
||||||
const timestamp = Math.floor(Date.now() / 1000)
|
const timestamp = unixNow()
|
||||||
const { hostname } = window.location
|
const { hostname } = window.location
|
||||||
|
|
||||||
const authEvent: EventTemplate = {
|
const authEvent: EventTemplate = {
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
import { NostrJoiningBlock, ProfileMetadata, RelaySet } from '../types'
|
import { NostrJoiningBlock, ProfileMetadata, RelaySet } from '../types'
|
||||||
import { NostrController } from '.'
|
import { NostrController } from '.'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { queryNip05 } from '../utils'
|
import { queryNip05, unixNow } 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 { EventEmitter } from 'tseep'
|
||||||
import { localCache } from '../services'
|
import { localCache } from '../services'
|
||||||
@ -194,7 +194,7 @@ export class MetadataController extends EventEmitter {
|
|||||||
let signedMetadataEvent = event
|
let signedMetadataEvent = event
|
||||||
|
|
||||||
if (event.sig.length < 1) {
|
if (event.sig.length < 1) {
|
||||||
const timestamp = Math.floor(Date.now() / 1000)
|
const timestamp = unixNow()
|
||||||
|
|
||||||
// Metadata event to publish to the wss://purplepag.es relay
|
// Metadata event to publish to the wss://purplepag.es relay
|
||||||
const newMetadataEvent: Event = {
|
const newMetadataEvent: Event = {
|
||||||
@ -265,7 +265,7 @@ export class MetadataController extends EventEmitter {
|
|||||||
// initialize job request
|
// initialize job request
|
||||||
const jobEventTemplate: EventTemplate = {
|
const jobEventTemplate: EventTemplate = {
|
||||||
content: '',
|
content: '',
|
||||||
created_at: Math.round(Date.now() / 1000),
|
created_at: unixNow(),
|
||||||
kind: 68001,
|
kind: 68001,
|
||||||
tags: [
|
tags: [
|
||||||
['i', `${created_at * 1000}`],
|
['i', `${created_at * 1000}`],
|
||||||
|
@ -42,6 +42,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
compareObjects,
|
compareObjects,
|
||||||
getNsecBunkerDelegatedKey,
|
getNsecBunkerDelegatedKey,
|
||||||
|
unixNow,
|
||||||
verifySignedEvent
|
verifySignedEvent
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { getDefaultRelayMap } from '../utils/relays.ts'
|
import { getDefaultRelayMap } from '../utils/relays.ts'
|
||||||
@ -244,7 +245,7 @@ export class NostrController extends EventEmitter {
|
|||||||
|
|
||||||
if (!firstSuccessfulPublish) {
|
if (!firstSuccessfulPublish) {
|
||||||
// If no publish was successful, collect the reasons for failures
|
// If no publish was successful, collect the reasons for failures
|
||||||
const failedPublishes: any[] = []
|
const failedPublishes: unknown[] = []
|
||||||
const fallbackRejectionReason =
|
const fallbackRejectionReason =
|
||||||
'Attempt to publish an event has been rejected with unknown reason.'
|
'Attempt to publish an event has been rejected with unknown reason.'
|
||||||
|
|
||||||
@ -504,7 +505,9 @@ export class NostrController extends EventEmitter {
|
|||||||
} else if (loginMethod === LoginMethods.extension) {
|
} else if (loginMethod === LoginMethods.extension) {
|
||||||
const nostr = this.getNostrObject()
|
const nostr = this.getNostrObject()
|
||||||
|
|
||||||
return (await nostr.signEvent(event as NostrEvent).catch((err: any) => {
|
return (await nostr
|
||||||
|
.signEvent(event as NostrEvent)
|
||||||
|
.catch((err: unknown) => {
|
||||||
console.log('Error while signing event: ', err)
|
console.log('Error while signing event: ', err)
|
||||||
|
|
||||||
throw err
|
throw err
|
||||||
@ -625,8 +628,12 @@ export class NostrController extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
capturePublicKey = async (): Promise<string> => {
|
capturePublicKey = async (): Promise<string> => {
|
||||||
const nostr = this.getNostrObject()
|
const nostr = this.getNostrObject()
|
||||||
const pubKey = await nostr.getPublicKey().catch((err: any) => {
|
const pubKey = await nostr.getPublicKey().catch((err: unknown) => {
|
||||||
|
if (err instanceof Error) {
|
||||||
return Promise.reject(err.message)
|
return Promise.reject(err.message)
|
||||||
|
} else {
|
||||||
|
return Promise.reject(JSON.stringify(err))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!pubKey) {
|
if (!pubKey) {
|
||||||
@ -708,7 +715,7 @@ export class NostrController extends EventEmitter {
|
|||||||
npub: string,
|
npub: string,
|
||||||
extraRelaysToPublish?: string[]
|
extraRelaysToPublish?: string[]
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
const timestamp = Math.floor(Date.now() / 1000)
|
const timestamp = unixNow()
|
||||||
const relayURIs = Object.keys(relayMap)
|
const relayURIs = Object.keys(relayMap)
|
||||||
|
|
||||||
// More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md
|
// More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md
|
||||||
@ -810,7 +817,7 @@ export class NostrController extends EventEmitter {
|
|||||||
// initialize job request
|
// initialize job request
|
||||||
const jobEventTemplate: EventTemplate = {
|
const jobEventTemplate: EventTemplate = {
|
||||||
content: '',
|
content: '',
|
||||||
created_at: Math.round(Date.now() / 1000),
|
created_at: unixNow(),
|
||||||
kind: 68001,
|
kind: 68001,
|
||||||
tags: [
|
tags: [
|
||||||
['i', `${JSON.stringify(relayURIs)}`],
|
['i', `${JSON.stringify(relayURIs)}`],
|
||||||
|
148
src/hooks/useSigitMeta.tsx
Normal file
148
src/hooks/useSigitMeta.tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { CreateSignatureEventContent, Meta } from '../types'
|
||||||
|
import { Mark } from '../types/mark'
|
||||||
|
import {
|
||||||
|
fromUnixTimestamp,
|
||||||
|
parseCreateSignatureEvent,
|
||||||
|
parseCreateSignatureEventContent,
|
||||||
|
SigitMetaParseError,
|
||||||
|
SigitStatus,
|
||||||
|
SignStatus
|
||||||
|
} from '../utils'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { verifyEvent } from 'nostr-tools'
|
||||||
|
import { Event } from 'nostr-tools'
|
||||||
|
|
||||||
|
interface FlatMeta extends Meta, CreateSignatureEventContent, Partial<Event> {
|
||||||
|
// Validated create signature event
|
||||||
|
isValid: boolean
|
||||||
|
|
||||||
|
// Calculated status fields
|
||||||
|
signedStatus: SigitStatus
|
||||||
|
signersStatus: {
|
||||||
|
[signer: `npub1${string}`]: SignStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom use hook for parsing the Sigit Meta
|
||||||
|
* @param meta Sigit Meta
|
||||||
|
* @returns flattened Meta object with calculated signed status
|
||||||
|
*/
|
||||||
|
export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||||
|
const [isValid, setIsValid] = useState(false)
|
||||||
|
const [kind, setKind] = useState<number>()
|
||||||
|
const [tags, setTags] = useState<string[][]>()
|
||||||
|
const [created_at, setCreatedAt] = useState<number>()
|
||||||
|
const [pubkey, setPubkey] = useState<string>() // submittedBy, pubkey from nostr event
|
||||||
|
const [id, setId] = useState<string>()
|
||||||
|
const [sig, setSig] = useState<string>()
|
||||||
|
|
||||||
|
const [signers, setSigners] = useState<`npub1${string}`[]>([])
|
||||||
|
const [viewers, setViewers] = useState<`npub1${string}`[]>([])
|
||||||
|
const [fileHashes, setFileHashes] = useState<{
|
||||||
|
[user: `npub1${string}`]: string
|
||||||
|
}>({})
|
||||||
|
const [markConfig, setMarkConfig] = useState<Mark[]>([])
|
||||||
|
const [title, setTitle] = useState<string>('')
|
||||||
|
const [zipUrl, setZipUrl] = useState<string>('')
|
||||||
|
|
||||||
|
const [signedStatus, setSignedStatus] = useState<SigitStatus>(
|
||||||
|
SigitStatus.Partial
|
||||||
|
)
|
||||||
|
const [signersStatus, setSignersStatus] = useState<{
|
||||||
|
[signer: `npub1${string}`]: SignStatus
|
||||||
|
}>({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!meta) return
|
||||||
|
;(async function () {
|
||||||
|
try {
|
||||||
|
const createSignatureEvent = await parseCreateSignatureEvent(
|
||||||
|
meta.createSignature
|
||||||
|
)
|
||||||
|
|
||||||
|
const { kind, tags, created_at, pubkey, id, sig, content } =
|
||||||
|
createSignatureEvent
|
||||||
|
|
||||||
|
setIsValid(verifyEvent(createSignatureEvent))
|
||||||
|
setKind(kind)
|
||||||
|
setTags(tags)
|
||||||
|
// created_at in nostr events are stored in seconds
|
||||||
|
|||||||
|
setCreatedAt(fromUnixTimestamp(created_at))
|
||||||
|
setPubkey(pubkey)
|
||||||
|
setId(id)
|
||||||
|
setSig(sig)
|
||||||
|
|
||||||
|
const { title, signers, viewers, fileHashes, markConfig, zipUrl } =
|
||||||
|
await parseCreateSignatureEventContent(content)
|
||||||
|
|
||||||
|
setTitle(title)
|
||||||
|
setSigners(signers)
|
||||||
|
setViewers(viewers)
|
||||||
|
setFileHashes(fileHashes)
|
||||||
|
setMarkConfig(markConfig)
|
||||||
|
setZipUrl(zipUrl)
|
||||||
|
|
||||||
|
// Parse each signature event and set signer status
|
||||||
|
for (const npub in meta.docSignatures) {
|
||||||
|
try {
|
||||||
|
const event = await parseCreateSignatureEvent(
|
||||||
|
meta.docSignatures[npub as `npub1${string}`]
|
||||||
|
)
|
||||||
|
const isValidSignature = verifyEvent(event)
|
||||||
|
setSignersStatus((prev) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[npub]: isValidSignature
|
||||||
|
? SignStatus.Signed
|
||||||
|
: SignStatus.Invalid
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
setSignersStatus((prev) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[npub]: SignStatus.Invalid
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
|
||||||
|
const isCompletelySigned = signers.every((signer) =>
|
||||||
|
signedBy.includes(signer)
|
||||||
|
)
|
||||||
|
setSignedStatus(
|
||||||
|
isCompletelySigned ? SigitStatus.Complete : SigitStatus.Partial
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof SigitMetaParseError) {
|
||||||
|
toast.error(error.message)
|
||||||
|
}
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}, [meta])
|
||||||
|
|
||||||
|
return {
|
||||||
|
modifiedAt: meta.modifiedAt,
|
||||||
|
createSignature: meta.createSignature,
|
||||||
|
docSignatures: meta.docSignatures,
|
||||||
|
keys: meta.keys,
|
||||||
|
isValid,
|
||||||
|
kind,
|
||||||
|
tags,
|
||||||
|
created_at,
|
||||||
|
pubkey,
|
||||||
|
id,
|
||||||
|
sig,
|
||||||
|
signers,
|
||||||
|
viewers,
|
||||||
|
fileHashes,
|
||||||
|
markConfig,
|
||||||
|
title,
|
||||||
|
zipUrl,
|
||||||
|
signedStatus,
|
||||||
|
signersStatus
|
||||||
|
}
|
||||||
|
}
|
@ -101,6 +101,15 @@ button:disabled {
|
|||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.line-clamp-2 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.profile-image {
|
.profile-image {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
@ -5,7 +5,7 @@ $default-modal-padding: 15px 25px;
|
|||||||
.modal {
|
.modal {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 50%;
|
left: calc(50% - 10px);
|
||||||
transform: translate(-50%, 0);
|
transform: translate(-50%, 0);
|
||||||
|
|
||||||
background-color: $overlay-background-color;
|
background-color: $overlay-background-color;
|
||||||
@ -16,6 +16,8 @@ $default-modal-padding: 15px 25px;
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
|
margin: 25px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
@ -24,7 +24,7 @@ import JSZip from 'jszip'
|
|||||||
import { MuiFileInput } from 'mui-file-input'
|
import { MuiFileInput } from 'mui-file-input'
|
||||||
import { Event, kinds } from 'nostr-tools'
|
import { Event, kinds } from 'nostr-tools'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { DndProvider, DragSourceMonitor, useDrag, useDrop } from 'react-dnd'
|
import { DndProvider, useDrag, useDrop } from 'react-dnd'
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend'
|
import { HTML5Backend } from 'react-dnd-html5-backend'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
@ -50,7 +50,7 @@ import {
|
|||||||
getHash,
|
getHash,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
isOnline,
|
isOnline,
|
||||||
now,
|
unixNow,
|
||||||
npubToHex,
|
npubToHex,
|
||||||
queryNip05,
|
queryNip05,
|
||||||
sendNotification,
|
sendNotification,
|
||||||
@ -68,7 +68,7 @@ import { Mark } from '../../types/mark.ts'
|
|||||||
export const CreatePage = () => {
|
export const CreatePage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { uploadedFile } = location.state || {}
|
const { uploadedFiles } = location.state || {}
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
@ -134,10 +134,10 @@ export const CreatePage = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (uploadedFile) {
|
if (uploadedFiles) {
|
||||||
setSelectedFiles([uploadedFile])
|
setSelectedFiles([...uploadedFiles])
|
||||||
}
|
}
|
||||||
}, [uploadedFile])
|
}, [uploadedFiles])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (usersPubkey) {
|
if (usersPubkey) {
|
||||||
@ -407,7 +407,6 @@ export const CreatePage = () => {
|
|||||||
encryptionKey: string
|
encryptionKey: string
|
||||||
): Promise<File | null> => {
|
): Promise<File | null> => {
|
||||||
// Get the current timestamp in seconds
|
// Get the current timestamp in seconds
|
||||||
const unixNow = now()
|
|
||||||
const blob = new Blob([encryptedArrayBuffer])
|
const blob = new Blob([encryptedArrayBuffer])
|
||||||
// Create a File object with the Blob data
|
// Create a File object with the Blob data
|
||||||
const file = new File([blob], `compressed.sigit`, {
|
const file = new File([blob], `compressed.sigit`, {
|
||||||
@ -455,10 +454,9 @@ export const CreatePage = () => {
|
|||||||
const uploadFile = async (
|
const uploadFile = async (
|
||||||
arrayBuffer: ArrayBuffer
|
arrayBuffer: ArrayBuffer
|
||||||
): Promise<string | null> => {
|
): Promise<string | null> => {
|
||||||
const unixNow = now()
|
|
||||||
const blob = new Blob([arrayBuffer])
|
const blob = new Blob([arrayBuffer])
|
||||||
// Create a File object with the Blob data
|
// Create a File object with the Blob data
|
||||||
const file = new File([blob], `compressed-${unixNow}.sigit`, {
|
const file = new File([blob], `compressed-${unixNow()}.sigit`, {
|
||||||
type: 'application/sigit'
|
type: 'application/sigit'
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -485,7 +483,7 @@ export const CreatePage = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
saveAs(finalZipFile, `request-${now()}.sigit.zip`)
|
saveAs(finalZipFile, `request-${unixNow()}.sigit.zip`)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -615,7 +613,7 @@ export const CreatePage = () => {
|
|||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
createSignature,
|
createSignature,
|
||||||
keys,
|
keys,
|
||||||
modifiedAt: now(),
|
modifiedAt: unixNow(),
|
||||||
docSignatures: {}
|
docSignatures: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -654,7 +652,7 @@ export const CreatePage = () => {
|
|||||||
|
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
createSignature,
|
createSignature,
|
||||||
modifiedAt: now(),
|
modifiedAt: unixNow(),
|
||||||
docSignatures: {}
|
docSignatures: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -979,7 +977,7 @@ const SignerRow = ({
|
|||||||
item: () => {
|
item: () => {
|
||||||
return { id: user.pubkey, index }
|
return { id: user.pubkey, index }
|
||||||
},
|
},
|
||||||
collect: (monitor: DragSourceMonitor) => ({
|
collect: (monitor) => ({
|
||||||
isDragging: monitor.isDragging()
|
isDragging: monitor.isDragging()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,51 +1,92 @@
|
|||||||
import { CalendarMonth, Description, Upload } from '@mui/icons-material'
|
import { Button, TextField } from '@mui/material'
|
||||||
import { Box, Button, Tooltip, Typography } from '@mui/material'
|
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import { Event, kinds, verifyEvent } from 'nostr-tools'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { UserAvatar } from '../../components/UserAvatar'
|
|
||||||
import { MetadataController } from '../../controllers'
|
|
||||||
import { useAppSelector } from '../../hooks'
|
import { useAppSelector } from '../../hooks'
|
||||||
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
||||||
import { CreateSignatureEventContent, Meta, ProfileMetadata } from '../../types'
|
import { Meta } from '../../types'
|
||||||
import {
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
formatTimestamp,
|
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||||
hexToNpub,
|
import { Select } from '../../components/Select'
|
||||||
npubToHex,
|
import { DisplaySigit } from '../../components/DisplaySigit'
|
||||||
parseJson,
|
import { useDropzone } from 'react-dropzone'
|
||||||
shorten
|
|
||||||
} from '../../utils'
|
|
||||||
import styles from './style.module.scss'
|
|
||||||
import { Container } from '../../components/Container'
|
import { Container } from '../../components/Container'
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
import {
|
||||||
|
extractSigitCardDisplayInfo,
|
||||||
|
SigitCardDisplayInfo,
|
||||||
|
SigitStatus
|
||||||
|
} from '../../utils'
|
||||||
|
|
||||||
|
// Unsupported Filter options are commented
|
||||||
|
const FILTERS = [
|
||||||
|
'Show all',
|
||||||
|
// 'Drafts',
|
||||||
|
'In-progress',
|
||||||
|
'Completed'
|
||||||
|
// 'Archived'
|
||||||
|
] as const
|
||||||
|
type Filter = (typeof FILTERS)[number]
|
||||||
|
|
||||||
|
const SORT_BY = [
|
||||||
|
{
|
||||||
|
label: 'Newest',
|
||||||
|
value: 'desc'
|
||||||
|
},
|
||||||
|
{ label: 'Oldest', value: 'asc' }
|
||||||
|
] as const
|
||||||
|
type Sort = (typeof SORT_BY)[number]['value']
|
||||||
|
|
||||||
export const HomePage = () => {
|
export const HomePage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const [sigits, setSigits] = useState<Meta[]>([])
|
const q = searchParams.get('q') ?? ''
|
||||||
const [profiles, setProfiles] = useState<{ [key: string]: ProfileMetadata }>(
|
|
||||||
{}
|
useEffect(() => {
|
||||||
)
|
const searchInput = document.getElementById('q') as HTMLInputElement | null
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.value = q
|
||||||
|
}
|
||||||
|
}, [q])
|
||||||
|
|
||||||
|
const [sigits, setSigits] = useState<{ [key: string]: Meta }>({})
|
||||||
|
const [parsedSigits, setParsedSigits] = useState<{
|
||||||
|
[key: string]: SigitCardDisplayInfo
|
||||||
|
}>({})
|
||||||
const usersAppData = useAppSelector((state) => state.userAppData)
|
const usersAppData = useAppSelector((state) => state.userAppData)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (usersAppData) {
|
if (usersAppData) {
|
||||||
setSigits(Object.values(usersAppData.sigits))
|
const getSigitInfo = async () => {
|
||||||
|
const parsedSigits: { [key: string]: SigitCardDisplayInfo } = {}
|
||||||
|
for (const key in usersAppData.sigits) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(usersAppData.sigits, key)) {
|
||||||
|
const sigitInfo = await extractSigitCardDisplayInfo(
|
||||||
|
usersAppData.sigits[key]
|
||||||
|
)
|
||||||
|
if (sigitInfo) {
|
||||||
|
parsedSigits[key] = sigitInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setParsedSigits({
|
||||||
|
...parsedSigits
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setSigits(usersAppData.sigits)
|
||||||
|
getSigitInfo()
|
||||||
}
|
}
|
||||||
}, [usersAppData])
|
}, [usersAppData])
|
||||||
|
|
||||||
const handleUploadClick = () => {
|
const onDrop = useCallback(
|
||||||
if (fileInputRef.current) {
|
async (acceptedFiles: File[]) => {
|
||||||
fileInputRef.current.click()
|
// When uploading single file check if it's .sigit.zip
|
||||||
}
|
if (acceptedFiles.length === 1) {
|
||||||
}
|
const file = acceptedFiles[0]
|
||||||
|
|
||||||
const handleFileChange = async (
|
|
||||||
event: React.ChangeEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
const file = event.target.files?.[0]
|
|
||||||
if (file) {
|
|
||||||
// Check if the file extension is .sigit.zip
|
// Check if the file extension is .sigit.zip
|
||||||
const fileName = file.name
|
const fileName = file.name
|
||||||
const fileExtension = fileName.slice(-10) // ".sigit.zip" has 10 characters
|
const fileExtension = fileName.slice(-10) // ".sigit.zip" has 10 characters
|
||||||
@ -72,313 +113,155 @@ export const HomePage = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.error('Invalid zip file')
|
toast.error('Invalid SiGit zip file')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// navigate to create page
|
// navigate to create page
|
||||||
navigate(appPrivateRoutes.create, { state: { uploadedFile: file } })
|
navigate(appPrivateRoutes.create, {
|
||||||
}
|
state: { uploadedFiles: acceptedFiles }
|
||||||
}
|
})
|
||||||
|
},
|
||||||
|
[navigate]
|
||||||
|
)
|
||||||
|
|
||||||
|
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
|
||||||
|
onDrop,
|
||||||
|
noClick: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const [filter, setFilter] = useState<Filter>('Show all')
|
||||||
|
const [sort, setSort] = useState<Sort>('desc')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div {...getRootProps()} tabIndex={-1}>
|
||||||
<Container className={styles.container}>
|
<Container className={styles.container}>
|
||||||
<Box className={styles.header}>
|
<div className={styles.header}>
|
||||||
<Typography variant="h3" className={styles.title}>
|
<div className={styles.filters}>
|
||||||
Sigits
|
<Select
|
||||||
</Typography>
|
name={'filter-select'}
|
||||||
{/* This is for desktop view */}
|
value={filter}
|
||||||
<Box
|
setValue={setFilter}
|
||||||
className={styles.actionButtons}
|
options={FILTERS.map((f) => {
|
||||||
sx={{
|
return {
|
||||||
display: {
|
label: f,
|
||||||
xs: 'none',
|
value: f
|
||||||
md: 'flex'
|
|
||||||
}
|
}
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
name={'sort-select'}
|
||||||
|
value={sort}
|
||||||
|
setValue={setSort}
|
||||||
|
options={SORT_BY.map((s) => {
|
||||||
|
return { ...s }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.actionButtons}>
|
||||||
|
<form
|
||||||
|
className={styles.search}
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const searchInput = e.currentTarget.elements.namedItem(
|
||||||
|
'q'
|
||||||
|
) as HTMLInputElement
|
||||||
|
searchParams.set('q', searchInput.value)
|
||||||
|
setSearchParams(searchParams)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input
|
<TextField
|
||||||
type="file"
|
id="q"
|
||||||
ref={fileInputRef}
|
name="q"
|
||||||
style={{ display: 'none' }}
|
placeholder="Search"
|
||||||
onChange={handleFileChange}
|
size="small"
|
||||||
|
type="search"
|
||||||
|
defaultValue={q}
|
||||||
|
onChange={(e) => {
|
||||||
|
// Handle the case when users click native search input's clear or x
|
||||||
|
if (e.currentTarget.value === '') {
|
||||||
|
searchParams.delete('q')
|
||||||
|
setSearchParams(searchParams)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
fontSize: '16px',
|
||||||
|
borderTopLeftRadius: 'var(----mui-shape-borderRadius)',
|
||||||
|
borderBottomLeftRadius: 'var(----mui-shape-borderRadius)',
|
||||||
|
'& .MuiInputBase-root': {
|
||||||
|
borderTopRightRadius: 0,
|
||||||
|
borderBottomRightRadius: 0
|
||||||
|
},
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
padding: '7px 14px'
|
||||||
|
},
|
||||||
|
'& .MuiOutlinedInput-notchedOutline': {
|
||||||
|
display: 'none'
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
type="submit"
|
||||||
startIcon={<Upload />}
|
|
||||||
onClick={handleUploadClick}
|
|
||||||
>
|
|
||||||
Upload
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
{/* This is for mobile view */}
|
|
||||||
<Box
|
|
||||||
className={styles.actionButtons}
|
|
||||||
sx={{
|
sx={{
|
||||||
display: {
|
minWidth: '44px',
|
||||||
xs: 'flex',
|
padding: '11.5px 12px',
|
||||||
md: 'none'
|
borderTopLeftRadius: 0,
|
||||||
}
|
borderBottomLeftRadius: 0
|
||||||
}}
|
}}
|
||||||
|
variant={'contained'}
|
||||||
|
aria-label="submit search"
|
||||||
>
|
>
|
||||||
<Tooltip title="Upload" arrow>
|
<FontAwesomeIcon icon={faSearch} />
|
||||||
<Button variant="outlined" onClick={handleUploadClick}>
|
|
||||||
<Upload />
|
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</form>
|
||||||
</Box>
|
</div>
|
||||||
</Box>
|
</div>
|
||||||
<Box className={styles.submissions}>
|
<button
|
||||||
{sigits.map((sigit, index) => (
|
className={`${styles.dropzone} ${isDragActive ? styles.isDragActive : ''}`}
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={open}
|
||||||
|
type="button"
|
||||||
|
aria-label="upload files"
|
||||||
|
>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
{isDragActive ? (
|
||||||
|
<p>Drop the files here ...</p>
|
||||||
|
) : (
|
||||||
|
<p>Click or drag files to upload!</p>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<div className={styles.submissions}>
|
||||||
|
{Object.keys(parsedSigits)
|
||||||
|
.filter((s) => {
|
||||||
|
const { title, signedStatus } = parsedSigits[s]
|
||||||
|
const isMatch = title?.toLowerCase().includes(q.toLowerCase())
|
||||||
|
switch (filter) {
|
||||||
|
case 'Completed':
|
||||||
|
return signedStatus === SigitStatus.Complete && isMatch
|
||||||
|
case 'In-progress':
|
||||||
|
return signedStatus === SigitStatus.Partial && isMatch
|
||||||
|
case 'Show all':
|
||||||
|
return isMatch
|
||||||
|
default:
|
||||||
|
console.error('Filter case not handled.')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const x = parsedSigits[a].createdAt ?? 0
|
||||||
|
const y = parsedSigits[b].createdAt ?? 0
|
||||||
|
return sort === 'desc' ? y - x : x - y
|
||||||
|
})
|
||||||
|
.map((key) => (
|
||||||
<DisplaySigit
|
<DisplaySigit
|
||||||
key={`sigit-${index}`}
|
key={`sigit-${key}`}
|
||||||
meta={sigit}
|
parsedMeta={parsedSigits[key]}
|
||||||
profiles={profiles}
|
meta={sigits[key]}
|
||||||
setProfiles={setProfiles}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
</div>
|
||||||
}
|
|
||||||
|
|
||||||
type SigitProps = {
|
|
||||||
meta: Meta
|
|
||||||
profiles: { [key: string]: ProfileMetadata }
|
|
||||||
setProfiles: Dispatch<SetStateAction<{ [key: string]: ProfileMetadata }>>
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SignedStatus {
|
|
||||||
Partial = 'Partially Signed',
|
|
||||||
Complete = 'Completely Signed'
|
|
||||||
}
|
|
||||||
|
|
||||||
const DisplaySigit = ({ meta, profiles, setProfiles }: SigitProps) => {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const [title, setTitle] = useState<string>()
|
|
||||||
const [createdAt, setCreatedAt] = useState('')
|
|
||||||
const [submittedBy, setSubmittedBy] = useState<string>()
|
|
||||||
const [signers, setSigners] = useState<`npub1${string}`[]>([])
|
|
||||||
const [signedStatus, setSignedStatus] = useState<SignedStatus>(
|
|
||||||
SignedStatus.Partial
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const extractInfo = async () => {
|
|
||||||
const createSignatureEvent = await parseJson<Event>(
|
|
||||||
meta.createSignature
|
|
||||||
).catch((err) => {
|
|
||||||
console.log('err in parsing the createSignature event:>> ', err)
|
|
||||||
toast.error(
|
|
||||||
err.message || 'error occurred in parsing the create signature event'
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!createSignatureEvent) return
|
|
||||||
|
|
||||||
// created_at in nostr events are stored in seconds
|
|
||||||
// convert it to ms before formatting
|
|
||||||
setCreatedAt(formatTimestamp(createSignatureEvent.created_at * 1000))
|
|
||||||
|
|
||||||
const createSignatureContent =
|
|
||||||
await parseJson<CreateSignatureEventContent>(
|
|
||||||
createSignatureEvent.content
|
|
||||||
).catch((err) => {
|
|
||||||
console.log(
|
|
||||||
`err in parsing the createSignature event's content :>> `,
|
|
||||||
err
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!createSignatureContent) return
|
|
||||||
|
|
||||||
setTitle(createSignatureContent.title)
|
|
||||||
setSubmittedBy(createSignatureEvent.pubkey)
|
|
||||||
setSigners(createSignatureContent.signers)
|
|
||||||
|
|
||||||
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
|
|
||||||
const isCompletelySigned = createSignatureContent.signers.every(
|
|
||||||
(signer) => signedBy.includes(signer)
|
|
||||||
)
|
|
||||||
if (isCompletelySigned) {
|
|
||||||
setSignedStatus(SignedStatus.Complete)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
extractInfo()
|
|
||||||
}, [meta])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const hexKeys: string[] = []
|
|
||||||
|
|
||||||
if (submittedBy) {
|
|
||||||
hexKeys.push(npubToHex(submittedBy)!)
|
|
||||||
}
|
|
||||||
hexKeys.push(...signers.map((signer) => npubToHex(signer)!))
|
|
||||||
|
|
||||||
const metadataController = new MetadataController()
|
|
||||||
hexKeys.forEach((key) => {
|
|
||||||
if (!(key in profiles)) {
|
|
||||||
const handleMetadataEvent = (event: Event) => {
|
|
||||||
const metadataContent =
|
|
||||||
metadataController.extractProfileMetadataContent(event)
|
|
||||||
|
|
||||||
if (metadataContent)
|
|
||||||
setProfiles((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[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) => {
|
|
||||||
console.error(`error occurred in finding metadata for: ${key}`, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [submittedBy, signers])
|
|
||||||
|
|
||||||
const handleNavigation = () => {
|
|
||||||
if (signedStatus === SignedStatus.Complete) {
|
|
||||||
navigate(appPublicRoutes.verify, { state: { meta } })
|
|
||||||
} else {
|
|
||||||
navigate(appPrivateRoutes.sign, { state: { meta } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
className={styles.item}
|
|
||||||
sx={{
|
|
||||||
flexDirection: {
|
|
||||||
xs: 'column',
|
|
||||||
md: 'row'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onClick={handleNavigation}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
className={styles.titleBox}
|
|
||||||
sx={{
|
|
||||||
borderBottomLeftRadius: {
|
|
||||||
xs: 'initial',
|
|
||||||
md: 'inherit'
|
|
||||||
},
|
|
||||||
borderTopRightRadius: {
|
|
||||||
xs: 'inherit',
|
|
||||||
md: 'initial'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="body1" className={styles.title}>
|
|
||||||
<Description />
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
{submittedBy &&
|
|
||||||
(function () {
|
|
||||||
const profile = profiles[submittedBy]
|
|
||||||
return (
|
|
||||||
<UserAvatar
|
|
||||||
pubkey={submittedBy}
|
|
||||||
name={
|
|
||||||
profile?.display_name ||
|
|
||||||
profile?.name ||
|
|
||||||
shorten(hexToNpub(submittedBy))
|
|
||||||
}
|
|
||||||
image={profile?.picture}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})()}
|
|
||||||
<Typography variant="body2" className={styles.date}>
|
|
||||||
<CalendarMonth />
|
|
||||||
{createdAt}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box className={styles.signers}>
|
|
||||||
{signers.map((signer) => {
|
|
||||||
const pubkey = npubToHex(signer)!
|
|
||||||
const profile = profiles[pubkey]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DisplaySigner
|
|
||||||
key={signer}
|
|
||||||
meta={meta}
|
|
||||||
profile={profile}
|
|
||||||
pubkey={pubkey}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SignStatus {
|
|
||||||
Signed = 'Signed',
|
|
||||||
Pending = 'Pending',
|
|
||||||
Invalid = 'Invalid Sign'
|
|
||||||
}
|
|
||||||
|
|
||||||
type DisplaySignerProps = {
|
|
||||||
meta: Meta
|
|
||||||
profile: ProfileMetadata
|
|
||||||
pubkey: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const DisplaySigner = ({ meta, profile, pubkey }: DisplaySignerProps) => {
|
|
||||||
const [signStatus, setSignedStatus] = useState<SignStatus>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const updateSignStatus = async () => {
|
|
||||||
const npub = hexToNpub(pubkey)
|
|
||||||
if (npub in meta.docSignatures) {
|
|
||||||
parseJson<Event>(meta.docSignatures[npub])
|
|
||||||
.then((event) => {
|
|
||||||
const isValidSignature = verifyEvent(event)
|
|
||||||
if (isValidSignature) {
|
|
||||||
setSignedStatus(SignStatus.Signed)
|
|
||||||
} else {
|
|
||||||
setSignedStatus(SignStatus.Invalid)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(`err in parsing the docSignatures for ${npub}:>> `, err)
|
|
||||||
setSignedStatus(SignStatus.Invalid)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setSignedStatus(SignStatus.Pending)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSignStatus()
|
|
||||||
}, [meta, pubkey])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box className={styles.signerItem}>
|
|
||||||
<Typography variant="button" className={styles.status}>
|
|
||||||
{signStatus}
|
|
||||||
</Typography>
|
|
||||||
<UserAvatar
|
|
||||||
pubkey={pubkey}
|
|
||||||
name={
|
|
||||||
profile?.display_name ||
|
|
||||||
profile?.name ||
|
|
||||||
shorten(hexToNpub(pubkey), 5)
|
|
||||||
}
|
|
||||||
image={profile?.picture}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,94 +1,101 @@
|
|||||||
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 25px;
|
gap: 25px;
|
||||||
|
container-type: inline-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
.title {
|
@container (width < 610px) {
|
||||||
color: var(--mui-palette-primary-light);
|
flex-direction: column-reverse;
|
||||||
flex: 1;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actionButtons {
|
.actionButtons {
|
||||||
justify-content: center;
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
padding: 1.5px 0;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: end;
|
||||||
|
|
||||||
|
height: 34px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: solid 1px #dddddd;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@container (width >= 610px) {
|
||||||
|
max-width: 246px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
outline-color: $primary-main;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: $overlay-background-color;
|
||||||
|
height: 250px;
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
transition:
|
||||||
|
background-color ease 0.2s,
|
||||||
|
inset ease 0.2s;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 2px;
|
||||||
|
border: dashed 3px rgba(0, 0, 0, 0.1);
|
||||||
|
inset: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&.isDragActive,
|
||||||
|
&:hover {
|
||||||
|
&::before {
|
||||||
|
inset: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override button styles
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.submissions {
|
.submissions {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
gap: 25px;
|
||||||
gap: 10px;
|
grid-template-columns: repeat(auto-fit, minmax(365px, 1fr));
|
||||||
|
|
||||||
.item {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
background-color: #efeae6;
|
|
||||||
border-radius: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.titleBox {
|
|
||||||
display: flex;
|
|
||||||
flex: 4;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: #cdc8c499;
|
|
||||||
border-top-left-radius: inherit;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--mui-palette-primary-light);
|
|
||||||
font-size: 1.5rem;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.date {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--mui-palette-primary-light);
|
|
||||||
font-size: 1rem;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.signers {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 6;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
color: var(--mui-palette-primary-light);
|
|
||||||
|
|
||||||
.signerItem {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
.status {
|
|
||||||
border-radius: 2rem;
|
|
||||||
width: 100px;
|
|
||||||
text-align: center;
|
|
||||||
background-color: var(--mui-palette-info-light);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ export const Nostr = () => {
|
|||||||
/**
|
/**
|
||||||
* Call login function when enter is pressed
|
* Call login function when enter is pressed
|
||||||
*/
|
*/
|
||||||
const handleInputKeyDown = (event: any) => {
|
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (event.code === 'Enter' || event.code === 'NumpadEnter') {
|
if (event.code === 'Enter' || event.code === 'NumpadEnter') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
login()
|
login()
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
useTheme
|
useTheme
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { UnsignedEvent, nip19, kinds, VerifiedEvent, Event } from 'nostr-tools'
|
import { UnsignedEvent, nip19, kinds, VerifiedEvent, Event } from 'nostr-tools'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import React, { 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'
|
||||||
import { MetadataController, NostrController } from '../../../controllers'
|
import { MetadataController, NostrController } from '../../../controllers'
|
||||||
@ -26,7 +26,7 @@ import { setMetadataEvent } from '../../../store/actions'
|
|||||||
import { LoadingSpinner } from '../../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../../components/LoadingSpinner'
|
||||||
import { LoginMethods } from '../../../store/auth/types'
|
import { LoginMethods } from '../../../store/auth/types'
|
||||||
import { SmartToy } from '@mui/icons-material'
|
import { SmartToy } from '@mui/icons-material'
|
||||||
import { getRoboHashPicture } from '../../../utils'
|
import { getRoboHashPicture, unixNow } from '../../../utils'
|
||||||
import { Container } from '../../../components/Container'
|
import { Container } from '../../../components/Container'
|
||||||
|
|
||||||
export const ProfileSettingsPage = () => {
|
export const ProfileSettingsPage = () => {
|
||||||
@ -197,7 +197,7 @@ export const ProfileSettingsPage = () => {
|
|||||||
// Relay will reject if created_at is too late
|
// Relay will reject if created_at is too late
|
||||||
const updatedMetadataState: UnsignedEvent = {
|
const updatedMetadataState: UnsignedEvent = {
|
||||||
content: content,
|
content: content,
|
||||||
created_at: Math.round(Date.now() / 1000),
|
created_at: unixNow(),
|
||||||
kind: kinds.Metadata,
|
kind: kinds.Metadata,
|
||||||
pubkey: pubkey!,
|
pubkey: pubkey!,
|
||||||
tags: metadataState?.tags || []
|
tags: metadataState?.tags || []
|
||||||
@ -321,8 +321,8 @@ export const ProfileSettingsPage = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
onError={(event: any) => {
|
onError={(event: React.SyntheticEvent<HTMLImageElement>) => {
|
||||||
event.target.src = getRoboHashPicture(npub!)
|
event.currentTarget.src = getRoboHashPicture(npub!)
|
||||||
}}
|
}}
|
||||||
className={styles.img}
|
className={styles.img}
|
||||||
src={getProfileImage(profileMetadata)}
|
src={getProfileImage(profileMetadata)}
|
||||||
|
@ -26,7 +26,7 @@ import {
|
|||||||
hexToNpub,
|
hexToNpub,
|
||||||
isOnline,
|
isOnline,
|
||||||
loadZip,
|
loadZip,
|
||||||
now,
|
unixNow,
|
||||||
npubToHex,
|
npubToHex,
|
||||||
parseJson,
|
parseJson,
|
||||||
readContentOfZipEntry,
|
readContentOfZipEntry,
|
||||||
@ -554,7 +554,7 @@ export const SignPage = () => {
|
|||||||
...metaCopy.docSignatures,
|
...metaCopy.docSignatures,
|
||||||
[hexToNpub(signedEvent.pubkey)]: JSON.stringify(signedEvent, null, 2)
|
[hexToNpub(signedEvent.pubkey)]: JSON.stringify(signedEvent, null, 2)
|
||||||
}
|
}
|
||||||
metaCopy.modifiedAt = now()
|
metaCopy.modifiedAt = unixNow()
|
||||||
return metaCopy
|
return metaCopy
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -564,7 +564,6 @@ export const SignPage = () => {
|
|||||||
encryptionKey: string
|
encryptionKey: string
|
||||||
): Promise<File | null> => {
|
): Promise<File | null> => {
|
||||||
// Get the current timestamp in seconds
|
// Get the current timestamp in seconds
|
||||||
const unixNow = now()
|
|
||||||
const blob = new Blob([encryptedArrayBuffer])
|
const blob = new Blob([encryptedArrayBuffer])
|
||||||
// Create a File object with the Blob data
|
// Create a File object with the Blob data
|
||||||
const file = new File([blob], `compressed.sigit`, {
|
const file = new File([blob], `compressed.sigit`, {
|
||||||
@ -614,7 +613,7 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
if (!arraybuffer) return null
|
if (!arraybuffer) return null
|
||||||
|
|
||||||
return new File([new Blob([arraybuffer])], `${unixNow}.sigit.zip`, {
|
return new File([new Blob([arraybuffer])], `${unixNow()}.sigit.zip`, {
|
||||||
type: 'application/zip'
|
type: 'application/zip'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -758,8 +757,7 @@ export const SignPage = () => {
|
|||||||
if (!arrayBuffer) return
|
if (!arrayBuffer) return
|
||||||
|
|
||||||
const blob = new Blob([arrayBuffer])
|
const blob = new Blob([arrayBuffer])
|
||||||
const unixNow = now()
|
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
||||||
saveAs(blob, `exported-${unixNow}.sigit.zip`)
|
|
||||||
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
|
|
||||||
@ -804,8 +802,7 @@ export const SignPage = () => {
|
|||||||
const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)
|
const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)
|
||||||
|
|
||||||
if (!finalZipFile) return
|
if (!finalZipFile) return
|
||||||
const unixNow = now()
|
saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`)
|
||||||
saveAs(finalZipFile, `exported-${unixNow}.sigit.zip`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,7 +28,7 @@ import {
|
|||||||
extractZipUrlAndEncryptionKey,
|
extractZipUrlAndEncryptionKey,
|
||||||
getHash,
|
getHash,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
now,
|
unixNow,
|
||||||
npubToHex,
|
npubToHex,
|
||||||
parseJson,
|
parseJson,
|
||||||
readContentOfZipEntry,
|
readContentOfZipEntry,
|
||||||
@ -239,7 +239,7 @@ export const VerifyPage = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [submittedBy, signers, viewers])
|
}, [submittedBy, signers, viewers, metadata])
|
||||||
|
|
||||||
const handleVerify = async () => {
|
const handleVerify = async () => {
|
||||||
if (!selectedFile) return
|
if (!selectedFile) return
|
||||||
@ -445,7 +445,7 @@ export const VerifyPage = () => {
|
|||||||
if (!arrayBuffer) return
|
if (!arrayBuffer) return
|
||||||
|
|
||||||
const blob = new Blob([arrayBuffer])
|
const blob = new Blob([arrayBuffer])
|
||||||
saveAs(blob, `exported-${now()}.sigit.zip`)
|
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
||||||
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
@ -23,14 +23,6 @@ export const theme = extendTheme({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
MuiModal: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
insetBlock: '25px',
|
|
||||||
insetInline: '10px'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
MuiButton: {
|
MuiButton: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
@ -41,6 +33,9 @@ export const theme = extendTheme({
|
|||||||
boxShadow: 'unset',
|
boxShadow: 'unset',
|
||||||
lineHeight: 'inherit',
|
lineHeight: 'inherit',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
|
':focus': {
|
||||||
|
textDecoration: 'none'
|
||||||
|
},
|
||||||
':hover': {
|
':hover': {
|
||||||
background: 'var(--primary-light)',
|
background: 'var(--primary-light)',
|
||||||
boxShadow: 'unset'
|
boxShadow: 'unset'
|
||||||
|
@ -7,3 +7,4 @@ export * from './string'
|
|||||||
export * from './zip'
|
export * from './zip'
|
||||||
export * from './utils'
|
export * from './utils'
|
||||||
export * from './mark'
|
export * from './mark'
|
||||||
|
export * from './meta'
|
||||||
|
181
src/utils/meta.ts
Normal file
181
src/utils/meta.ts
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import { CreateSignatureEventContent, Meta } from '../types'
|
||||||
|
import { fromUnixTimestamp, parseJson } from '.'
|
||||||
|
import { Event } from 'nostr-tools'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
|
||||||
|
export enum SignStatus {
|
||||||
|
Signed = 'Signed',
|
||||||
|
Pending = 'Pending',
|
||||||
|
Invalid = 'Invalid Sign'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SigitStatus {
|
||||||
|
Partial = 'In-Progress',
|
||||||
|
Complete = 'Completed'
|
||||||
|
}
|
||||||
|
|
||||||
|
type Jsonable =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
| readonly Jsonable[]
|
||||||
|
| { readonly [key: string]: Jsonable }
|
||||||
|
| { toJSON(): Jsonable }
|
||||||
|
|
||||||
|
export class SigitMetaParseError extends Error {
|
||||||
|
public readonly context?: Jsonable
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
options: { cause?: Error; context?: Jsonable } = {}
|
||||||
|
) {
|
||||||
|
const { cause, context } = options
|
||||||
|
|
||||||
|
super(message, { cause })
|
||||||
|
this.name = this.constructor.name
|
||||||
|
|
||||||
|
this.context = context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle meta errors
|
||||||
|
* Wraps the errors without message property and stringify to a message so we can use it later
|
||||||
|
* @param error
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function handleError(error: unknown): Error {
|
||||||
|
if (error instanceof Error) return error
|
||||||
|
|
||||||
|
// No message error, wrap it and stringify
|
||||||
|
let stringified = 'Unable to stringify the thrown value'
|
||||||
|
try {
|
||||||
|
stringified = JSON.stringify(error)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(stringified, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Error(`[SiGit Error]: ${stringified}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reuse common error messages for meta parsing
|
||||||
|
export enum SigitMetaParseErrorType {
|
||||||
|
'PARSE_ERROR_SIGNATURE_EVENT' = 'error occurred in parsing the create signature event',
|
||||||
|
'PARSE_ERROR_SIGNATURE_EVENT_CONTENT' = "err in parsing the createSignature event's content"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SigitCardDisplayInfo {
|
||||||
|
createdAt?: number
|
||||||
|
title?: string
|
||||||
|
submittedBy?: string
|
||||||
|
signers: `npub1${string}`[]
|
||||||
|
fileExtensions: string[]
|
||||||
|
signedStatus: SigitStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for createSignatureEvent parse that throws custom SigitMetaParseError with cause and context
|
||||||
|
* @param raw Raw string for parsing
|
||||||
|
* @returns parsed Event
|
||||||
|
*/
|
||||||
|
export const parseCreateSignatureEvent = async (
|
||||||
|
raw: string
|
||||||
|
): Promise<Event> => {
|
||||||
|
try {
|
||||||
|
const createSignatureEvent = await parseJson<Event>(raw)
|
||||||
|
return createSignatureEvent
|
||||||
|
} catch (error) {
|
||||||
|
throw new SigitMetaParseError(
|
||||||
|
SigitMetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT,
|
||||||
|
{
|
||||||
|
cause: handleError(error),
|
||||||
|
context: raw
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for event content parser that throws custom SigitMetaParseError with cause and context
|
||||||
|
* @param raw Raw string for parsing
|
||||||
|
* @returns parsed CreateSignatureEventContent
|
||||||
|
*/
|
||||||
|
export const parseCreateSignatureEventContent = async (
|
||||||
|
raw: string
|
||||||
|
): Promise<CreateSignatureEventContent> => {
|
||||||
|
try {
|
||||||
|
const createSignatureEventContent =
|
||||||
|
await parseJson<CreateSignatureEventContent>(raw)
|
||||||
|
return createSignatureEventContent
|
||||||
|
} catch (error) {
|
||||||
|
throw new SigitMetaParseError(
|
||||||
|
SigitMetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT_CONTENT,
|
||||||
|
{
|
||||||
|
cause: handleError(error),
|
||||||
|
context: raw
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts only necessary metadata for the card display
|
||||||
|
* @param meta Sigit metadata
|
||||||
|
* @returns SigitCardDisplayInfo
|
||||||
|
*/
|
||||||
|
export const extractSigitCardDisplayInfo = async (meta: Meta) => {
|
||||||
|
if (!meta?.createSignature) return
|
||||||
|
|
||||||
|
const sigitInfo: SigitCardDisplayInfo = {
|
||||||
|
signers: [],
|
||||||
|
fileExtensions: [],
|
||||||
|
signedStatus: SigitStatus.Partial
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const createSignatureEvent = await parseCreateSignatureEvent(
|
||||||
|
meta.createSignature
|
||||||
|
)
|
||||||
|
|
||||||
|
// created_at in nostr events are stored in seconds
|
||||||
|
sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at)
|
||||||
|
|
||||||
|
const createSignatureContent = await parseCreateSignatureEventContent(
|
||||||
|
createSignatureEvent.content
|
||||||
|
)
|
||||||
|
|
||||||
|
const files = Object.keys(createSignatureContent.fileHashes)
|
||||||
|
const extensions = files.reduce((result: string[], file: string) => {
|
||||||
|
const extension = file.split('.').pop()
|
||||||
|
if (extension) {
|
||||||
|
result.push(extension)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
|
||||||
|
const isCompletelySigned = createSignatureContent.signers.every((signer) =>
|
||||||
|
signedBy.includes(signer)
|
||||||
|
)
|
||||||
|
|
||||||
|
sigitInfo.title = createSignatureContent.title
|
||||||
|
sigitInfo.submittedBy = createSignatureEvent.pubkey
|
||||||
|
sigitInfo.signers = createSignatureContent.signers
|
||||||
|
sigitInfo.fileExtensions = extensions
|
||||||
|
|
||||||
|
if (isCompletelySigned) {
|
||||||
|
sigitInfo.signedStatus = SigitStatus.Complete
|
||||||
|
}
|
||||||
|
|
||||||
|
return sigitInfo
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof SigitMetaParseError) {
|
||||||
|
toast.error(error.message)
|
||||||
|
console.error(error.name, error.message, error.cause, error.context)
|
||||||
|
} else {
|
||||||
|
console.error('Unexpected error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,7 @@ import { NostrController } from '../controllers'
|
|||||||
import { AuthState } from '../store/auth/types'
|
import { AuthState } from '../store/auth/types'
|
||||||
import store from '../store/store'
|
import store from '../store/store'
|
||||||
import { CreateSignatureEventContent, Meta } from '../types'
|
import { CreateSignatureEventContent, Meta } from '../types'
|
||||||
import { hexToNpub, now } from './nostr'
|
import { hexToNpub, unixNow } from './nostr'
|
||||||
import { parseJson } from './string'
|
import { parseJson } from './string'
|
||||||
import { hexToBytes } from '@noble/hashes/utils'
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
|
|
||||||
@ -28,10 +28,10 @@ export const uploadToFileStorage = async (file: File) => {
|
|||||||
const event: EventTemplate = {
|
const event: EventTemplate = {
|
||||||
kind: 24242,
|
kind: 24242,
|
||||||
content: 'Authorize Upload',
|
content: 'Authorize Upload',
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: unixNow(),
|
||||||
tags: [
|
tags: [
|
||||||
['t', 'upload'],
|
['t', 'upload'],
|
||||||
['expiration', String(now() + 60 * 5)], // Set expiration time to 5 minutes from now
|
['expiration', String(unixNow() + 60 * 5)], // Set expiration time to 5 minutes from now
|
||||||
['name', file.name],
|
['name', file.name],
|
||||||
['size', String(file.size)]
|
['size', String(file.size)]
|
||||||
]
|
]
|
||||||
@ -78,7 +78,7 @@ export const signEventForMetaFile = async (
|
|||||||
const event: EventTemplate = {
|
const event: EventTemplate = {
|
||||||
kind: 27235, // Event type for meta file
|
kind: 27235, // Event type for meta file
|
||||||
content: content, // content for event
|
content: content, // content for event
|
||||||
created_at: Math.floor(Date.now() / 1000), // Current timestamp
|
created_at: unixNow(), // Current timestamp
|
||||||
tags: [['-']] // For understanding why "-" tag is used here see: https://github.com/nostr-protocol/nips/blob/protected-events-tag/70.md
|
tags: [['-']] // For understanding why "-" tag is used here see: https://github.com/nostr-protocol/nips/blob/protected-events-tag/70.md
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,7 +120,8 @@ export const queryNip05 = async (
|
|||||||
if (!match) throw new Error('Invalid nip05')
|
if (!match) throw new Error('Invalid nip05')
|
||||||
|
|
||||||
// Destructure the match result, assigning default value '_' to name if not provided
|
// Destructure the match result, assigning default value '_' to name if not provided
|
||||||
const [name = '_', domain] = match
|
// First variable from the match destructuring is ignored
|
||||||
|
const [, name = '_', domain] = match
|
||||||
|
|
||||||
// Construct the URL to query the NIP-05 data
|
// Construct the URL to query the NIP-05 data
|
||||||
const url = `https://${domain}/.well-known/nostr.json?name=${name}`
|
const url = `https://${domain}/.well-known/nostr.json?name=${name}`
|
||||||
@ -210,7 +211,22 @@ export const getRoboHashPicture = (
|
|||||||
return `https://robohash.org/${npub}.png?set=set${set}`
|
return `https://robohash.org/${npub}.png?set=set${set}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const now = () => Math.round(Date.now() / 1000)
|
export const unixNow = () => Math.round(Date.now() / 1000)
|
||||||
|
export const toUnixTimestamp = (date: number | Date) => {
|
||||||
|
let time
|
||||||
|
if (typeof date === 'number') {
|
||||||
|
time = Math.round(date / 1000)
|
||||||
|
} else if (date instanceof Date) {
|
||||||
|
time = Math.round(date.getTime() / 1000)
|
||||||
|
} else {
|
||||||
|
throw Error('Unsupported type when converting to unix timestamp')
|
||||||
|
}
|
||||||
|
|
||||||
|
return time
|
||||||
|
}
|
||||||
|
export const fromUnixTimestamp = (unix: number) => {
|
||||||
|
return unix * 1000
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate nip44 conversation key
|
* Generate nip44 conversation key
|
||||||
@ -287,7 +303,7 @@ export const createWrap = (unsignedEvent: UnsignedEvent, receiver: string) => {
|
|||||||
kind: 1059, // Event kind
|
kind: 1059, // Event kind
|
||||||
content, // Encrypted content
|
content, // Encrypted content
|
||||||
pubkey, // Public key of the creator
|
pubkey, // Public key of the creator
|
||||||
created_at: now(), // Current timestamp
|
created_at: unixNow(), // Current timestamp
|
||||||
tags: [
|
tags: [
|
||||||
// Tags including receiver and nonce
|
// Tags including receiver and nonce
|
||||||
['p', receiver],
|
['p', receiver],
|
||||||
@ -541,7 +557,7 @@ export const updateUsersAppData = async (meta: Meta) => {
|
|||||||
const updatedEvent: UnsignedEvent = {
|
const updatedEvent: UnsignedEvent = {
|
||||||
kind: kinds.Application,
|
kind: kinds.Application,
|
||||||
pubkey: usersPubkey!,
|
pubkey: usersPubkey!,
|
||||||
created_at: now(),
|
created_at: unixNow(),
|
||||||
tags: [['d', hash]],
|
tags: [['d', hash]],
|
||||||
content: encryptedContent
|
content: encryptedContent
|
||||||
}
|
}
|
||||||
@ -607,10 +623,10 @@ const deleteBlossomFile = async (url: string, privateKey: string) => {
|
|||||||
const event: EventTemplate = {
|
const event: EventTemplate = {
|
||||||
kind: 24242,
|
kind: 24242,
|
||||||
content: 'Authorize Upload',
|
content: 'Authorize Upload',
|
||||||
created_at: now(),
|
created_at: unixNow(),
|
||||||
tags: [
|
tags: [
|
||||||
['t', 'delete'],
|
['t', 'delete'],
|
||||||
['expiration', String(now() + 60 * 5)], // Set expiration time to 5 minutes from now
|
['expiration', String(unixNow() + 60 * 5)], // Set expiration time to 5 minutes from now
|
||||||
['x', hash]
|
['x', hash]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -666,10 +682,10 @@ const uploadUserAppDataToBlossom = async (
|
|||||||
const event: EventTemplate = {
|
const event: EventTemplate = {
|
||||||
kind: 24242,
|
kind: 24242,
|
||||||
content: 'Authorize Upload',
|
content: 'Authorize Upload',
|
||||||
created_at: now(),
|
created_at: unixNow(),
|
||||||
tags: [
|
tags: [
|
||||||
['t', 'upload'],
|
['t', 'upload'],
|
||||||
['expiration', String(now() + 60 * 5)], // Set expiration time to 5 minutes from now
|
['expiration', String(unixNow() + 60 * 5)], // Set expiration time to 5 minutes from now
|
||||||
['name', file.name],
|
['name', file.name],
|
||||||
['size', String(file.size)]
|
['size', String(file.size)]
|
||||||
]
|
]
|
||||||
@ -874,7 +890,7 @@ export const sendNotification = async (receiver: string, meta: Meta) => {
|
|||||||
pubkey: usersPubkey,
|
pubkey: usersPubkey,
|
||||||
content: JSON.stringify(meta),
|
content: JSON.stringify(meta),
|
||||||
tags: [],
|
tags: [],
|
||||||
created_at: now()
|
created_at: unixNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap the unsigned event with the receiver's information
|
// Wrap the unsigned event with the receiver's information
|
||||||
|
Loading…
Reference in New Issue
Block a user
created_at * 1000
should be made into a utility function and used in other similar places (there is at least one that I could find).Similarly, there is a reverse action,
created_at / 1000
, which should also be a utility.