1- import { createContext , type PropsWithChildren , useContext } from 'react'
1+ import {
2+ createContext ,
3+ type PropsWithChildren ,
4+ useCallback ,
5+ useContext ,
6+ useState ,
7+ } from 'react'
28
39import { generateComputedConfig } from '#/lib/community/configGenerator'
410import {
@@ -9,6 +15,8 @@ import {
915import { DISCOVER_FEED_URI , VIDEO_FEED_URI } from '#/lib/constants'
1016import { DEFAULT_BLUE_HUE } from '#/alf/util/colorGeneration'
1117
18+ const BLACKSKY_MODERATION_DID = 'did:plc:d2mkddsbmnrgr3domzg5qexf'
19+
1220// Blacksky defaults used as the dev-mode fallback when no brand config is
1321// injected by bskyweb (i.e. when running `yarn web` locally).
1422const BLACKSKY_CONFIG : RawCommunityConfig = {
@@ -39,6 +47,7 @@ const BLACKSKY_CONFIG: RawCommunityConfig = {
3947 pds : {
4048 url : 'https://blacksky.app' ,
4149 } ,
50+ moderation : [ BLACKSKY_MODERATION_DID ] ,
4251 } ,
4352 feeds : {
4453 defaultPinned : [
@@ -103,15 +112,48 @@ const BLACKSKY_PRIMARY_SCALE = {
103112 primary_975 : '#13133B' ,
104113}
105114
115+ type Pin = { type : string ; value : string ; pinned : boolean }
116+
117+ // The Following timeline is a protocol-level home invariant, not per-community
118+ // configurable data. Guarantee exactly one entry (appended if missing) and drop
119+ // any following entry mis-typed as a feed. Backstops a served config that omits
120+ // Following (stale cache, service-down fallback, or upstream serialization bug).
121+ export function ensureFollowingPinned ( pins : Pin [ ] ) : Pin [ ] {
122+ let hasFollowing = false
123+ const out = pins . filter ( p => {
124+ if ( p . type === 'feed' && p . value === 'following' ) return false // drop corruption
125+ if ( p . type === 'timeline' && p . value === 'following' ) {
126+ if ( hasFollowing ) return false // dedupe
127+ hasFollowing = true
128+ }
129+ return true
130+ } )
131+ if ( ! hasFollowing )
132+ out . push ( { type : 'timeline' , value : 'following' , pinned : true } )
133+ return out
134+ }
135+
136+ function normalizeBrandConfig (
137+ config : ComputedBrandConfig ,
138+ ) : ComputedBrandConfig {
139+ return {
140+ ...config ,
141+ feeds : {
142+ ...config . feeds ,
143+ defaultPinned : ensureFollowingPinned ( config . feeds ?. defaultPinned ?? [ ] ) ,
144+ } ,
145+ }
146+ }
147+
106148// In multi-brand mode, bskyweb injects the config into the HTML before React loads.
107149// In dev mode (yarn web), fall back to the Blacksky defaults above.
108150function resolveBrandConfig ( ) : ComputedBrandConfig {
109151 if ( typeof window !== 'undefined' && window . __BRAND_CONFIG__ ) {
110- return window . __BRAND_CONFIG__
152+ return normalizeBrandConfig ( window . __BRAND_CONFIG__ )
111153 }
112154
113155 const config = generateComputedConfig ( BLACKSKY_CONFIG )
114- return {
156+ return normalizeBrandConfig ( {
115157 ...config ,
116158 theme : {
117159 ...config . theme ,
@@ -125,7 +167,7 @@ function resolveBrandConfig(): ComputedBrandConfig {
125167 selectionDark : '#464985' ,
126168 } ,
127169 } ,
128- }
170+ } )
129171}
130172
131173export const DEFAULT_BRAND_CONFIG = resolveBrandConfig ( )
@@ -181,18 +223,41 @@ if (typeof document !== 'undefined') {
181223}
182224
183225const BrandContext = createContext < ComputedBrandConfig > ( DEFAULT_BRAND_CONFIG )
226+ const SetBrandContext = createContext < ( config : ComputedBrandConfig ) => void > (
227+ ( ) => { } ,
228+ )
184229
185230export function BrandProvider ( {
186231 children,
187232 config,
188233} : PropsWithChildren < { config ?: ComputedBrandConfig } > ) {
234+ // On web the active config is fixed at page load (bskyweb injects it by
235+ // hostname), so this state never changes. On native it starts at the bundled
236+ // Blacksky default and is updated to follow the active account's community —
237+ // see useCommunityBrandSync. Account switches don't remount this provider
238+ // (it sits above the account-keyed subtree), so re-theming rides this state.
239+ const [ current , setCurrent ] = useState < ComputedBrandConfig > (
240+ config || DEFAULT_BRAND_CONFIG ,
241+ )
242+ const setBrandConfig = useCallback ( ( next : ComputedBrandConfig ) => {
243+ setCurrent ( normalizeBrandConfig ( next ) )
244+ } , [ ] )
189245 return (
190- < BrandContext . Provider value = { config || DEFAULT_BRAND_CONFIG } >
191- { children }
192- </ BrandContext . Provider >
246+ < SetBrandContext . Provider value = { setBrandConfig } >
247+ < BrandContext . Provider value = { current } > { children } </ BrandContext . Provider >
248+ </ SetBrandContext . Provider >
193249 )
194250}
195251
196252export function useBrand ( ) : ComputedBrandConfig {
197253 return useContext ( BrandContext )
198254}
255+
256+ /**
257+ * Setter for the active brand config. Used by useCommunityBrandSync (native) to
258+ * re-theme when the active account resolves to a different community. The value
259+ * is normalized (following-pinned) before it is applied.
260+ */
261+ export function useSetBrandConfig ( ) : ( config : ComputedBrandConfig ) => void {
262+ return useContext ( SetBrandContext )
263+ }
0 commit comments