mirror of
https://github.com/hanzoai/social.git
synced 2026-10-17 11:18:21 +00:00
A social publishing surface on @hanzo/gui + @hanzo/ui, written against the platform's own /v1/social and /v1/integration. Six screens: the org roll-up, a composer, the posts in every state, a month calendar, the networks with their consent and their handles, and the delivery record. Two facts shape it. A post carries one channel, so the composer stores one post per network — which is also what makes writing separately for a single network free, since it is a different content on that row. Media is a list of addresses, so a video is ordinary content rather than a second kind of post, and each network's own rules are checked before the post is stored rather than discovered at a publish hours later. State is drawn in luminance on a palette with no hues, and the inversion is spent once per screen on the thing a person has to act on. Refusals are the platform's own sentence, read off the RFC 7807 detail the SDK's extractor drops — a 503 that names the missing credentials is the whole of what anyone can act on. Co-authored-by: Hanzo Dev <[email protected]>
92 lines
3.2 KiB
TypeScript
92 lines
3.2 KiB
TypeScript
// The platform client, and the tenant it speaks for.
|
|
//
|
|
// ONE CLIENT, mounted once, so no screen carries a second. A second client is a
|
|
// second credential and a second organization, and the two disagree the moment
|
|
// either changes.
|
|
//
|
|
// SIGNED OUT THERE IS NO CLIENT. Every /v1/social route requires a validated
|
|
// principal and answers 403 without one, so an anonymous client would exist only
|
|
// to be refused — every screen would fill with a failure that is really just
|
|
// "nobody is signed in". `useHttp()` answers null instead and each screen says
|
|
// so once, in its own words.
|
|
|
|
import { createContext, useContext, useMemo, useState, type ReactNode } from 'react'
|
|
import { useIam } from '@hanzo/iam/react'
|
|
import { AiProvider } from '@hanzo/ai/react'
|
|
import { createAiClient, type HttpClient } from '@hanzo/ai'
|
|
import { api } from '~/api'
|
|
import { bearer, hasSession, org, orgs, work } from '~/token'
|
|
|
|
/** The tenant this browser works in, and the way to move to another. */
|
|
export interface Tenant {
|
|
/** The organization every read and write is scoped to, or null when the
|
|
* account belongs to several and has not picked. */
|
|
org: string | null
|
|
/** Every organization the token carries. One is not a choice. */
|
|
orgs: string[]
|
|
choose: (org: string) => void
|
|
}
|
|
|
|
const Who = createContext<Tenant>({ org: null, orgs: [], choose: () => {} })
|
|
const Transport = createContext<HttpClient | null>(null)
|
|
|
|
/** The transport every read and write on this surface goes through, or null
|
|
* when nobody is signed in. */
|
|
export function useHttp(): HttpClient | null {
|
|
return useContext(Transport)
|
|
}
|
|
|
|
/** The tenant, and the switch. */
|
|
export function useTenant(): Tenant {
|
|
return useContext(Who)
|
|
}
|
|
|
|
export function Client({ children }: { children: ReactNode }) {
|
|
const { sdk, isAuthenticated } = useIam()
|
|
// The selection is REACT STATE as well as a stored key. Storage events fire
|
|
// in other tabs only, so a switch made here would move the key and leave this
|
|
// tab's client scoped to the organization it was built with.
|
|
const [picked, setPicked] = useState<string | null>(() => org())
|
|
const mine = useMemo(() => orgs(), [isAuthenticated, picked])
|
|
|
|
const account = Boolean((isAuthenticated || hasSession()) && sdk)
|
|
|
|
const client = useMemo(
|
|
() =>
|
|
account && sdk
|
|
? createAiClient({
|
|
baseUrl: api(),
|
|
headers: picked ? { 'X-Org-Id': picked } : {},
|
|
auth: {
|
|
...sdk,
|
|
getValidAccessToken: async () => (await sdk.getValidAccessToken?.()) || bearer(),
|
|
},
|
|
})
|
|
: null,
|
|
[account, sdk, picked],
|
|
)
|
|
|
|
const tenant = useMemo<Tenant>(
|
|
() => ({
|
|
org: picked,
|
|
orgs: mine,
|
|
choose: (pick) => {
|
|
work(pick)
|
|
setPicked(pick)
|
|
},
|
|
}),
|
|
[picked, mine],
|
|
)
|
|
|
|
// AiProvider as well as the transport context: @hanzo/ui's shared components
|
|
// read the client from it, and a component that finds none renders its own
|
|
// signed-out state rather than throwing.
|
|
return (
|
|
<Who.Provider value={tenant}>
|
|
<Transport.Provider value={client ? client.http : null}>
|
|
{client ? <AiProvider client={client}>{children}</AiProvider> : children}
|
|
</Transport.Provider>
|
|
</Who.Provider>
|
|
)
|
|
}
|