diff --git a/assets/icons/qrCode_stroke2_corner0_rounded.svg b/assets/icons/qrCode_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..b17db39533
--- /dev/null
+++ b/assets/icons/qrCode_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx
new file mode 100644
index 0000000000..7318267442
--- /dev/null
+++ b/src/components/StarterPack/ProfileStarterPacks.tsx
@@ -0,0 +1,193 @@
+import React from 'react'
+import {
+ findNodeHandle,
+ ListRenderItemInfo,
+ StyleProp,
+ View,
+ ViewStyle,
+} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useQueryClient} from '@tanstack/react-query'
+
+import {cleanError} from '#/lib/strings/errors'
+import {useTheme} from '#/lib/ThemeContext'
+import {logger} from '#/logger'
+import {isNative} from '#/platform/detection'
+import {hydrateFeedGenerator} from '#/state/queries/feed'
+import {usePreferencesQuery} from '#/state/queries/preferences'
+import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens'
+import {usePalette} from 'lib/hooks/usePalette'
+import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
+import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
+import {List, ListRef} from 'view/com/util/List'
+import {LoadMoreRetryBtn} from 'view/com/util/LoadMoreRetryBtn'
+import {Text} from 'view/com/util/text/Text'
+import {StarterPackCard} from '#/components/StarterPack/StarterPackCard'
+
+const LOADING = {_reactKey: '__loading__'}
+const EMPTY = {_reactKey: '__empty__'}
+const ERROR_ITEM = {_reactKey: '__error__'}
+const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
+
+interface SectionRef {
+ scrollToTop: () => void
+}
+
+interface ProfileFeedgensProps {
+ did: string
+ scrollElRef: ListRef
+ headerOffset: number
+ enabled?: boolean
+ style?: StyleProp
+ testID?: string
+ setScrollViewTag: (tag: number | null) => void
+}
+
+export const ProfileStarterPacks = React.forwardRef<
+ SectionRef,
+ ProfileFeedgensProps
+>(function ProfileFeedgensImpl(
+ {did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag},
+ ref,
+) {
+ const pal = usePalette('default')
+ const {_} = useLingui()
+ const theme = useTheme()
+ const [isPTRing, setIsPTRing] = React.useState(false)
+ const opts = React.useMemo(() => ({enabled}), [enabled])
+ const {
+ data,
+ isFetching,
+ isFetched,
+ hasNextPage,
+ fetchNextPage,
+ isError,
+ error,
+ refetch,
+ } = useProfileFeedgensQuery(did, opts)
+ const isEmpty = !isFetching && !data?.pages[0]?.feeds.length
+ const {data: preferences} = usePreferencesQuery()
+
+ const items = React.useMemo(() => {
+ let items: any[] = []
+ if (isError && isEmpty) {
+ items = items.concat([ERROR_ITEM])
+ }
+ if (!isFetched && isFetching) {
+ items = items.concat([LOADING])
+ } else if (isEmpty) {
+ items = items.concat([EMPTY])
+ } else if (data?.pages) {
+ for (const page of data?.pages) {
+ items = items.concat(page.feeds.map(feed => hydrateFeedGenerator(feed)))
+ }
+ }
+ if (isError && !isEmpty) {
+ items = items.concat([LOAD_MORE_ERROR_ITEM])
+ }
+ return items
+ }, [isError, isEmpty, isFetched, isFetching, data])
+
+ const queryClient = useQueryClient()
+
+ const onScrollToTop = React.useCallback(() => {
+ scrollElRef.current?.scrollToOffset({
+ animated: isNative,
+ offset: -headerOffset,
+ })
+ queryClient.invalidateQueries({queryKey: RQKEY(did)})
+ }, [scrollElRef, queryClient, headerOffset, did])
+
+ React.useImperativeHandle(ref, () => ({
+ scrollToTop: onScrollToTop,
+ }))
+
+ const onRefresh = React.useCallback(async () => {
+ setIsPTRing(true)
+ try {
+ await refetch()
+ } catch (err) {
+ logger.error('Failed to refresh starter packs', {message: err})
+ }
+ setIsPTRing(false)
+ }, [refetch, setIsPTRing])
+
+ const onEndReached = React.useCallback(async () => {
+ if (isFetching || !hasNextPage || isError) return
+
+ try {
+ await fetchNextPage()
+ } catch (err) {
+ logger.error('Failed to load more starter packs', {message: err})
+ }
+ }, [isFetching, hasNextPage, isError, fetchNextPage])
+
+ const onPressRetryLoadMore = React.useCallback(() => {
+ fetchNextPage()
+ }, [fetchNextPage])
+
+ const renderItem = React.useCallback(
+ ({item, index}: ListRenderItemInfo) => {
+ if (item === EMPTY) {
+ return (
+
+
+ You have no feeds.
+
+
+ )
+ } else if (item === ERROR_ITEM) {
+ return (
+
+ )
+ } else if (item === LOAD_MORE_ERROR_ITEM) {
+ return (
+
+ )
+ } else if (item === LOADING) {
+ return
+ }
+ if (preferences) {
+ return
+ }
+ return null
+ },
+ [error, refetch, onPressRetryLoadMore, pal, preferences, _],
+ )
+
+ React.useEffect(() => {
+ if (enabled && scrollElRef.current) {
+ const nativeTag = findNodeHandle(scrollElRef.current)
+ setScrollViewTag(nativeTag)
+ }
+ }, [enabled, scrollElRef, setScrollViewTag])
+
+ return (
+
+ item._reactKey || item.uri}
+ renderItem={renderItem}
+ refreshing={isPTRing}
+ onRefresh={onRefresh}
+ headerOffset={headerOffset}
+ contentContainerStyle={isNative && {paddingBottom: headerOffset + 100}}
+ indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
+ removeClippedSubviews={true}
+ // @ts-ignore our .web version only -prf
+ desktopFixedHeight
+ onEndReached={onEndReached}
+ />
+
+ )
+})
diff --git a/src/components/StarterPack/StarterPackCard.tsx b/src/components/StarterPack/StarterPackCard.tsx
new file mode 100644
index 0000000000..52cae2040a
--- /dev/null
+++ b/src/components/StarterPack/StarterPackCard.tsx
@@ -0,0 +1,39 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a, useTheme} from '#/alf'
+import {StarterPackIcon} from '#/components/icons/StarterPackIcon'
+import {Link} from '#/components/Link'
+import {Text} from '#/components/Typography'
+
+export function StarterPackCard({hideTopBorder}: {hideTopBorder?: boolean}) {
+ const t = useTheme()
+
+ return (
+
+
+
+
+
+ Science
+
+ Starter pack by @bossett.social
+
+
+
+ 380 users have joined!
+
+
+
+
+ )
+}
diff --git a/src/components/icons/QrCode.tsx b/src/components/icons/QrCode.tsx
new file mode 100644
index 0000000000..e841071f70
--- /dev/null
+++ b/src/components/icons/QrCode.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const QrCode_Stroke2_Corner0_Rounded = createSinglePathSVG({
+ path: 'M3 5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm6 0H5v4h4V5ZM3 15a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4Zm6 0H5v4h4v-4ZM13 5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2V5Zm6 0h-4v4h4V5ZM14 13a1 1 0 0 1 1 1v1h1a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1Zm3 1a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Zm0 4a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-1v1a1 1 0 1 1-2 0v-2Z',
+})
diff --git a/src/screens/StarterPack/Main/index.tsx b/src/screens/StarterPack/Main/index.tsx
index 1bb4fa11f4..d4bf8b8274 100644
--- a/src/screens/StarterPack/Main/index.tsx
+++ b/src/screens/StarterPack/Main/index.tsx
@@ -8,11 +8,15 @@ import {CommonNavigatorParams} from 'lib/routes/types'
import {isWeb} from 'platform/detection'
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
+import {EventStopper} from 'view/com/util/EventStopper'
import {CenteredView} from 'view/com/util/Views'
import {FeedsList} from '#/screens/StarterPack/Main/FeedsList'
import {ProfilesList} from '#/screens/StarterPack/Main/ProfilesList'
import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
+import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox'
+import {QrCode_Stroke2_Corner0_Rounded as QrCode} from '#/components/icons/QrCode'
+import * as Menu from '#/components/Menu'
/**
* TEMPORARY CONTENT, DO NOT TRANSLATE
@@ -96,7 +100,7 @@ export function StarterPackScreen({}: NativeStackScreenProps<
// const {id} = route.params
return (
-
+
)
@@ -148,16 +152,48 @@ function Header({isOwn}: {isOwn: boolean}) {
creator={undefined}
avatarType="starter-pack">
-
+
+
+
+ {({props}) => {
+ return (
+
+ )
+ }}
+
+
+
+ {}}>
+
+ Share link
+
+
+
+ {}}>
+
+ Create QR code
+
+
+
+
+
+
+
{isOwn && (