issue-166-open-timestamps #220

Merged
eugene merged 25 commits from issue-166-open-timestamps into staging 2024-10-25 11:18:47 +00:00
11 changed files with 318 additions and 230 deletions
Showing only changes of commit 3b447dcf6a - Show all commits

View File

@ -28,9 +28,9 @@ import {
} from '../../routes'
import {
clearAuthToken,
getProfileUsername,
hexToNpub,
saveNsecBunkerDelegatedKey,
shorten
saveNsecBunkerDelegatedKey
} from '../../utils'
import styles from './style.module.scss'
import { setUserRobotImage } from '../../store/userRobotImage/action'
@ -58,9 +58,8 @@ export const AppBar = () => {
useEffect(() => {
if (metadataState) {
if (metadataState.content) {
const { picture, display_name, name } = JSON.parse(
metadataState.content
)
const profileMetadata = JSON.parse(metadataState.content)
const { picture } = profileMetadata
if (picture || userRobotImage) {
setUserAvatar(picture || userRobotImage)
@ -70,7 +69,7 @@ export const AppBar = () => {
? hexToNpub(authState.usersPubkey)
: ''
setUsername(shorten(display_name || name || npub, 7))
setUsername(getProfileUsername(npub, profileMetadata))
} else {
setUserAvatar(userRobotImage || '')
setUsername('')
@ -133,8 +132,10 @@ export const AppBar = () => {
<div className={styles.banner}>
<Container>
<div className={styles.bannerInner}>
<p className={styles.bannerText}>
SIGit is currently Alpha software (available for internal
testing), use at your own risk!
</p>
<Button
aria-label={`close banner`}
variant="text"

View File

@ -67,3 +67,9 @@
}
}
}
.bannerText {
margin-left: 54px;
flex-grow: 1;
text-align: center;
}

View File

@ -11,16 +11,16 @@ import styles from './style.module.scss'
import React, { useEffect, useState } from 'react'
import { ProfileMetadata, User, UserRole } from '../../types'
import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing'
import { truncate } from 'lodash'
import { settleAllFullfilfedPromises, hexToNpub, npubToHex } from '../../utils'
import { getSigitFile, SigitFile } from '../../utils/file'
import { hexToNpub, npubToHex, getProfileUsername } from '../../utils'
import { SigitFile } from '../../utils/file'
import { getToolboxLabelByMarkType } from '../../utils/mark'
import { FileDivider } from '../FileDivider'
import { ExtensionFileBox } from '../ExtensionFileBox'
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf'
import { useScale } from '../../hooks/useScale'
import { AvatarIconButton } from '../UserAvatarIconButton'
import { LoadingSpinner } from '../LoadingSpinner'
import { UserAvatar } from '../UserAvatar'
import _ from 'lodash'
const DEFAULT_START_SIZE = {
width: 140,
@ -28,52 +28,50 @@ const DEFAULT_START_SIZE = {
} as const
interface Props {
selectedFiles: File[]
users: User[]
metadata: { [key: string]: ProfileMetadata }
onDrawFieldsChange: (sigitFiles: SigitFile[]) => void
sigitFiles: SigitFile[]
setSigitFiles: React.Dispatch<React.SetStateAction<SigitFile[]>>
selectedTool?: DrawTool
}
export const DrawPDFFields = (props: Props) => {
const { selectedFiles, selectedTool, onDrawFieldsChange, users } = props
const { to, from } = useScale()
const { selectedTool, sigitFiles, setSigitFiles, users } = props
const [sigitFiles, setSigitFiles] = useState<SigitFile[]>([])
const [parsingPdf, setIsParsing] = useState<boolean>(false)
const signers = users.filter((u) => u.role === UserRole.signer)
const defaultSignerNpub = signers.length ? hexToNpub(signers[0].pubkey) : ''
const [lastSigner, setLastSigner] = useState(defaultSignerNpub)
/**
* Return first pubkey that is present in the signers list
* @param pubkeys
* @returns available pubkey or empty string
*/
const getAvailableSigner = (...pubkeys: string[]) => {
const availableSigner: string | undefined = pubkeys.find((pubkey) =>
signers.some((s) => s.pubkey === npubToHex(pubkey))
)
return availableSigner || ''
}
const { to, from } = useScale()
const [mouseState, setMouseState] = useState<MouseState>({
clicked: false
})
const [activeDrawField, setActiveDrawField] = useState<number>()
useEffect(() => {
if (selectedFiles) {
/**
* Reads the binary files and converts to internal file type
* and sets to a state (adds images if it's a PDF)
*/
const parsePages = async () => {
const files = await settleAllFullfilfedPromises(
selectedFiles,
getSigitFile
)
setSigitFiles(files)
}
setIsParsing(true)
parsePages().finally(() => {
setIsParsing(false)
})
}
}, [selectedFiles])
useEffect(() => {
if (sigitFiles) onDrawFieldsChange(sigitFiles)
}, [onDrawFieldsChange, sigitFiles])
const [activeDrawnField, setActiveDrawnField] = useState<{
fileIndex: number
pageIndex: number
drawnFieldIndex: number
}>()
const isActiveDrawnField = (
fileIndex: number,
pageIndex: number,
drawnFieldIndex: number
) =>
activeDrawnField?.fileIndex === fileIndex &&
activeDrawnField?.pageIndex === pageIndex &&
activeDrawnField?.drawnFieldIndex === drawnFieldIndex
/**
* Drawing events
@ -100,7 +98,12 @@ export const DrawPDFFields = (props: Props) => {
* @param event Pointer event
* @param page PdfPage where press happened
*/
const handlePointerDown = (event: React.PointerEvent, page: PdfPage) => {
const handlePointerDown = (
event: React.PointerEvent,
page: PdfPage,
fileIndex: number,
pageIndex: number
) => {
// Proceed only if left click
if (event.button !== 0) return
@ -115,12 +118,17 @@ export const DrawPDFFields = (props: Props) => {
top: to(page.width, y),
width: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.width,
height: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.height,
counterpart: '',
counterpart: getAvailableSigner(lastSigner, defaultSignerNpub),
type: selectedTool.identifier
}
page.drawnFields.push(newField)
setActiveDrawnField({
fileIndex,
pageIndex,
drawnFieldIndex: page.drawnFields.length - 1
})
setMouseState((prev) => {
return {
...prev,
@ -189,6 +197,8 @@ export const DrawPDFFields = (props: Props) => {
*/
const handleDrawnFieldPointerDown = (
event: React.PointerEvent,
fileIndex: number,
pageIndex: number,
drawnFieldIndex: number
) => {
event.stopPropagation()
@ -198,7 +208,7 @@ export const DrawPDFFields = (props: Props) => {
const drawingRectangleCoords = getPointerCoordinates(event)
setActiveDrawField(drawnFieldIndex)
setActiveDrawnField({ fileIndex, pageIndex, drawnFieldIndex })
setMouseState({
dragging: true,
clicked: false,
@ -254,13 +264,15 @@ export const DrawPDFFields = (props: Props) => {
*/
const handleResizePointerDown = (
event: React.PointerEvent,
fileIndex: number,
pageIndex: number,
drawnFieldIndex: number
) => {
// Proceed only if left click
if (event.button !== 0) return
event.stopPropagation()
setActiveDrawField(drawnFieldIndex)
setActiveDrawnField({ fileIndex, pageIndex, drawnFieldIndex })
setMouseState({
resizing: true
})
@ -369,7 +381,7 @@ export const DrawPDFFields = (props: Props) => {
handlePointerMove(event, page)
}}
onPointerDown={(event) => {
handlePointerDown(event, page)
handlePointerDown(event, page, fileIndex, pageIndex)
}}
draggable="false"
src={page.image}
@ -381,7 +393,12 @@ export const DrawPDFFields = (props: Props) => {
<div
key={drawnFieldIndex}
onPointerDown={(event) =>
handleDrawnFieldPointerDown(event, drawnFieldIndex)
handleDrawnFieldPointerDown(
event,
fileIndex,
pageIndex,
drawnFieldIndex
)
}
onPointerMove={(event) => {
handleDrawnFieldPointerMove(event, drawnField, page.width)
@ -402,7 +419,11 @@ export const DrawPDFFields = (props: Props) => {
touchAction: 'none',
opacity:
mouseState.dragging &&
activeDrawField === drawnFieldIndex
isActiveDrawnField(
fileIndex,
pageIndex,
drawnFieldIndex
)
? 0.8
: undefined
}}
@ -419,7 +440,12 @@ export const DrawPDFFields = (props: Props) => {
</div>
<span
onPointerDown={(event) =>
handleResizePointerDown(event, drawnFieldIndex)
handleResizePointerDown(
event,
fileIndex,
pageIndex,
drawnFieldIndex
)
}
onPointerMove={(event) => {
handleResizePointerMove(event, drawnField, page.width)
@ -428,7 +454,11 @@ export const DrawPDFFields = (props: Props) => {
style={{
background:
mouseState.resizing &&
activeDrawField === drawnFieldIndex
isActiveDrawnField(
fileIndex,
pageIndex,
drawnFieldIndex
)
? 'var(--primary-main)'
: undefined
}}
@ -446,6 +476,23 @@ export const DrawPDFFields = (props: Props) => {
>
<Close fontSize="small" />
</span>
{!isActiveDrawnField(
fileIndex,
pageIndex,
drawnFieldIndex
) &&
!!drawnField.counterpart && (
<div className={styles.counterpartAvatar}>
<UserAvatar
pubkey={npubToHex(drawnField.counterpart)!}
/>
</div>
)}
{isActiveDrawnField(
fileIndex,
pageIndex,
drawnFieldIndex
) && (
<div
onPointerDown={handleUserSelectPointerDown}
className={styles.userSelect}
@ -453,9 +500,10 @@ export const DrawPDFFields = (props: Props) => {
<FormControl fullWidth size="small">
<InputLabel id="counterparts">Counterpart</InputLabel>
<Select
value={drawnField.counterpart}
value={getAvailableSigner(drawnField.counterpart)}
onChange={(event) => {
drawnField.counterpart = event.target.value
setLastSigner(event.target.value)
refreshPdfFiles()
}}
labelId="counterparts"
@ -463,39 +511,24 @@ export const DrawPDFFields = (props: Props) => {
sx={{
background: 'white'
}}
renderValue={(value) => renderCounterpartValue(value)}
renderValue={(value) =>
renderCounterpartValue(value)
}
>
{users
.filter((u) => u.role === UserRole.signer)
.map((user, index) => {
const npub = hexToNpub(user.pubkey)
let displayValue = truncate(npub, {
length: 16
})
const metadata = props.metadata[user.pubkey]
if (metadata) {
displayValue = truncate(
metadata.name ||
metadata.display_name ||
metadata.username ||
{signers.map((signer, index) => {
const npub = hexToNpub(signer.pubkey)
const metadata = props.metadata[signer.pubkey]
const displayValue = getProfileUsername(
npub,
{
length: 16
}
metadata
)
}
return (
<MenuItem
key={index}
value={hexToNpub(user.pubkey)}
>
<MenuItem key={index} value={npub}>
<ListItemIcon>
<AvatarIconButton
src={metadata?.picture}
hexKey={user.pubkey}
hexKey={signer.pubkey}
aria-label={`account of user ${displayValue}`}
color="inherit"
sx={{
@ -514,6 +547,7 @@ export const DrawPDFFields = (props: Props) => {
</Select>
</FormControl>
</div>
)}
</div>
)
})}
@ -524,28 +558,19 @@ export const DrawPDFFields = (props: Props) => {
)
}
const renderCounterpartValue = (value: string) => {
const user = users.find((u) => u.pubkey === npubToHex(value))
if (user) {
let displayValue = truncate(value, {
length: 16
})
const renderCounterpartValue = (npub: string) => {
let displayValue = _.truncate(npub, { length: 16 })
const metadata = props.metadata[user.pubkey]
const signer = signers.find((u) => u.pubkey === npubToHex(npub))
if (signer) {
const metadata = props.metadata[signer.pubkey]
displayValue = getProfileUsername(npub, metadata)
if (metadata) {
displayValue = truncate(
metadata.name || metadata.display_name || metadata.username || value,
{
length: 16
}
)
}
return (
<>
<div className={styles.counterpartSelectValue}>
<AvatarIconButton
src={props.metadata[user.pubkey]?.picture}
hexKey={npubToHex(user.pubkey) || undefined}
src={props.metadata[signer.pubkey]?.picture}
hexKey={signer.pubkey || undefined}
sx={{
padding: 0,
marginRight: '6px',
@ -556,19 +581,11 @@ export const DrawPDFFields = (props: Props) => {
}}
/>
{displayValue}
</>
</div>
)
}
return value
}
if (parsingPdf) {
return <LoadingSpinner variant="small" />
}
if (!sigitFiles.length) {
return ''
return displayValue
}
return (
@ -589,7 +606,7 @@ export const DrawPDFFields = (props: Props) => {
<ExtensionFileBox extension={file.extension} />
)}
</div>
{i < selectedFiles.length - 1 && <FileDivider />}
{i < sigitFiles.length - 1 && <FileDivider />}
</React.Fragment>
)
})}

View File

@ -84,3 +84,14 @@
padding: 5px 0;
}
}
.counterpartSelectValue {
display: flex;
}
.counterpartAvatar {
img {
width: 21px;
height: 21px;
}
}

View File

@ -5,7 +5,7 @@ import { AvatarIconButton } from '../UserAvatarIconButton'
import { Link } from 'react-router-dom'
import { useProfileMetadata } from '../../hooks/useProfileMetadata'
import { Tooltip } from '@mui/material'
import { shorten } from '../../utils'
import { getProfileUsername } from '../../utils'
import { TooltipChild } from '../TooltipChild'
interface UserAvatarProps {
@ -22,7 +22,7 @@ export const UserAvatar = ({
isNameVisible = false
}: UserAvatarProps) => {
const profile = useProfileMetadata(pubkey)
const name = profile?.display_name || profile?.name || shorten(pubkey)
const name = getProfileUsername(pubkey, profile)
const image = profile?.picture
return (

View File

@ -40,7 +40,8 @@ import {
signEventForMetaFile,
updateUsersAppData,
uploadToFileStorage,
DEFAULT_TOOLBOX
DEFAULT_TOOLBOX,
settleAllFullfilfedPromises
} from '../../utils'
import { Container } from '../../components/Container'
import fileListStyles from '../../components/FileList/style.module.scss'
@ -61,7 +62,8 @@ import {
faTrash,
faUpload
} from '@fortawesome/free-solid-svg-icons'
import { SigitFile } from '../../utils/file.ts'
import { getSigitFile, SigitFile } from '../../utils/file.ts'
import _ from 'lodash'
import { generateTimestamp } from '../../utils/opentimestamps.ts'
export const CreatePage = () => {
@ -108,9 +110,31 @@ export const CreatePage = () => {
{}
)
const [drawnFiles, setDrawnFiles] = useState<SigitFile[]>([])
const [parsingPdf, setIsParsing] = useState<boolean>(false)
useEffect(() => {
if (selectedFiles) {
/**
* Reads the binary files and converts to internal file type
* and sets to a state (adds images if it's a PDF)
*/
const parsePages = async () => {
const files = await settleAllFullfilfedPromises(
selectedFiles,
getSigitFile
)
setDrawnFiles(files)
}
setIsParsing(true)
parsePages().finally(() => {
setIsParsing(false)
})
}
}, [selectedFiles])
const [selectedTool, setSelectedTool] = useState<DrawTool>()
const [toolbox] = useState<DrawTool[]>(DEFAULT_TOOLBOX)
/**
* Changes the drawing tool
@ -284,6 +308,19 @@ export const CreatePage = () => {
const handleRemoveUser = (pubkey: string) => {
setUsers((prev) => prev.filter((user) => user.pubkey !== pubkey))
// Set counterpart to ''
const drawnFilesCopy = _.cloneDeep(drawnFiles)
drawnFilesCopy.forEach((s) => {
s.pages?.forEach((p) => {
p.drawnFields.forEach((d) => {
if (d.counterpart === hexToNpub(pubkey)) {
d.counterpart = ''
}
})
})
})
setDrawnFiles(drawnFilesCopy)
}
/**
@ -739,10 +776,6 @@ export const CreatePage = () => {
}
}
const onDrawFieldsChange = (sigitFiles: SigitFile[]) => {
setDrawnFiles(sigitFiles)
}
if (authUrl) {
return (
<iframe
@ -856,20 +889,27 @@ export const CreatePage = () => {
</div>
<div className={`${styles.paperGroup} ${styles.toolbox}`}>
{toolbox.map((drawTool: DrawTool, index: number) => {
{DEFAULT_TOOLBOX.filter((drawTool) => !drawTool.isHidden).map(
(drawTool: DrawTool, index: number) => {
return (
<div
key={index}
{...(drawTool.active && {
{...(!drawTool.isComingSoon && {
onClick: () => handleToolSelect(drawTool)
})}
className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''} ${!drawTool.active ? styles.comingSoon : ''}
className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''} ${drawTool.isComingSoon ? styles.comingSoon : ''}
`}
>
<FontAwesomeIcon fontSize={'15px'} icon={drawTool.icon} />
<FontAwesomeIcon
fontSize={'15px'}
icon={drawTool.icon}
/>
{drawTool.label}
{drawTool.active ? (
<FontAwesomeIcon fontSize={'15px'} icon={faEllipsis} />
{!drawTool.isComingSoon ? (
<FontAwesomeIcon
fontSize={'15px'}
icon={faEllipsis}
/>
) : (
<span className={styles.comingSoonPlaceholder}>
Coming soon
@ -877,7 +917,8 @@ export const CreatePage = () => {
)}
</div>
)
})}
}
)}
</div>
<Button onClick={handleCreate} variant="contained">
@ -893,13 +934,17 @@ export const CreatePage = () => {
centerIcon={faFile}
rightIcon={faToolbox}
>
{parsingPdf ? (
<LoadingSpinner variant="small" />
) : (
<DrawPDFFields
metadata={metadata}
users={users}
selectedFiles={selectedFiles}
onDrawFieldsChange={onDrawFieldsChange}
metadata={metadata}
selectedTool={selectedTool}
sigitFiles={drawnFiles}
setSigitFiles={setDrawnFiles}
/>
)}
</StickySideColumns>
</Container>
</>

View File

@ -1,7 +1,6 @@
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
import EditIcon from '@mui/icons-material/Edit'
import { Box, IconButton, SxProps, Theme, Typography } from '@mui/material'
import { truncate } from 'lodash'
import { Event, VerifiedEvent, kinds, nip19 } from 'nostr-tools'
import { useEffect, useMemo, useState } from 'react'
import { useSelector } from 'react-redux'
@ -14,6 +13,7 @@ import { State } from '../../store/rootReducer'
import { NostrJoiningBlock, ProfileMetadata } from '../../types'
import {
getNostrJoiningBlockNumber,
getProfileUsername,
getRoboHashPicture,
hexToNpub,
shorten
@ -42,15 +42,7 @@ export const ProfilePage = () => {
const [isLoading, setIsLoading] = useState(true)
const [loadingSpinnerDesc] = useState('Fetching metadata')
const profileName =
pubkey &&
profileMetadata &&
truncate(
profileMetadata.display_name || profileMetadata.name || hexToNpub(pubkey),
{
length: 16
}
)
const profileName = pubkey && getProfileUsername(pubkey, profileMetadata)
useEffect(() => {
if (npub) {

View File

@ -31,7 +31,10 @@ export interface DrawTool {
icon: IconDefinition
defaultValue?: string
selected?: boolean
active?: boolean
/** show or hide the toolbox item */
isHidden?: boolean
/** show or hide "coming soon" message on the toolbox item */
isComingSoon?: boolean
}
export enum MarkType {

View File

@ -1,6 +1,7 @@
export interface ProfileMetadata {
name?: string
display_name?: string
/** @deprecated use name instead */
username?: string
picture?: string
banner?: string

View File

@ -3,26 +3,26 @@ import { hexToNpub } from './nostr.ts'
import { Meta, SignedEventContent } from '../types'
import { Event } from 'nostr-tools'
import { EMPTY } from './const.ts'
import { MarkType } from '../types/drawing.ts'
import { DrawTool, MarkType } from '../types/drawing.ts'
import {
faT,
faSignature,
faBriefcase,
faIdCard,
faHeading,
faClock,
faCalendarDays,
fa1,
faImage,
faSquareCheck,
faCalendarDays,
faCheckDouble,
faPaperclip,
faCircleDot,
faSquareCaretDown,
faTableCellsLarge,
faStamp,
faCreditCard,
faPhone
faHeading,
faImage,
faPaperclip,
faPhone,
faSquareCaretDown,
faSquareCheck,
faStamp,
faTableCellsLarge
} from '@fortawesome/free-solid-svg-icons'
/**
@ -152,114 +152,113 @@ const findOtherUserMarks = (marks: Mark[], pubkey: string): Mark[] => {
return marks.filter((mark) => mark.npub !== hexToNpub(pubkey))
}
export const DEFAULT_TOOLBOX = [
export const DEFAULT_TOOLBOX: DrawTool[] = [
{
identifier: MarkType.TEXT,
icon: faT,
label: 'Text',
active: true
},
{
identifier: MarkType.SIGNATURE,
icon: faSignature,
label: 'Signature',
active: false
identifier: MarkType.FULLNAME,
icon: faIdCard,
label: 'Full Name',
isComingSoon: true
},
{
identifier: MarkType.JOBTITLE,
icon: faBriefcase,
label: 'Job Title',
active: false
isComingSoon: true
},
{
identifier: MarkType.FULLNAME,
icon: faIdCard,
label: 'Full Name',
active: false
},
{
identifier: MarkType.INITIALS,
icon: faHeading,
label: 'Initials',
active: false
identifier: MarkType.SIGNATURE,
icon: faSignature,
label: 'Signature',
isComingSoon: true
},
{
identifier: MarkType.DATETIME,
icon: faClock,
label: 'Date Time',
active: false
isComingSoon: true
},
{
identifier: MarkType.DATE,
icon: faCalendarDays,
label: 'Date',
active: false
identifier: MarkType.TEXT,
icon: faT,
label: 'Text'
},
{
identifier: MarkType.NUMBER,
icon: fa1,
label: 'Number',
active: false
isComingSoon: true
},
{
identifier: MarkType.INITIALS,
icon: faHeading,
label: 'Initials',
isHidden: true
},
{
identifier: MarkType.DATE,
icon: faCalendarDays,
label: 'Date',
isHidden: true
},
{
identifier: MarkType.IMAGES,
icon: faImage,
label: 'Images',
active: false
isHidden: true
},
{
identifier: MarkType.CHECKBOX,
icon: faSquareCheck,
label: 'Checkbox',
active: false
isHidden: true
},
{
identifier: MarkType.MULTIPLE,
icon: faCheckDouble,
label: 'Multiple',
active: false
isHidden: true
},
{
identifier: MarkType.FILE,
icon: faPaperclip,
label: 'File',
active: false
isHidden: true
},
{
identifier: MarkType.RADIO,
icon: faCircleDot,
label: 'Radio',
active: false
isHidden: true
},
{
identifier: MarkType.SELECT,
icon: faSquareCaretDown,
label: 'Select',
active: false
isHidden: true
},
{
identifier: MarkType.CELLS,
icon: faTableCellsLarge,
label: 'Cells',
active: false
isHidden: true
},
{
identifier: MarkType.STAMP,
icon: faStamp,
label: 'Stamp',
active: false
isHidden: true
},
{
identifier: MarkType.PAYMENT,
icon: faCreditCard,
label: 'Payment',
active: false
isHidden: true
},
{
identifier: MarkType.PHONE,
icon: faPhone,
label: 'Phone',
active: false
isHidden: true
}
]

View File

@ -1,6 +1,6 @@
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
import axios from 'axios'
import _ from 'lodash'
import _, { truncate } from 'lodash'
import {
Event,
EventTemplate,
@ -30,7 +30,7 @@ import {
import { AuthState, Keys } from '../store/auth/types'
import { RelaysState } from '../store/relays/types'
import store from '../store/store'
import { Meta, SignedEvent, UserAppData } from '../types'
import { Meta, ProfileMetadata, SignedEvent, UserAppData } from '../types'
import { getDefaultRelayMap } from './relays'
import { parseJson, removeLeadingSlash } from './string'
import { timeout } from './utils'
@ -974,3 +974,16 @@ export const sendNotification = async (receiver: string, meta: Meta) => {
throw err
})
}
/**
* Show user's name, first available in order: display_name, name, or npub as fallback
* @param npub User identifier, it can be either pubkey or npub1 (we only show npub)
* @param profile User profile
*/
export const getProfileUsername = (
npub: `npub1${string}` | string,
profile?: ProfileMetadata
) =>
truncate(profile?.display_name || profile?.name || hexToNpub(npub), {
length: 16
})