add identity to account id picker and display
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ?? ""
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user