mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Move invite-state to new persistence + context and replace the notifications with just showing uses in the modal (#1840)

authored by

Paul Frazee and committed by
GitHub
e75b2d50 74f8390f

+137 -259
+4 -1
src/App.native.tsx
··· 23 23 import {TestCtrls} from 'view/com/testing/TestCtrls' 24 24 import {Provider as ShellStateProvider} from 'state/shell' 25 25 import {Provider as MutedThreadsProvider} from 'state/muted-threads' 26 + import {Provider as InvitesStateProvider} from 'state/invites' 26 27 27 28 SplashScreen.preventAutoHideAsync() 28 29 ··· 80 81 return ( 81 82 <ShellStateProvider> 82 83 <MutedThreadsProvider> 83 - <InnerApp /> 84 + <InvitesStateProvider> 85 + <InnerApp /> 86 + </InvitesStateProvider> 84 87 </MutedThreadsProvider> 85 88 </ShellStateProvider> 86 89 )
+4 -1
src/App.web.tsx
··· 18 18 import {queryClient} from 'lib/react-query' 19 19 import {Provider as ShellStateProvider} from 'state/shell' 20 20 import {Provider as MutedThreadsProvider} from 'state/muted-threads' 21 + import {Provider as InvitesStateProvider} from 'state/invites' 21 22 22 23 const InnerApp = observer(function AppImpl() { 23 24 const colorMode = useColorMode() ··· 70 71 return ( 71 72 <ShellStateProvider> 72 73 <MutedThreadsProvider> 73 - <InnerApp /> 74 + <InvitesStateProvider> 75 + <InnerApp /> 76 + </InvitesStateProvider> 74 77 </MutedThreadsProvider> 75 78 </ShellStateProvider> 76 79 )
+56
src/state/invites.tsx
··· 1 + import React from 'react' 2 + import * as persisted from '#/state/persisted' 3 + 4 + type StateContext = persisted.Schema['invites'] 5 + type ApiContext = { 6 + setInviteCopied: (code: string) => void 7 + } 8 + 9 + const stateContext = React.createContext<StateContext>( 10 + persisted.defaults.invites, 11 + ) 12 + const apiContext = React.createContext<ApiContext>({ 13 + setInviteCopied(_: string) {}, 14 + }) 15 + 16 + export function Provider({children}: React.PropsWithChildren<{}>) { 17 + const [state, setState] = React.useState(persisted.get('invites')) 18 + 19 + const api = React.useMemo( 20 + () => ({ 21 + setInviteCopied(code: string) { 22 + setState(state => { 23 + state = { 24 + ...state, 25 + copiedInvites: state.copiedInvites.includes(code) 26 + ? state.copiedInvites 27 + : state.copiedInvites.concat([code]), 28 + } 29 + persisted.write('invites', state) 30 + return state 31 + }) 32 + }, 33 + }), 34 + [setState], 35 + ) 36 + 37 + React.useEffect(() => { 38 + return persisted.onUpdate(() => { 39 + setState(persisted.get('invites')) 40 + }) 41 + }, [setState]) 42 + 43 + return ( 44 + <stateContext.Provider value={state}> 45 + <apiContext.Provider value={api}>{children}</apiContext.Provider> 46 + </stateContext.Provider> 47 + ) 48 + } 49 + 50 + export function useInvitesState() { 51 + return React.useContext(stateContext) 52 + } 53 + 54 + export function useInvitesAPI() { 55 + return React.useContext(apiContext) 56 + }
+1 -1
src/state/models/feeds/notifications.ts
··· 304 304 } 305 305 306 306 get unreadCountLabel(): string { 307 - const count = this.unreadCount + this.rootStore.invitedUsers.numNotifs 307 + const count = this.unreadCount 308 308 if (count >= MAX_VISIBLE_NOTIFS) { 309 309 return `${MAX_VISIBLE_NOTIFS}+` 310 310 }
-88
src/state/models/invited-users.ts
··· 1 - import {makeAutoObservable, runInAction} from 'mobx' 2 - import {ComAtprotoServerDefs, AppBskyActorDefs} from '@atproto/api' 3 - import {RootStoreModel} from './root-store' 4 - import {isObj, hasProp, isStrArray} from 'lib/type-guards' 5 - import {logger} from '#/logger' 6 - 7 - export class InvitedUsers { 8 - copiedInvites: string[] = [] 9 - seenDids: string[] = [] 10 - profiles: AppBskyActorDefs.ProfileViewDetailed[] = [] 11 - 12 - get numNotifs() { 13 - return this.profiles.length 14 - } 15 - 16 - constructor(public rootStore: RootStoreModel) { 17 - makeAutoObservable( 18 - this, 19 - {rootStore: false, serialize: false, hydrate: false}, 20 - {autoBind: true}, 21 - ) 22 - } 23 - 24 - serialize() { 25 - return {seenDids: this.seenDids, copiedInvites: this.copiedInvites} 26 - } 27 - 28 - hydrate(v: unknown) { 29 - if (isObj(v) && hasProp(v, 'seenDids') && isStrArray(v.seenDids)) { 30 - this.seenDids = v.seenDids 31 - } 32 - if ( 33 - isObj(v) && 34 - hasProp(v, 'copiedInvites') && 35 - isStrArray(v.copiedInvites) 36 - ) { 37 - this.copiedInvites = v.copiedInvites 38 - } 39 - } 40 - 41 - async fetch(invites: ComAtprotoServerDefs.InviteCode[]) { 42 - // pull the dids of invited users not marked seen 43 - const dids = [] 44 - for (const invite of invites) { 45 - for (const use of invite.uses) { 46 - if (!this.seenDids.includes(use.usedBy)) { 47 - dids.push(use.usedBy) 48 - } 49 - } 50 - } 51 - 52 - // fetch their profiles 53 - this.profiles = [] 54 - if (dids.length) { 55 - try { 56 - const res = await this.rootStore.agent.app.bsky.actor.getProfiles({ 57 - actors: dids, 58 - }) 59 - runInAction(() => { 60 - // save the ones following -- these are the ones we want to notify the user about 61 - this.profiles = res.data.profiles.filter( 62 - profile => !profile.viewer?.following, 63 - ) 64 - }) 65 - this.rootStore.me.follows.hydrateMany(this.profiles) 66 - } catch (e) { 67 - logger.error('Failed to fetch profiles for invited users', { 68 - error: e, 69 - }) 70 - } 71 - } 72 - } 73 - 74 - isInviteCopied(invite: string) { 75 - return this.copiedInvites.includes(invite) 76 - } 77 - 78 - setInviteCopied(invite: string) { 79 - if (!this.isInviteCopied(invite)) { 80 - this.copiedInvites.push(invite) 81 - } 82 - } 83 - 84 - markSeen(did: string) { 85 - this.seenDids.push(did) 86 - this.profiles = this.profiles.filter(profile => profile.did !== did) 87 - } 88 - }
-1
src/state/models/me.ts
··· 193 193 error: e, 194 194 }) 195 195 } 196 - await this.rootStore.invitedUsers.fetch(this.invites) 197 196 } 198 197 } 199 198
-6
src/state/models/root-store.ts
··· 15 15 import {PostsCache} from './cache/posts' 16 16 import {LinkMetasCache} from './cache/link-metas' 17 17 import {MeModel} from './me' 18 - import {InvitedUsers} from './invited-users' 19 18 import {PreferencesModel} from './ui/preferences' 20 19 import {resetToTab} from '../../Navigation' 21 20 import {ImageSizesCache} from './cache/image-sizes' ··· 42 41 shell = new ShellUiModel(this) 43 42 preferences = new PreferencesModel(this) 44 43 me = new MeModel(this) 45 - invitedUsers = new InvitedUsers(this) 46 44 handleResolutions = new HandleResolutionsCache() 47 45 profiles = new ProfilesCache(this) 48 46 posts = new PostsCache(this) ··· 68 66 session: this.session.serialize(), 69 67 me: this.me.serialize(), 70 68 preferences: this.preferences.serialize(), 71 - invitedUsers: this.invitedUsers.serialize(), 72 69 } 73 70 } 74 71 ··· 88 85 } 89 86 if (hasProp(v, 'preferences')) { 90 87 this.preferences.hydrate(v.preferences) 91 - } 92 - if (hasProp(v, 'invitedUsers')) { 93 - this.invitedUsers.hydrate(v.invitedUsers) 94 88 } 95 89 } 96 90 }
+2 -4
src/state/persisted/legacy.ts
··· 97 97 legacy.preferences.requireAltTextEnabled || 98 98 defaults.requireAltTextEnabled, 99 99 mutedThreads: legacy.mutedThreads.uris || defaults.mutedThreads, 100 - invitedUsers: { 101 - seenDids: legacy.invitedUsers.seenDids || defaults.invitedUsers.seenDids, 100 + invites: { 102 101 copiedInvites: 103 - legacy.invitedUsers.copiedInvites || 104 - defaults.invitedUsers.copiedInvites, 102 + legacy.invitedUsers.copiedInvites || defaults.invites.copiedInvites, 105 103 }, 106 104 onboarding: { 107 105 step: legacy.onboarding.step || defaults.onboarding.step,
+2 -4
src/state/persisted/schema.ts
··· 29 29 }), 30 30 requireAltTextEnabled: z.boolean(), // should move to server 31 31 mutedThreads: z.array(z.string()), // should move to server 32 - invitedUsers: z.object({ 33 - seenDids: z.array(z.string()), 32 + invites: z.object({ 34 33 copiedInvites: z.array(z.string()), 35 34 }), 36 35 onboarding: z.object({ ··· 58 57 }, 59 58 requireAltTextEnabled: false, 60 59 mutedThreads: [], 61 - invitedUsers: { 62 - seenDids: [], 60 + invites: { 63 61 copiedInvites: [], 64 62 }, 65 63 onboarding: {
+68 -37
src/view/com/modals/InviteCodes.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 3 import {observer} from 'mobx-react-lite' 4 + import {ComAtprotoServerDefs} from '@atproto/api' 4 5 import { 5 6 FontAwesomeIcon, 6 7 FontAwesomeIconStyle, ··· 14 15 import {usePalette} from 'lib/hooks/usePalette' 15 16 import {isWeb} from 'platform/detection' 16 17 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 18 + import {useInvitesState, useInvitesAPI} from '#/state/invites' 19 + import {UserInfoText} from '../util/UserInfoText' 20 + import {makeProfileLink} from '#/lib/routes/links' 21 + import {Link} from '../util/Link' 17 22 18 23 export const snapPoints = ['70%'] 19 24 ··· 66 71 <InviteCode 67 72 testID={`inviteCode-${i}`} 68 73 key={invite.code} 69 - code={invite.code} 74 + invite={invite} 70 75 used={invite.available - invite.uses.length <= 0 || invite.disabled} 71 76 /> 72 77 ))} ··· 87 92 88 93 const InviteCode = observer(function InviteCodeImpl({ 89 94 testID, 90 - code, 95 + invite, 91 96 used, 92 97 }: { 93 98 testID: string 94 - code: string 99 + invite: ComAtprotoServerDefs.InviteCode 95 100 used?: boolean 96 101 }) { 97 102 const pal = usePalette('default') 98 103 const store = useStores() 99 104 const {invitesAvailable} = store.me 105 + const invitesState = useInvitesState() 106 + const {setInviteCopied} = useInvitesAPI() 100 107 101 108 const onPress = React.useCallback(() => { 102 - Clipboard.setString(code) 109 + Clipboard.setString(invite.code) 103 110 Toast.show('Copied to clipboard') 104 - store.invitedUsers.setInviteCopied(code) 105 - }, [store, code]) 111 + setInviteCopied(invite.code) 112 + }, [setInviteCopied, invite]) 106 113 107 114 return ( 108 - <TouchableOpacity 109 - testID={testID} 110 - style={[styles.inviteCode, pal.border]} 111 - onPress={onPress} 112 - accessibilityRole="button" 113 - accessibilityLabel={ 114 - invitesAvailable === 1 115 - ? 'Invite codes: 1 available' 116 - : `Invite codes: ${invitesAvailable} available` 117 - } 118 - accessibilityHint="Opens list of invite codes"> 119 - <Text 120 - testID={`${testID}-code`} 121 - type={used ? 'md' : 'md-bold'} 122 - style={used ? [pal.textLight, styles.strikeThrough] : pal.text}> 123 - {code} 124 - </Text> 125 - <View style={styles.flex1} /> 126 - {!used && store.invitedUsers.isInviteCopied(code) && ( 127 - <Text style={[pal.textLight, styles.codeCopied]}>Copied</Text> 128 - )} 129 - {!used && ( 130 - <FontAwesomeIcon 131 - icon={['far', 'clone']} 132 - style={pal.text as FontAwesomeIconStyle} 133 - /> 134 - )} 135 - </TouchableOpacity> 115 + <View 116 + style={[ 117 + pal.border, 118 + {borderBottomWidth: 1, paddingHorizontal: 20, paddingVertical: 14}, 119 + ]}> 120 + <TouchableOpacity 121 + testID={testID} 122 + style={[styles.inviteCode]} 123 + onPress={onPress} 124 + accessibilityRole="button" 125 + accessibilityLabel={ 126 + invitesAvailable === 1 127 + ? 'Invite codes: 1 available' 128 + : `Invite codes: ${invitesAvailable} available` 129 + } 130 + accessibilityHint="Opens list of invite codes"> 131 + <Text 132 + testID={`${testID}-code`} 133 + type={used ? 'md' : 'md-bold'} 134 + style={used ? [pal.textLight, styles.strikeThrough] : pal.text}> 135 + {invite.code} 136 + </Text> 137 + <View style={styles.flex1} /> 138 + {!used && invitesState.copiedInvites.includes(invite.code) && ( 139 + <Text style={[pal.textLight, styles.codeCopied]}>Copied</Text> 140 + )} 141 + {!used && ( 142 + <FontAwesomeIcon 143 + icon={['far', 'clone']} 144 + style={pal.text as FontAwesomeIconStyle} 145 + /> 146 + )} 147 + </TouchableOpacity> 148 + {invite.uses.length > 0 ? ( 149 + <View 150 + style={{ 151 + flexDirection: 'column', 152 + gap: 8, 153 + paddingTop: 6, 154 + }}> 155 + <Text style={pal.text}>Used by:</Text> 156 + {invite.uses.map(use => ( 157 + <Link 158 + key={use.usedBy} 159 + href={makeProfileLink({handle: use.usedBy, did: ''})} 160 + style={{ 161 + flexDirection: 'row', 162 + }}> 163 + <Text style={pal.text}>• </Text> 164 + <UserInfoText did={use.usedBy} style={pal.link} /> 165 + </Link> 166 + ))} 167 + </View> 168 + ) : null} 169 + </View> 136 170 ) 137 171 }) 138 172 ··· 176 210 inviteCode: { 177 211 flexDirection: 'row', 178 212 alignItems: 'center', 179 - borderBottomWidth: 1, 180 - paddingHorizontal: 20, 181 - paddingVertical: 14, 182 213 }, 183 214 codeCopied: { 184 215 marginRight: 8,
-114
src/view/com/notifications/InvitedUsers.tsx
··· 1 - import React from 'react' 2 - import { 3 - FontAwesomeIcon, 4 - FontAwesomeIconStyle, 5 - } from '@fortawesome/react-native-fontawesome' 6 - import {StyleSheet, View} from 'react-native' 7 - import {observer} from 'mobx-react-lite' 8 - import {AppBskyActorDefs} from '@atproto/api' 9 - import {UserAvatar} from '../util/UserAvatar' 10 - import {Text} from '../util/text/Text' 11 - import {Link, TextLink} from '../util/Link' 12 - import {Button} from '../util/forms/Button' 13 - import {FollowButton} from '../profile/FollowButton' 14 - import {CenteredView} from '../util/Views.web' 15 - import {useStores} from 'state/index' 16 - import {usePalette} from 'lib/hooks/usePalette' 17 - import {s} from 'lib/styles' 18 - import {sanitizeDisplayName} from 'lib/strings/display-names' 19 - import {makeProfileLink} from 'lib/routes/links' 20 - 21 - export const InvitedUsers = observer(function InvitedUsersImpl() { 22 - const store = useStores() 23 - return ( 24 - <CenteredView> 25 - {store.invitedUsers.profiles.map(profile => ( 26 - <InvitedUser key={profile.did} profile={profile} /> 27 - ))} 28 - </CenteredView> 29 - ) 30 - }) 31 - 32 - function InvitedUser({ 33 - profile, 34 - }: { 35 - profile: AppBskyActorDefs.ProfileViewDetailed 36 - }) { 37 - const pal = usePalette('default') 38 - const store = useStores() 39 - 40 - const onPressDismiss = React.useCallback(() => { 41 - store.invitedUsers.markSeen(profile.did) 42 - }, [store, profile]) 43 - 44 - return ( 45 - <View 46 - testID="invitedUser" 47 - style={[ 48 - styles.layout, 49 - { 50 - backgroundColor: pal.colors.unreadNotifBg, 51 - borderColor: pal.colors.unreadNotifBorder, 52 - }, 53 - ]}> 54 - <View style={styles.layoutIcon}> 55 - <FontAwesomeIcon 56 - icon="user-plus" 57 - size={24} 58 - style={[styles.icon, s.blue3 as FontAwesomeIconStyle]} 59 - /> 60 - </View> 61 - <View style={s.flex1}> 62 - <Link href={makeProfileLink(profile)}> 63 - <UserAvatar avatar={profile.avatar} size={35} /> 64 - </Link> 65 - <Text style={[styles.desc, pal.text]}> 66 - <TextLink 67 - type="md-bold" 68 - style={pal.text} 69 - href={makeProfileLink(profile)} 70 - text={sanitizeDisplayName(profile.displayName || profile.handle)} 71 - />{' '} 72 - joined using your invite code! 73 - </Text> 74 - <View style={styles.btns}> 75 - <FollowButton 76 - unfollowedType="primary" 77 - followedType="primary-light" 78 - profile={profile} 79 - /> 80 - <Button 81 - testID="dismissBtn" 82 - type="primary-light" 83 - label="Dismiss" 84 - onPress={onPressDismiss} 85 - /> 86 - </View> 87 - </View> 88 - </View> 89 - ) 90 - } 91 - 92 - const styles = StyleSheet.create({ 93 - layout: { 94 - flexDirection: 'row', 95 - borderTopWidth: 1, 96 - padding: 10, 97 - }, 98 - layoutIcon: { 99 - width: 70, 100 - alignItems: 'flex-end', 101 - paddingTop: 2, 102 - }, 103 - icon: { 104 - marginRight: 10, 105 - marginTop: 4, 106 - }, 107 - desc: { 108 - paddingVertical: 6, 109 - }, 110 - btns: { 111 - flexDirection: 'row', 112 - gap: 10, 113 - }, 114 - })
-2
src/view/screens/Notifications.tsx
··· 10 10 import {ViewHeader} from '../com/util/ViewHeader' 11 11 import {Feed} from '../com/notifications/Feed' 12 12 import {TextLink} from 'view/com/util/Link' 13 - import {InvitedUsers} from '../com/notifications/InvitedUsers' 14 13 import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' 15 14 import {useStores} from 'state/index' 16 15 import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' ··· 145 144 return ( 146 145 <View testID="notificationsScreen" style={s.hContentRegion}> 147 146 <ViewHeader title="Notifications" canGoBack={false} /> 148 - <InvitedUsers /> 149 147 <Feed 150 148 view={store.me.notifications} 151 149 onPressTryAgain={onPressTryAgain}