forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {Keyboard, View} from 'react-native'
2import {
3 type AppBskyActorDefs,
4 type AppBskyFeedDefs,
5 moderateFeedGenerator,
6 moderateProfile,
7 type ModerationOpts,
8 type ModerationUI,
9} from '@atproto/api'
10import {msg, Trans} from '@lingui/macro'
11import {useLingui} from '@lingui/react'
12
13import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from '#/lib/constants'
14import {sanitizeDisplayName} from '#/lib/strings/display-names'
15import {sanitizeHandle} from '#/lib/strings/handles'
16import {useSession} from '#/state/session'
17import {UserAvatar} from '#/view/com/util/UserAvatar'
18import {
19 type WizardAction,
20 type WizardState,
21} from '#/screens/StarterPack/Wizard/State'
22import {atoms as a, useTheme} from '#/alf'
23import {Button, ButtonText} from '#/components/Button'
24import * as Toggle from '#/components/forms/Toggle'
25import {Checkbox} from '#/components/forms/Toggle'
26import {Text} from '#/components/Typography'
27import {useAnalytics} from '#/analytics'
28import type * as bsky from '#/types/bsky'
29
30function WizardListCard({
31 type,
32 btnType,
33 displayName,
34 subtitle,
35 onPress,
36 avatar,
37 included,
38 disabled,
39 moderationUi,
40}: {
41 type: 'user' | 'algo'
42 btnType: 'checkbox' | 'remove'
43 profile?: AppBskyActorDefs.ProfileViewBasic
44 feed?: AppBskyFeedDefs.GeneratorView
45 displayName: string
46 subtitle: string
47 onPress: () => void
48 avatar?: string
49 included?: boolean
50 disabled?: boolean
51 moderationUi: ModerationUI
52}) {
53 const t = useTheme()
54 const {_} = useLingui()
55
56 return (
57 <Toggle.Item
58 name={type === 'user' ? _(msg`Person toggle`) : _(msg`Feed toggle`)}
59 label={
60 included
61 ? _(msg`Remove ${displayName} from starter pack`)
62 : _(msg`Add ${displayName} to starter pack`)
63 }
64 value={included}
65 disabled={btnType === 'remove' || disabled}
66 onChange={onPress}
67 style={[
68 a.flex_row,
69 a.align_center,
70 a.px_lg,
71 a.py_md,
72 a.gap_md,
73 a.border_b,
74 t.atoms.border_contrast_low,
75 ]}>
76 <UserAvatar
77 size={45}
78 avatar={avatar}
79 moderation={moderationUi}
80 type={type}
81 />
82 <View style={[a.flex_1, a.gap_2xs]}>
83 <Text
84 emoji
85 style={[
86 a.flex_1,
87 a.font_semi_bold,
88 a.text_md,
89 a.leading_tight,
90 a.self_start,
91 ]}
92 numberOfLines={1}>
93 {displayName}
94 </Text>
95 <Text
96 style={[a.flex_1, a.leading_tight, t.atoms.text_contrast_medium]}
97 numberOfLines={1}>
98 {subtitle}
99 </Text>
100 </View>
101 {btnType === 'checkbox' ? (
102 <Checkbox />
103 ) : !disabled ? (
104 <Button
105 label={_(msg`Remove`)}
106 variant="solid"
107 color="secondary"
108 size="small"
109 style={[a.self_center, {marginLeft: 'auto'}]}
110 onPress={onPress}>
111 <ButtonText>
112 <Trans>Remove</Trans>
113 </ButtonText>
114 </Button>
115 ) : null}
116 </Toggle.Item>
117 )
118}
119
120export function WizardProfileCard({
121 btnType,
122 state,
123 dispatch,
124 profile,
125 moderationOpts,
126}: {
127 btnType: 'checkbox' | 'remove'
128 state: WizardState
129 dispatch: (action: WizardAction) => void
130 profile: bsky.profile.AnyProfileView
131 moderationOpts: ModerationOpts
132}) {
133 const ax = useAnalytics()
134 const {currentAccount} = useSession()
135
136 // Determine the "main" profile for this starter pack - either targetDid or current account
137 const targetProfileDid = state.targetDid || currentAccount?.did
138 const isTarget = profile.did === targetProfileDid
139 const included = isTarget || state.profiles.some(p => p.did === profile.did)
140 const disabled =
141 isTarget ||
142 (!included && state.profiles.length >= STARTER_PACK_MAX_SIZE - 1)
143 const moderationUi = moderateProfile(profile, moderationOpts).ui('avatar')
144 const displayName = profile.displayName
145 ? sanitizeDisplayName(profile.displayName)
146 : `@${sanitizeHandle(profile.handle)}`
147
148 const onPress = () => {
149 if (disabled) return
150
151 Keyboard.dismiss()
152 if (profile.did === targetProfileDid) return
153
154 if (!included) {
155 ax.metric('starterPack:addUser', {})
156 dispatch({type: 'AddProfile', profile})
157 } else {
158 ax.metric('starterPack:removeUser', {})
159 dispatch({type: 'RemoveProfile', profileDid: profile.did})
160 }
161 }
162
163 return (
164 <WizardListCard
165 type="user"
166 btnType={btnType}
167 displayName={displayName}
168 subtitle={`@${sanitizeHandle(profile.handle)}`}
169 onPress={onPress}
170 avatar={profile.avatar}
171 included={included}
172 disabled={disabled}
173 moderationUi={moderationUi}
174 />
175 )
176}
177
178export function WizardFeedCard({
179 btnType,
180 generator,
181 state,
182 dispatch,
183 moderationOpts,
184}: {
185 btnType: 'checkbox' | 'remove'
186 generator: AppBskyFeedDefs.GeneratorView
187 state: WizardState
188 dispatch: (action: WizardAction) => void
189 moderationOpts: ModerationOpts
190}) {
191 const isDiscover = generator.uri === DISCOVER_FEED_URI
192 const included = isDiscover || state.feeds.some(f => f.uri === generator.uri)
193 const disabled = isDiscover || (!included && state.feeds.length >= 3)
194 const moderationUi = moderateFeedGenerator(generator, moderationOpts).ui(
195 'avatar',
196 )
197
198 const onPress = () => {
199 if (disabled) return
200
201 Keyboard.dismiss()
202 if (included) {
203 dispatch({type: 'RemoveFeed', feedUri: generator.uri})
204 } else {
205 dispatch({type: 'AddFeed', feed: generator})
206 }
207 }
208
209 return (
210 <WizardListCard
211 type="algo"
212 btnType={btnType}
213 displayName={sanitizeDisplayName(generator.displayName)}
214 subtitle={`Feed by @${sanitizeHandle(generator.creator.handle)}`}
215 onPress={onPress}
216 avatar={generator.avatar}
217 included={included}
218 disabled={disabled}
219 moderationUi={moderationUi}
220 />
221 )
222}