diff --git a/src/App.native.tsx b/src/App.native.tsx
index 4500b5d076..865e6dc195 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -23,6 +23,7 @@ import {queryClient} from 'lib/react-query'
import {TestCtrls} from 'view/com/testing/TestCtrls'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
+import {Provider as InvitesStateProvider} from 'state/invites'
SplashScreen.preventAutoHideAsync()
@@ -80,7 +81,9 @@ function App() {
return (
-
+
+
+
)
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 9792274b02..cfc2a0028b 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -18,6 +18,7 @@ import {ThemeProvider} from 'lib/ThemeContext'
import {queryClient} from 'lib/react-query'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
+import {Provider as InvitesStateProvider} from 'state/invites'
const InnerApp = observer(function AppImpl() {
const colorMode = useColorMode()
@@ -70,7 +71,9 @@ function App() {
return (
-
+
+
+
)
diff --git a/src/state/invites.tsx b/src/state/invites.tsx
new file mode 100644
index 0000000000..6a0d1b5900
--- /dev/null
+++ b/src/state/invites.tsx
@@ -0,0 +1,56 @@
+import React from 'react'
+import * as persisted from '#/state/persisted'
+
+type StateContext = persisted.Schema['invites']
+type ApiContext = {
+ setInviteCopied: (code: string) => void
+}
+
+const stateContext = React.createContext(
+ persisted.defaults.invites,
+)
+const apiContext = React.createContext({
+ setInviteCopied(_: string) {},
+})
+
+export function Provider({children}: React.PropsWithChildren<{}>) {
+ const [state, setState] = React.useState(persisted.get('invites'))
+
+ const api = React.useMemo(
+ () => ({
+ setInviteCopied(code: string) {
+ setState(state => {
+ state = {
+ ...state,
+ copiedInvites: state.copiedInvites.includes(code)
+ ? state.copiedInvites
+ : state.copiedInvites.concat([code]),
+ }
+ persisted.write('invites', state)
+ return state
+ })
+ },
+ }),
+ [setState],
+ )
+
+ React.useEffect(() => {
+ return persisted.onUpdate(() => {
+ setState(persisted.get('invites'))
+ })
+ }, [setState])
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useInvitesState() {
+ return React.useContext(stateContext)
+}
+
+export function useInvitesAPI() {
+ return React.useContext(apiContext)
+}
diff --git a/src/state/models/feeds/notifications.ts b/src/state/models/feeds/notifications.ts
index 272d528811..5f34feb662 100644
--- a/src/state/models/feeds/notifications.ts
+++ b/src/state/models/feeds/notifications.ts
@@ -304,7 +304,7 @@ export class NotificationsFeedModel {
}
get unreadCountLabel(): string {
- const count = this.unreadCount + this.rootStore.invitedUsers.numNotifs
+ const count = this.unreadCount
if (count >= MAX_VISIBLE_NOTIFS) {
return `${MAX_VISIBLE_NOTIFS}+`
}
diff --git a/src/state/models/invited-users.ts b/src/state/models/invited-users.ts
deleted file mode 100644
index 9ba65e19ec..0000000000
--- a/src/state/models/invited-users.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import {makeAutoObservable, runInAction} from 'mobx'
-import {ComAtprotoServerDefs, AppBskyActorDefs} from '@atproto/api'
-import {RootStoreModel} from './root-store'
-import {isObj, hasProp, isStrArray} from 'lib/type-guards'
-import {logger} from '#/logger'
-
-export class InvitedUsers {
- copiedInvites: string[] = []
- seenDids: string[] = []
- profiles: AppBskyActorDefs.ProfileViewDetailed[] = []
-
- get numNotifs() {
- return this.profiles.length
- }
-
- constructor(public rootStore: RootStoreModel) {
- makeAutoObservable(
- this,
- {rootStore: false, serialize: false, hydrate: false},
- {autoBind: true},
- )
- }
-
- serialize() {
- return {seenDids: this.seenDids, copiedInvites: this.copiedInvites}
- }
-
- hydrate(v: unknown) {
- if (isObj(v) && hasProp(v, 'seenDids') && isStrArray(v.seenDids)) {
- this.seenDids = v.seenDids
- }
- if (
- isObj(v) &&
- hasProp(v, 'copiedInvites') &&
- isStrArray(v.copiedInvites)
- ) {
- this.copiedInvites = v.copiedInvites
- }
- }
-
- async fetch(invites: ComAtprotoServerDefs.InviteCode[]) {
- // pull the dids of invited users not marked seen
- const dids = []
- for (const invite of invites) {
- for (const use of invite.uses) {
- if (!this.seenDids.includes(use.usedBy)) {
- dids.push(use.usedBy)
- }
- }
- }
-
- // fetch their profiles
- this.profiles = []
- if (dids.length) {
- try {
- const res = await this.rootStore.agent.app.bsky.actor.getProfiles({
- actors: dids,
- })
- runInAction(() => {
- // save the ones following -- these are the ones we want to notify the user about
- this.profiles = res.data.profiles.filter(
- profile => !profile.viewer?.following,
- )
- })
- this.rootStore.me.follows.hydrateMany(this.profiles)
- } catch (e) {
- logger.error('Failed to fetch profiles for invited users', {
- error: e,
- })
- }
- }
- }
-
- isInviteCopied(invite: string) {
- return this.copiedInvites.includes(invite)
- }
-
- setInviteCopied(invite: string) {
- if (!this.isInviteCopied(invite)) {
- this.copiedInvites.push(invite)
- }
- }
-
- markSeen(did: string) {
- this.seenDids.push(did)
- this.profiles = this.profiles.filter(profile => profile.did !== did)
- }
-}
diff --git a/src/state/models/me.ts b/src/state/models/me.ts
index d12cb68c41..d3061f166b 100644
--- a/src/state/models/me.ts
+++ b/src/state/models/me.ts
@@ -193,7 +193,6 @@ export class MeModel {
error: e,
})
}
- await this.rootStore.invitedUsers.fetch(this.invites)
}
}
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index fadd279fc9..d11e9a1480 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -15,7 +15,6 @@ import {ProfilesCache} from './cache/profiles-view'
import {PostsCache} from './cache/posts'
import {LinkMetasCache} from './cache/link-metas'
import {MeModel} from './me'
-import {InvitedUsers} from './invited-users'
import {PreferencesModel} from './ui/preferences'
import {resetToTab} from '../../Navigation'
import {ImageSizesCache} from './cache/image-sizes'
@@ -42,7 +41,6 @@ export class RootStoreModel {
shell = new ShellUiModel(this)
preferences = new PreferencesModel(this)
me = new MeModel(this)
- invitedUsers = new InvitedUsers(this)
handleResolutions = new HandleResolutionsCache()
profiles = new ProfilesCache(this)
posts = new PostsCache(this)
@@ -68,7 +66,6 @@ export class RootStoreModel {
session: this.session.serialize(),
me: this.me.serialize(),
preferences: this.preferences.serialize(),
- invitedUsers: this.invitedUsers.serialize(),
}
}
@@ -89,9 +86,6 @@ export class RootStoreModel {
if (hasProp(v, 'preferences')) {
this.preferences.hydrate(v.preferences)
}
- if (hasProp(v, 'invitedUsers')) {
- this.invitedUsers.hydrate(v.invitedUsers)
- }
}
}
diff --git a/src/state/persisted/legacy.ts b/src/state/persisted/legacy.ts
index 67eef81a0f..3da509304c 100644
--- a/src/state/persisted/legacy.ts
+++ b/src/state/persisted/legacy.ts
@@ -97,11 +97,9 @@ export function transform(legacy: LegacySchema): Schema {
legacy.preferences.requireAltTextEnabled ||
defaults.requireAltTextEnabled,
mutedThreads: legacy.mutedThreads.uris || defaults.mutedThreads,
- invitedUsers: {
- seenDids: legacy.invitedUsers.seenDids || defaults.invitedUsers.seenDids,
+ invites: {
copiedInvites:
- legacy.invitedUsers.copiedInvites ||
- defaults.invitedUsers.copiedInvites,
+ legacy.invitedUsers.copiedInvites || defaults.invites.copiedInvites,
},
onboarding: {
step: legacy.onboarding.step || defaults.onboarding.step,
diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts
index 708930610e..9c52661e44 100644
--- a/src/state/persisted/schema.ts
+++ b/src/state/persisted/schema.ts
@@ -29,8 +29,7 @@ export const schema = z.object({
}),
requireAltTextEnabled: z.boolean(), // should move to server
mutedThreads: z.array(z.string()), // should move to server
- invitedUsers: z.object({
- seenDids: z.array(z.string()),
+ invites: z.object({
copiedInvites: z.array(z.string()),
}),
onboarding: z.object({
@@ -58,8 +57,7 @@ export const defaults: Schema = {
},
requireAltTextEnabled: false,
mutedThreads: [],
- invitedUsers: {
- seenDids: [],
+ invites: {
copiedInvites: [],
},
onboarding: {
diff --git a/src/view/com/modals/InviteCodes.tsx b/src/view/com/modals/InviteCodes.tsx
index 09cfd4de79..a8aa164c38 100644
--- a/src/view/com/modals/InviteCodes.tsx
+++ b/src/view/com/modals/InviteCodes.tsx
@@ -1,6 +1,7 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {observer} from 'mobx-react-lite'
+import {ComAtprotoServerDefs} from '@atproto/api'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
@@ -14,6 +15,10 @@ import {ScrollView} from './util'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {useInvitesState, useInvitesAPI} from '#/state/invites'
+import {UserInfoText} from '../util/UserInfoText'
+import {makeProfileLink} from '#/lib/routes/links'
+import {Link} from '../util/Link'
export const snapPoints = ['70%']
@@ -66,7 +71,7 @@ export function Component({}: {}) {
))}
@@ -87,52 +92,81 @@ export function Component({}: {}) {
const InviteCode = observer(function InviteCodeImpl({
testID,
- code,
+ invite,
used,
}: {
testID: string
- code: string
+ invite: ComAtprotoServerDefs.InviteCode
used?: boolean
}) {
const pal = usePalette('default')
const store = useStores()
const {invitesAvailable} = store.me
+ const invitesState = useInvitesState()
+ const {setInviteCopied} = useInvitesAPI()
const onPress = React.useCallback(() => {
- Clipboard.setString(code)
+ Clipboard.setString(invite.code)
Toast.show('Copied to clipboard')
- store.invitedUsers.setInviteCopied(code)
- }, [store, code])
+ setInviteCopied(invite.code)
+ }, [setInviteCopied, invite])
return (
-
-
- {code}
-
-
- {!used && store.invitedUsers.isInviteCopied(code) && (
- Copied
- )}
- {!used && (
-
- )}
-
+
+
+
+ {invite.code}
+
+
+ {!used && invitesState.copiedInvites.includes(invite.code) && (
+ Copied
+ )}
+ {!used && (
+
+ )}
+
+ {invite.uses.length > 0 ? (
+
+ Used by:
+ {invite.uses.map(use => (
+
+ •
+
+
+ ))}
+
+ ) : null}
+
)
})
@@ -176,9 +210,6 @@ const styles = StyleSheet.create({
inviteCode: {
flexDirection: 'row',
alignItems: 'center',
- borderBottomWidth: 1,
- paddingHorizontal: 20,
- paddingVertical: 14,
},
codeCopied: {
marginRight: 8,
diff --git a/src/view/com/notifications/InvitedUsers.tsx b/src/view/com/notifications/InvitedUsers.tsx
deleted file mode 100644
index aaf358b876..0000000000
--- a/src/view/com/notifications/InvitedUsers.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import React from 'react'
-import {
- FontAwesomeIcon,
- FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {StyleSheet, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
-import {AppBskyActorDefs} from '@atproto/api'
-import {UserAvatar} from '../util/UserAvatar'
-import {Text} from '../util/text/Text'
-import {Link, TextLink} from '../util/Link'
-import {Button} from '../util/forms/Button'
-import {FollowButton} from '../profile/FollowButton'
-import {CenteredView} from '../util/Views.web'
-import {useStores} from 'state/index'
-import {usePalette} from 'lib/hooks/usePalette'
-import {s} from 'lib/styles'
-import {sanitizeDisplayName} from 'lib/strings/display-names'
-import {makeProfileLink} from 'lib/routes/links'
-
-export const InvitedUsers = observer(function InvitedUsersImpl() {
- const store = useStores()
- return (
-
- {store.invitedUsers.profiles.map(profile => (
-
- ))}
-
- )
-})
-
-function InvitedUser({
- profile,
-}: {
- profile: AppBskyActorDefs.ProfileViewDetailed
-}) {
- const pal = usePalette('default')
- const store = useStores()
-
- const onPressDismiss = React.useCallback(() => {
- store.invitedUsers.markSeen(profile.did)
- }, [store, profile])
-
- return (
-
-
-
-
-
-
-
-
-
- {' '}
- joined using your invite code!
-
-
-
-
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- layout: {
- flexDirection: 'row',
- borderTopWidth: 1,
- padding: 10,
- },
- layoutIcon: {
- width: 70,
- alignItems: 'flex-end',
- paddingTop: 2,
- },
- icon: {
- marginRight: 10,
- marginTop: 4,
- },
- desc: {
- paddingVertical: 6,
- },
- btns: {
- flexDirection: 'row',
- gap: 10,
- },
-})
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index cd482bd1c4..b03e733765 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -10,7 +10,6 @@ import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewHeader} from '../com/util/ViewHeader'
import {Feed} from '../com/notifications/Feed'
import {TextLink} from 'view/com/util/Link'
-import {InvitedUsers} from '../com/notifications/InvitedUsers'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {useStores} from 'state/index'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
@@ -145,7 +144,6 @@ export const NotificationsScreen = withAuthRequired(
return (
-