add identity to account id picker and display

This commit is contained in:
Victor Oliva
2024-11-05 15:25:23 +01:00
parent d1061beaca
commit a348644175
5 changed files with 108 additions and 47 deletions

View File

@@ -1,11 +1,12 @@
import { PolkadotIdenticon } from "@/components/PolkadotIdenticon"
import { useSynchronizeInput } from "@/components/useSynchroniseInput"
import {
accountDetail$,
accounts$,
getAccountMapKey,
getPublicKey,
} from "@/extension-accounts.state"
import { PolkadotIdenticon } from "@/components/PolkadotIdenticon"
import { useSynchronizeInput } from "@/components/useSynchroniseInput"
import { identity$, isVerified } from "@/identity.state"
import { EditAccountId, NOTIN } from "@codec-components"
import {
Combobox,
@@ -18,17 +19,51 @@ import {
getSs58AddressInfo,
SS58String,
} from "@polkadot-api/substrate-bindings"
import { useStateObservable } from "@react-rxjs/core"
import { state, useStateObservable } from "@react-rxjs/core"
import { combineKeys } from "@react-rxjs/utils"
import { CheckCircle } from "lucide-react"
import { FC, useState } from "react"
import { map, switchMap, take } from "rxjs"
import { twMerge } from "tailwind-merge"
const hintedAccounts$ = state(
combineKeys(
accounts$.pipe(map((accounts) => [...accounts.keys()])),
(account) =>
accounts$.pipe(
map((v) => v.get(account)!),
take(1),
switchMap((details) =>
identity$(details.address).pipe(
map((identity) => ({
address: details.address,
name: identity?.displayName ?? details.name,
isVerified: isVerified(identity),
})),
),
),
),
).pipe(map((v) => new Map(v))),
new Map<
string,
{
address: string
name: string | undefined
isVerified: boolean | undefined
}
>(),
)
export const CAccountId: EditAccountId = ({ value, onValueChanged }) => {
const [localInput, setLocalInput] = useSynchronizeInput(
value,
onValueChanged,
parseValue,
)
const accounts = useStateObservable(accounts$)
const accounts = useStateObservable(hintedAccounts$)
const valueIdentity = useStateObservable(
identity$(value === NOTIN ? "" : value),
)
const [query, setQuery] = useState("")
const queryInfo = getSs58AddressInfo(query)
@@ -64,10 +99,12 @@ export const CAccountId: EditAccountId = ({ value, onValueChanged }) => {
/>
)}
<ComboboxInput
className="bg-transparent outline-none w-full"
className="bg-transparent outline-none w-full select-all"
aria-label="AccountId"
displayValue={(address: string) =>
accounts.get(getAccountMapKey(address))?.name ?? address
valueIdentity?.displayName ??
accounts.get(getAccountMapKey(address))?.name ??
address
}
onChange={(event) => setQuery(event.target.value)}
/>
@@ -103,6 +140,9 @@ const AccountOption: FC<{ account: string; publicKey: Uint8Array | null }> = ({
publicKey,
}) => {
const details = useStateObservable(accountDetail$(account))
const identity = useStateObservable(identity$(account))
const name = identity?.displayName ?? details?.name
return (
<ComboboxOption
@@ -115,7 +155,14 @@ const AccountOption: FC<{ account: string; publicKey: Uint8Array | null }> = ({
size={32}
/>
<div className="flex flex-col justify-center text-white leading-tight overflow-hidden">
{details?.name && <span>{details.name}</span>}
{name && (
<span className="inline-flex items-center gap-1">
{name}
{isVerified(identity) && (
<CheckCircle size={16} className="text-green-400" />
)}
</span>
)}
<span className="text-slate-400 text-ellipsis overflow-hidden">
{account}
</span>

View File

@@ -1,6 +1,8 @@
import { accountDetail$, getPublicKey } from "@/extension-accounts.state"
import { PolkadotIdenticon } from "@/components/PolkadotIdenticon"
import { accountDetail$, getPublicKey } from "@/extension-accounts.state"
import { identity$, isVerified } from "@/identity.state"
import { useStateObservable } from "@react-rxjs/core"
import { CheckCircle } from "lucide-react"
import { SS58String } from "polkadot-api"
import { FC } from "react"
import { twMerge } from "tailwind-merge"
@@ -10,6 +12,9 @@ export const AccountIdDisplay: FC<{
className?: string
}> = ({ value, className }) => {
const details = useStateObservable(accountDetail$(value))
const identity = useStateObservable(identity$(value))
const name = identity?.displayName ?? details?.name
return (
<div className={twMerge("flex items-center gap-2", className)}>
@@ -19,7 +24,14 @@ export const AccountIdDisplay: FC<{
size={32}
/>
<div className="flex flex-col justify-center text-white leading-tight overflow-hidden">
{details?.name && <span>{details.name}</span>}
{name && (
<span className="inline-flex items-center gap-1">
{name}
{isVerified(identity) && (
<CheckCircle size={16} className="text-green-400" />
)}
</span>
)}
<span className="text-slate-300 text-ellipsis overflow-hidden">
{value}
</span>

View File

@@ -37,7 +37,7 @@ export const SearchableSelect = <T,>({
>
<div className="flex flex-row p-2 rounded border border-polkadot-200 text-white bg-black">
<ComboboxInput
className="text-ellipsis overflow-hidden whitespace-nowrap focus:outline-none"
className="text-ellipsis overflow-hidden whitespace-nowrap focus:outline-none select-all"
displayValue={(option: T) =>
options.find((_option) => _option.value === option)?.text ?? ""
}

View File

@@ -3,21 +3,23 @@ import {
IdentityJudgement,
polkadot_people,
} from "@polkadot-api/descriptors"
import { state } from "@react-rxjs/core"
import { Binary, createClient, SS58String } from "polkadot-api"
import { chainSpec as relayChain } from "polkadot-api/chains/polkadot"
import { chainSpec } from "polkadot-api/chains/polkadot_people"
import { localStorageSubject } from "./utils/localStorageSubject"
import { Binary, createClient, SS58String } from "polkadot-api"
import { catchError, map, of, tap } from "rxjs"
import { getProvider } from "./chain.state"
import { state } from "@react-rxjs/core"
import { filter, map, merge, take, tap } from "rxjs"
import { localStorageSubject } from "./utils/localStorageSubject"
interface Identity {
export interface Identity {
displayName: string
judgments: Array<{
registrar: number
judgement: IdentityJudgement["type"]
}>
}
export const isVerified = (identity: Identity | null) =>
identity?.judgments.some((j) => j.judgement === "Reasonable")
const cache = localStorageSubject<Record<string, Identity>>(
"identity-cache",
@@ -37,41 +39,38 @@ const client = createClient(
)
const typedApi = client.getTypedApi(polkadot_people)
export const identity$ = state((address: SS58String) => {
const cached$ = cache.stream$.pipe(
take(1),
map((v) => v[address]),
filter((v) => !!v),
)
const chain$ = typedApi.query.Identity.IdentityOf.watchValue(address).pipe(
map((res): Identity | null =>
res
? {
displayName: readIdentityData(res[0].info.display).asText(),
judgments: res[0].judgements.map(([registrar, judgement]) => ({
registrar,
judgement: judgement.type,
})),
}
: null,
),
tap((v) =>
cache.setValue((c) => {
if (v) {
return { ...c, [address]: v }
} else {
delete c[address]
return c
}
export const identity$ = state(
(address: SS58String) =>
typedApi.query.Identity.IdentityOf.watchValue(address).pipe(
map((res): Identity | null => {
const displayName = res && readIdentityData(res[0].info.display)
return displayName
? {
displayName: displayName.asText(),
judgments: res[0].judgements.map(([registrar, judgement]) => ({
registrar,
judgement: judgement.type,
})),
}
: null
}),
tap((v) =>
cache.setValue((c) => {
if (v) {
return { ...c, [address]: v }
} else {
delete c[address]
return c
}
}),
),
catchError(() => of(null)),
),
)
return merge(cached$, chain$)
}, null)
(address) => cache.getValue()[address] ?? null,
)
const readIdentityData = (identityData: IdentityData): Binary => {
if (identityData.type === "None" || identityData.type === "Raw0")
return Binary.fromHex("")
const readIdentityData = (identityData: IdentityData): Binary | null => {
if (identityData.type === "None" || identityData.type === "Raw0") return null
if (identityData.type === "Raw1")
return Binary.fromBytes(new Uint8Array(identityData.value))
return identityData.value

View File

@@ -11,6 +11,7 @@ interface LocalStorageSubject {
): {
stream$: Observable<R | null>
setValue: (v: R | ((old: R) => R)) => void
getValue: () => R | null
clear: () => void
}
<R>(
@@ -20,6 +21,7 @@ interface LocalStorageSubject {
): {
stream$: Observable<R>
setValue: (v: R | ((old: R) => R)) => void
getValue: () => R
clear: () => void
}
}
@@ -44,6 +46,7 @@ export const localStorageSubject: LocalStorageSubject = (
localStorage.setItem(key, formatter.stringify(newValue))
subject.next(newValue)
},
getValue: () => subject.getValue(),
clear: () => {
localStorage.removeItem(key)
subject.next(defaultValue)