Skip to content

Commit

Permalink
react-sdk: auth status, space, channels, actions (#326)
Browse files Browse the repository at this point in the history
This PR adds

- [x] choose environment
- [x] fund wallet
- [x] log in
- [x] create space
- [x] render spaces
- [x] render space channels
- [x] render timelines
- [x] send message in channel

Also changes the implementation of `useObservable` to start using
`useSyncExternalStore`

---------

Co-authored-by: texuf <[email protected]>
  • Loading branch information
miguel-nascimento and texuf authored Jul 26, 2024
1 parent 59fb24f commit 64bb70e
Show file tree
Hide file tree
Showing 46 changed files with 1,722 additions and 166 deletions.
22 changes: 22 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"Casablanca",
"Casablanca-No-Entitlements",
"XChain-Single",
"Playground",
],
"group": {
"kind": "build",
Expand All @@ -79,6 +80,7 @@
"WatchCasablancaProto",
"WatchCasablancaWeb3",
"WatchCasablancaSdk",
"WatchReactSdk",
],
"group": {
"kind": "build",
Expand Down Expand Up @@ -358,6 +360,16 @@
"panel": "shared",
}
},
{
"label": "Playground",
"type": "shell",
"command": "cd packages/playground && yarn dev",
"isBackground": true,
"problemMatcher": [],
"presentation": {
"group": "apps"
}
},
{
"label": "WatchCasablancaSdk",
"type": "shell",
Expand All @@ -368,6 +380,16 @@
"group": "local-watch"
}
},
{
"label": "WatchReactSdk",
"type": "shell",
"command": "cd packages/react-sdk && yarn watch",
"isBackground": true,
"problemMatcher": [],
"presentation": {
"group": "local-watch"
}
},
{
"label": "WatchCasablancaEncryption",
"type": "shell",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"build:react": "turbo build --filter @river-build/react-sdk",
"build:metrics-discovery": "turbo build --filter @river-build/metrics-discovery",
"cast": "yarn workspace @river-build/contracts exec cast",
"csb:build": "yarn workspace @river-build/proto run build && yarn workspace @river-build/dlog run build && yarn workspace @river-build/web3 run build && yarn workspace @river-build/encryption run build && yarn workspace @river-build/sdk run build && echo BUILD DONE || (echo BUILD ERROR; exit 1)",
"csb:build": "yarn workspace @river-build/proto run build && yarn workspace @river-build/dlog run build && yarn workspace @river-build/web3 run build && yarn workspace @river-build/encryption run build && yarn workspace @river-build/sdk run build && yarn workspace @river-build/react-sdk run build && echo BUILD DONE || (echo BUILD ERROR; exit 1)",
"clean": "./scripts/yarn-clean.sh",
"csb:cb": "yarn csb:clean && yarn csb:build",
"csb:clean": "yarn csb:command run clean",
Expand Down
4 changes: 4 additions & 0 deletions packages/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,27 @@
"dependencies": {
"@hookform/resolvers": "^3.6.0",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@river-build/react-sdk": "workspace:^",
"@river-build/sdk": "workspace:^",
"@tanstack/react-query": "^5.51.15",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"ethers": "^5.7.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.38.0",
"react-router-dom": "^6.4.2",
"superjson": "^2.2.1",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"viem": "^1.18.2",
"vite-plugin-node-polyfills": "^0.22.0",
"wagmi": "^1.4.12",
"zod": "^3.21.4"
},
Expand Down
40 changes: 25 additions & 15 deletions packages/playground/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
import { RouterProvider } from 'react-router-dom'
import { WagmiConfig, configureChains, createConfig, mainnet } from 'wagmi'
import { publicProvider } from 'wagmi/providers/public'
import { InjectedConnector } from 'wagmi/connectors/injected'
import { RiverSyncProvider } from '@river-build/react-sdk'
import { router } from './routes'
import { WagmiConfig } from 'wagmi'

const { publicClient, webSocketPublicClient } = configureChains([mainnet], [publicProvider()])
import { RiverSyncProvider, connectRiver } from '@river-build/react-sdk'

const config = createConfig({
autoConnect: true,
publicClient,
webSocketPublicClient,
connectors: [new InjectedConnector()],
})
import { useEffect, useState } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { type SyncAgent } from '@river-build/sdk'
import { router } from './routes'
import { config } from './config/wagmi'
import { loadAuth } from './utils/persist-auth'

function App() {
const [queryClient] = useState(() => new QueryClient())
const [syncAgent, setSyncAgent] = useState<SyncAgent | undefined>()

useEffect(() => {
const auth = loadAuth()
if (auth) {
connectRiver(auth.signerContext, { riverConfig: auth.riverConfig }).then((syncAgent) =>
setSyncAgent(syncAgent),
)
}
}, [])

return (
<WagmiConfig config={config}>
<RiverSyncProvider>
<RouterProvider router={router} />
</RiverSyncProvider>
<QueryClientProvider client={queryClient}>
<RiverSyncProvider syncAgent={syncAgent}>
<RouterProvider router={router} />
</RiverSyncProvider>
</QueryClientProvider>
</WagmiConfig>
)
}
Expand Down
7 changes: 7 additions & 0 deletions packages/playground/src/components/blocks/auth-block.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useRiverAuthStatus } from '@river-build/react-sdk'
import { Block } from '../ui/block'

export const UserAuthStatusBlock = () => {
const { status } = useRiverAuthStatus()
return <Block title="User Auth Status">{JSON.stringify(status, null, 2)}</Block>
}
128 changes: 128 additions & 0 deletions packages/playground/src/components/blocks/channels.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { useChannel, useCreateChannel, useSpace } from '@river-build/react-sdk'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { useEthersSigner } from '@/utils/viem-to-ethers'
import { useCurrentSpaceId } from '@/hooks/current-space'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '../ui/form'
import { Block, type BlockProps } from '../ui/block'
import { Button } from '../ui/button'
import { Input } from '../ui/input'
import { JsonHover } from '../utils/json-hover'

type ChannelsBlockProps = {
changeChannel: (channelId: string) => void
}

const formSchema = z.object({
channelName: z.string().min(1, { message: 'Space name is required' }),
})

export const ChannelsBlock = ({ changeChannel }: ChannelsBlockProps) => {
const spaceId = useCurrentSpaceId()
const { data: space } = useSpace(spaceId)

return (
<Block title={`Channels in ${space.metadata?.name || 'Unnamed Space'}`}>
<CreateChannel variant="secondary" spaceId={spaceId} onChannelCreated={changeChannel} />
<div className="flex flex-col gap-1">
<span className="text-xs">Select a channel to start messaging</span>
{space.channelIds.map((channelId) => (
<ChannelInfo
key={`${spaceId}-${channelId}`}
spaceId={space.id}
channelId={channelId}
changeChannel={changeChannel}
/>
))}
</div>
{space.channelIds.length === 0 && (
<p className="pt-4 text-center text-sm text-secondary-foreground">
You're not in any Channels yet.
</p>
)}
</Block>
)
}

const ChannelInfo = ({
spaceId,
channelId,
changeChannel,
}: {
spaceId: string
channelId: string
changeChannel: (channelId: string) => void
}) => {
const { data: channel } = useChannel(spaceId, channelId)

return (
<JsonHover data={channel}>
<div>
<Button variant="outline" onClick={() => changeChannel(channelId)}>
{channel.metadata?.name || 'Unnamed Channel'}
</Button>
</div>
</JsonHover>
)
}

export const CreateChannel = (
props: {
spaceId: string
onChannelCreated: (channelId: string) => void
} & BlockProps,
) => {
const { onChannelCreated, spaceId, ...rest } = props
const { createChannel, isPending } = useCreateChannel(spaceId)
const signer = useEthersSigner()
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { channelName: '' },
})

// TODO: this should be a dialog
return (
<Block {...rest}>
<Form {...form}>
<form
className="space-y-8"
onSubmit={form.handleSubmit(async ({ channelName }) => {
if (!signer) {
return
}
const channelId = await createChannel(channelName, signer)
onChannelCreated(channelId)
})}
>
<FormField
control={form.control}
name="channelName"
render={({ field }) => (
<FormItem>
<FormLabel>New channel name</FormLabel>
<FormControl>
{/* TODO: input mask so it start with # but gets stripped */}
<Input placeholder="#cool-photos" {...field} />
</FormControl>
<FormDescription>
This will be the name of your channel.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit"> {isPending ? 'Creating...' : 'Create'}</Button>
</form>
</Form>
</Block>
)
}
17 changes: 17 additions & 0 deletions packages/playground/src/components/blocks/connection-block.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useRiver, useRiverConnection } from '@river-build/react-sdk'
import { Block } from '../ui/block'

export const ConnectionBlock = () => {
const { isConnected } = useRiverConnection()
const { data: nodeUrls } = useRiver((s) => s.riverStreamNodeUrls)

return (
<Block title={`Sync Connection ${isConnected ? '✅' : '❌'}`} className="rounded-lg">
<Block variant="secondary">
<pre className="overflow-auto whitespace-pre-wrap">
{JSON.stringify(nodeUrls, null, 2)}
</pre>
</Block>
</Block>
)
}
Loading

0 comments on commit 64bb70e

Please sign in to comment.