forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import React from 'react'
2import {
3 type AppBskyFeedDefs,
4 type AppBskyGraphDefs,
5 AppBskyGraphStarterpack,
6} from '@atproto/api'
7import {msg, plural} from '@lingui/core/macro'
8
9import {STARTER_PACK_MAX_SIZE} from '#/lib/constants'
10import * as Toast from '#/view/com/util/Toast'
11import * as bsky from '#/types/bsky'
12
13const steps = ['Details', 'Profiles', 'Feeds'] as const
14type Step = (typeof steps)[number]
15
16type Action =
17 | {type: 'Next'}
18 | {type: 'Back'}
19 | {type: 'SetCanNext'; canNext: boolean}
20 | {type: 'SetName'; name: string}
21 | {type: 'SetDescription'; description: string}
22 | {type: 'AddProfile'; profile: bsky.profile.AnyProfileView}
23 | {type: 'RemoveProfile'; profileDid: string}
24 | {type: 'AddFeed'; feed: AppBskyFeedDefs.GeneratorView}
25 | {type: 'RemoveFeed'; feedUri: string}
26 | {type: 'SetProcessing'; processing: boolean}
27 | {type: 'SetError'; error: string}
28
29interface State {
30 canNext: boolean
31 currentStep: Step
32 name?: string
33 description?: string
34 profiles: bsky.profile.AnyProfileView[]
35 feeds: AppBskyFeedDefs.GeneratorView[]
36 processing: boolean
37 error?: string
38 transitionDirection: 'Backward' | 'Forward'
39 targetDid?: string
40}
41
42type TStateContext = [State, (action: Action) => void]
43
44const StateContext = React.createContext<TStateContext>([
45 {} as State,
46 (_: Action) => {},
47])
48StateContext.displayName = 'StarterPackWizardStateContext'
49export const useWizardState = () => React.useContext(StateContext)
50
51function reducer(state: State, action: Action): State {
52 let updatedState = state
53
54 // -- Navigation
55 const currentIndex = steps.indexOf(state.currentStep)
56 if (action.type === 'Next' && state.currentStep !== 'Feeds') {
57 updatedState = {
58 ...state,
59 currentStep: steps[currentIndex + 1],
60 transitionDirection: 'Forward',
61 }
62 } else if (action.type === 'Back' && state.currentStep !== 'Details') {
63 updatedState = {
64 ...state,
65 currentStep: steps[currentIndex - 1],
66 transitionDirection: 'Backward',
67 }
68 }
69
70 switch (action.type) {
71 case 'SetName':
72 updatedState = {...state, name: action.name.slice(0, 50)}
73 break
74 case 'SetDescription':
75 updatedState = {...state, description: action.description}
76 break
77 case 'AddProfile':
78 if (state.profiles.length > STARTER_PACK_MAX_SIZE) {
79 Toast.show(
80 msg`You may only add up to ${plural(STARTER_PACK_MAX_SIZE, {
81 other: `${STARTER_PACK_MAX_SIZE} profiles`,
82 })}`.message ?? '',
83 'info',
84 )
85 } else {
86 updatedState = {...state, profiles: [...state.profiles, action.profile]}
87 }
88 break
89 case 'RemoveProfile':
90 updatedState = {
91 ...state,
92 profiles: state.profiles.filter(
93 profile => profile.did !== action.profileDid,
94 ),
95 }
96 break
97 case 'AddFeed':
98 if (state.feeds.length >= 3) {
99 Toast.show(msg`You may only add up to 3 feeds`.message ?? '', 'info')
100 } else {
101 updatedState = {...state, feeds: [...state.feeds, action.feed]}
102 }
103 break
104 case 'RemoveFeed':
105 updatedState = {
106 ...state,
107 feeds: state.feeds.filter(f => f.uri !== action.feedUri),
108 }
109 break
110 case 'SetProcessing':
111 updatedState = {...state, processing: action.processing}
112 break
113 }
114
115 return updatedState
116}
117
118export function Provider({
119 starterPack,
120 listItems,
121 targetProfile,
122 children,
123}: {
124 starterPack?: AppBskyGraphDefs.StarterPackView
125 listItems?: AppBskyGraphDefs.ListItemView[]
126 targetProfile: bsky.profile.AnyProfileView
127 children: React.ReactNode
128}) {
129 const createInitialState = (): State => {
130 const targetDid = targetProfile?.did
131
132 if (
133 starterPack &&
134 bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord)
135 ) {
136 return {
137 canNext: true,
138 currentStep: 'Details',
139 name: starterPack.record.name,
140 description: starterPack.record.description,
141 profiles: listItems?.map(i => i.subject) ?? [],
142 feeds: starterPack.feeds ?? [],
143 processing: false,
144 transitionDirection: 'Forward',
145 targetDid,
146 }
147 }
148
149 return {
150 canNext: true,
151 currentStep: 'Details',
152 profiles: [targetProfile],
153 feeds: [],
154 processing: false,
155 transitionDirection: 'Forward',
156 targetDid,
157 }
158 }
159
160 const [state, dispatch] = React.useReducer(reducer, null, createInitialState)
161
162 return (
163 <StateContext.Provider value={[state, dispatch]}>
164 {children}
165 </StateContext.Provider>
166 )
167}
168
169export {
170 type Action as WizardAction,
171 type State as WizardState,
172 type Step as WizardStep,
173}