Skip to content

Commit

Permalink
feat: implement community ui
Browse files Browse the repository at this point in the history
  • Loading branch information
beeman committed Sep 27, 2024
1 parent 58d96fc commit 14062eb
Show file tree
Hide file tree
Showing 69 changed files with 712 additions and 292 deletions.
20 changes: 10 additions & 10 deletions anchor/src/pubkey-protocol-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,16 @@ export function getPubkeyProtocolProgramId(cluster: Cluster) {
export const PUBKEY_PROTOCOL_PREFIX = new TextEncoder().encode('pubkey_protocol')
export const PUBKEY_PROTOCOL_SEED_PROFILE = new TextEncoder().encode('profile')
export const PUBKEY_PROTOCOL_SEED_POINTER = new TextEncoder().encode('pointer')
export const PUBKEY_PROTOCOl_SEED_COMMUNITY = new TextEncoder().encode('community')
export const PUBKEY_PROTOCOL_SEED_COMMUNITY = new TextEncoder().encode('community')

// Helper method to get the Community PDA
export function getPubKeyCommunityPda({ programId, slug }: { programId: PublicKey; slug: string }) {
const hash = sha256(
Uint8Array.from([...PUBKEY_PROTOCOL_PREFIX, ...PUBKEY_PROTOCOL_SEED_COMMUNITY, ...stringToUint8Array(slug)]),
)

return PublicKey.findProgramAddressSync([hash], programId)
}

// Helper method to get the PubKeyProfile PDA
export function getPubKeyProfilePda({ programId, username }: { programId: PublicKey; username: string }) {
Expand Down Expand Up @@ -63,15 +72,6 @@ export function getPubKeyPointerPda({
return PublicKey.findProgramAddressSync([hash], programId)
}

// Helper method to get the Community PDA
export function getPubKeyCommunityPda({ programId, slug }: { programId: PublicKey; slug: string }) {
const hash = sha256(
Uint8Array.from([...PUBKEY_PROTOCOL_PREFIX, ...PUBKEY_PROTOCOl_SEED_COMMUNITY, ...stringToUint8Array(slug)]),
)

return PublicKey.findProgramAddressSync([hash], programId)
}

export enum PubKeyIdentityProvider {
Discord = 'Discord',
Github = 'Github',
Expand Down
69 changes: 69 additions & 0 deletions sdk/src/lib/pubkey-protocol-sdk.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { AnchorProvider, Program } from '@coral-xyz/anchor'
import {
getPubKeyCommunityPda,
getPubKeyPointerPda,
getPubKeyProfilePda,
getPubkeyProtocolProgram,
PUBKEY_PROTOCOL_PROGRAM_ID,
PubKeyCommunity,
PubKeyIdentityProvider,
PubKeyPointer,
PubKeyProfile,
Expand All @@ -26,6 +28,12 @@ export interface PubKeyProfileSdkOptions {
readonly provider: AnchorProvider
}

export interface GetCommunityBySlug {
slug: string
}
export interface GetCommunityPdaOptions {
slug: string
}
export interface GetPointerPdaOptions {
provider: PubKeyIdentityProvider
providerId: string
Expand Down Expand Up @@ -75,6 +83,14 @@ export interface AddAuthorityOptions {
username: string
}

export interface CreateCommunityOptions {
avatarUrl: string
authority: PublicKey
feePayer: PublicKey
name: string
slug: string
}

export interface CreateProfileOptions {
avatarUrl: string
authority: PublicKey
Expand Down Expand Up @@ -142,6 +158,23 @@ export class PubkeyProtocolSdk {
return this.createTransaction({ ix, feePayer })
}

async createCommunity({ authority, avatarUrl, feePayer, name, slug }: CreateCommunityOptions) {
const [community] = this.getCommunityPda({ slug })

const ix = await this.program.methods
.createCommunity({ avatarUrl, name, slug, discord: '', github: '', website: '', x: '' })
.accountsStrict({
authority,
feePayer,
community,
pointer: PublicKey.default,
systemProgram: SystemProgram.programId,
})
.instruction()

return this.createTransaction({ ix, feePayer })
}

async createProfile({ authority, avatarUrl, feePayer, name, username }: CreateProfileOptions) {
const [profile] = this.getProfilePda({ username })
const [pointer] = this.getPointerPda({ provider: PubKeyIdentityProvider.Solana, providerId: authority.toString() })
Expand All @@ -159,6 +192,42 @@ export class PubkeyProtocolSdk {
return this.createTransaction({ ix, feePayer })
}

async getCommunity({ communityPda }: { communityPda: PublicKey }): Promise<PubKeyCommunity> {
return this.program.account.community.fetch(communityPda).then((res) => {
// FIXME: Properly clean up the PubKey Community Data.
return {
...res,
publicKey: communityPda,
providers: [],
} as PubKeyCommunity
})
}

async getCommunities(): Promise<PubKeyCommunity[]> {
return this.program.account.community.all().then((accounts) =>
accounts.map(({ account, publicKey }) => {
// FIXME: Properly clean up the PubKey Community Data.
return {
publicKey,
avatarUrl: account.avatarUrl,
bump: account.bump,
name: account.name,
slug: account.slug,
} as PubKeyCommunity
}),
)
}

async getCommunityBySlug({ slug }: GetCommunityBySlug): Promise<PubKeyCommunity> {
const [communityPda] = this.getCommunityPda({ slug })

return this.getCommunity({ communityPda })
}

getCommunityPda({ slug }: GetCommunityPdaOptions): [PublicKey, number] {
return getPubKeyCommunityPda({ programId: this.programId, slug })
}

async getProfiles(): Promise<PubKeyProfile[]> {
return this.program.account.profile.all().then((accounts) =>
accounts.map(({ account, publicKey }) => ({
Expand Down
16 changes: 13 additions & 3 deletions web/src/app/app-routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,22 @@ import { WalletIcon } from './features/solana/solana-provider'

const ClusterFeature = lazy(() => import('./features/cluster/cluster-feature'))

const PubkeyProfileFeature = lazy(() => import('./features/pubkey-protocol/feature/pubkey-protocol.routes'))
const links: UiHeaderLink[] = [{ label: 'PubKey Protocol', link: '/pubkey-protocol' }]
const PubkeyCommunityFeature = lazy(() => import('./features/pubkey-community/feature/pubkey-community.routes'))
const PubkeyProfileFeature = lazy(() => import('./features/pubkey-profile/feature/pubkey-profile.routes'))
const PubkeyProtocolFeature = lazy(() => import('./features/pubkey-protocol/feature/pubkey-protocol.routes'))

const links: UiHeaderLink[] = [
{ label: 'Communities', link: '/communities' },
{ label: 'Profiles', link: '/profiles' },
{ label: 'Debug', link: '/debug' },
]

const routes: RouteObject[] = [
{ path: '/clusters', element: <ClusterFeature /> },
{ path: '/keypairs/*', element: <KeypairFeature /> },
{ path: '/pubkey-protocol/*', element: <PubkeyProfileFeature basePath="/pubkey-protocol" /> },
{ path: '/communities/*', element: <PubkeyCommunityFeature basePath="/communities" /> },
{ path: '/profiles/*', element: <PubkeyProfileFeature basePath="/profiles" /> },
{ path: '/debug/*', element: <PubkeyProtocolFeature /> },
]

export function AppRoutes() {
Expand Down
2 changes: 1 addition & 1 deletion web/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { AppLabelsProvider } from './app-labels-provider'
import { AppRoutes, ThemeLink } from './app-routes'
import { ClusterProvider } from './features/cluster/cluster-data-access'
import { KeypairProvider } from './features/keypair/data-access'
import { SolanaProvider } from './features/solana/solana-provider'
import { SolanaProvider } from './features/solana'

const client = new QueryClient()

Expand Down
10 changes: 5 additions & 5 deletions web/src/app/features/cluster/cluster-data-access.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function ClusterProvider({ children }: { children: ReactNode }) {
setClusters(clusters.filter((item) => item.name !== cluster.name))
},
setCluster: (cluster: Cluster) => setCluster(cluster),
getExplorerUrl: (path: string) => `https://solana.fm/${path}${getClusterUrlParam(cluster)}`,
getExplorerUrl: (path: string) => `https://explorer.solana.com/${path}${getClusterUrlParam(cluster)}`,
}
return <Context.Provider value={value}>{children}</Context.Provider>
}
Expand All @@ -89,16 +89,16 @@ function getClusterUrlParam(cluster: Cluster): string {
let suffix = ''
switch (cluster.network) {
case ClusterNetwork.Devnet:
suffix = 'devnet-solana'
suffix = 'devnet'
break
case ClusterNetwork.Mainnet:
suffix = 'mainnet-solana'
suffix = 'mainnet'
break
case ClusterNetwork.Testnet:
suffix = 'testnet-solana'
suffix = 'testnet'
break
default:
suffix = `localnet-solana&customUrl=${encodeURIComponent(cluster.endpoint)}`
suffix = `custom&customUrl=${encodeURIComponent(cluster.endpoint)}`
break
}

Expand Down
3 changes: 3 additions & 0 deletions web/src/app/features/pubkey-community/data-access/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './use-mutation-create-community'
export * from './use-query-get-communities'
export * from './use-query-get-community-by-slug'
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CreateCommunityOptions } from '@pubkey-protocol/sdk'
import { useMutation } from '@tanstack/react-query'
import { usePubKeyProtocol } from '../../pubkey-protocol'

export type PubKeyCommunityCreateInput = Omit<CreateCommunityOptions, 'authority' | 'feePayer'>

export function useMutationCreateCommunity() {
const { authority, feePayer, sdk, signAndConfirmTransaction, onError, onSuccess } = usePubKeyProtocol()

return useMutation({
mutationFn: (options: PubKeyCommunityCreateInput) =>
sdk
.createCommunity({
...options,
avatarUrl: options.avatarUrl || `https://api.dicebear.com/9.x/bottts-neutral/svg?seed=${options.slug}`,
authority,
feePayer,
})
.then(signAndConfirmTransaction),
onError,
onSuccess,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useQuery } from '@tanstack/react-query'
import { usePubKeyProtocol } from '../../pubkey-protocol'

export function useQueryGetCommunities() {
const { cluster, sdk } = usePubKeyProtocol()

return useQuery({
queryKey: ['pubkey-protocol', 'getCommunities', { cluster }],
queryFn: () => sdk.getCommunities(),
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useQuery } from '@tanstack/react-query'
import { usePubKeyProtocol } from '../../pubkey-protocol'

export function useQueryGetCommunityBySlug({ slug }: { slug: string }) {
const { sdk, cluster } = usePubKeyProtocol()

return useQuery({
queryKey: ['pubkey-protocol', 'getCommunityBySlug', { cluster, slug }],
queryFn: () => sdk.getCommunityBySlug({ slug }),
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { toastError, toastSuccess, UiCard, UiPage } from '@pubkey-ui/core'
import { IconUserPlus } from '@tabler/icons-react'
import { useMutationCreateCommunity } from '../data-access'
import { PubkeyProtocolUiCommunityCreateForm } from '../ui'

export function PubkeyCommunityFeatureCreate() {
const mutation = useMutationCreateCommunity()

return (
<UiPage leftAction={<IconUserPlus />} title="Create Community">
<UiCard title="Create Community">
<PubkeyProtocolUiCommunityCreateForm
submit={(input) =>
mutation
.mutateAsync(input)
.then(() => toastSuccess(`Community created`))
.catch((err) => toastError(`Error: ${err}`))
}
/>
</UiCard>
</UiPage>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { UiLoader, UiPage, UiWarning } from '@pubkey-ui/core'
import { IconUsers } from '@tabler/icons-react'
import { useParams } from 'react-router-dom'
import { useQueryGetCommunityBySlug } from '../data-access'
import { PubkeyProtocolUiCommunityCard } from '../ui'

export function PubkeyCommunityFeatureDetail() {
const { slug } = useParams() as { slug: string }

const query = useQueryGetCommunityBySlug({ slug })

return (
<UiPage leftAction={<IconUsers />} title={slug}>
{query.isLoading ? (
<UiLoader />
) : query.data ? (
<PubkeyProtocolUiCommunityCard community={query.data} />
) : (
<UiWarning message="User not found" />
)}
</UiPage>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { UiDebug, UiLoader, UiPage, UiStack } from '@pubkey-ui/core'
import { IconUsers } from '@tabler/icons-react'
import { useQueryGetCommunities } from '../data-access'
import { PubkeyProtocolUiCommunityGrid } from '../ui/pubkey-protocol-ui-community-grid'

export function PubkeyCommunityFeatureList({ basePath }: { basePath: string }) {
const query = useQueryGetCommunities()

return (
<UiPage leftAction={<IconUsers />} title="Communities">
{query.isLoading ? (
<UiLoader />
) : (
<UiStack>
<PubkeyProtocolUiCommunityGrid communities={query.data ?? []} basePath={basePath} />
<UiDebug data={query.data ?? []} />
</UiStack>
)}
</UiPage>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { IconUsers, IconUsersPlus } from '@tabler/icons-react'
import { Navigate, useRoutes } from 'react-router-dom'
import { KeypairUiGridItem } from '../../keypair/ui'
import { PubkeyCommunityFeatureList } from './pubkey-community-feature-list'
import { PubkeyCommunityFeatureCreate } from './pubkey-community-feature-create'
import { PubkeyCommunityFeatureDetail } from './pubkey-community-feature-detail'
import { PubkeyProtocolProvider } from '../../pubkey-protocol'
import { UiSidebar } from '../../../ui'
import { SolanaConnectionLoader } from '../../solana'

export default function PubkeyCommunityRoutes({ basePath }: { basePath: string }) {
const sidebar: KeypairUiGridItem[] = [
{
label: 'Communities',
path: 'list',
leftSection: <IconUsers size={16} />,
},
{
label: 'Create',
path: 'create',
leftSection: <IconUsersPlus size={16} />,
},
]
const routes = useRoutes([
{ index: true, element: <Navigate to="list" replace /> },
{ path: 'list', element: <PubkeyCommunityFeatureList basePath={`${basePath}/profiles`} /> },
{ path: 'create', element: <PubkeyCommunityFeatureCreate /> },
{ path: ':username', element: <PubkeyCommunityFeatureDetail /> },
])

return (
<SolanaConnectionLoader
render={(props) => (
<PubkeyProtocolProvider {...props}>
<UiSidebar basePath={basePath} routes={sidebar}>
{routes}
</UiSidebar>
</PubkeyProtocolProvider>
)}
/>
)
}
4 changes: 4 additions & 0 deletions web/src/app/features/pubkey-community/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './pubkey-protocol-ui-community-anchor'
export * from './pubkey-protocol-ui-community-avatar'
export * from './pubkey-protocol-ui-community-card'
export * from './pubkey-protocol-ui-community-create-form'
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Anchor, Text } from '@mantine/core'
import { Link } from 'react-router-dom'

export function PubkeyProtocolUiCommunityAnchor({ slug, basePath }: { slug: string; basePath?: string }) {
return basePath ? (
<Anchor component={Link} to={`${basePath}/communities/${slug}`} size="xl" fw="bold">
{slug}
</Anchor>
) : (
<Text size="xl" fw="bold">
{slug}
</Text>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { PubKeyCommunity } from '@pubkey-protocol/anchor'
import { UiAvatar } from '../../../ui/ui-avatar'

export function PubkeyProtocolUiCommunityAvatar({ community: { avatarUrl, slug } }: { community: PubKeyCommunity }) {
return <UiAvatar url={avatarUrl ? avatarUrl : null} name={slug} radius={100} size="lg" />
}
Loading

0 comments on commit 14062eb

Please sign in to comment.