Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement community ui #22

Merged
merged 3 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading