Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Respect labels on feeds and lists (#4818)

* Prep

* Pass in optional moderation to FeedCard

* Compute moderation decision, filter contentList contexts, pass into card

* Let's go a different route

* Filter from within search queries

* Use same search query for starter packs

* Filter lists from profile tabs

* Cleanup

* Filter from profile feeds

* Moderate post embeds

* Memoize

* Use ScreenHider on lists

* Hide both list types

* Fix crash on iOS in screen hider, fix lineheight

* Memoize renderItem

* Reuse objects to prevent re-renders

authored by

Eric Bailey and committed by
GitHub
c3d8beee 293ac6fa

+261 -145
+15 -6
src/components/moderation/ScreenHider.tsx
··· 14 14 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 15 15 import {NavigationProp} from 'lib/routes/types' 16 16 import {CenteredView} from '#/view/com/util/Views' 17 - import {atoms as a, useTheme} from '#/alf' 17 + import {atoms as a, useTheme, web} from '#/alf' 18 18 import {Button, ButtonText} from '#/components/Button' 19 19 import { 20 20 ModerationDetailsDialog, ··· 105 105 a.mb_md, 106 106 a.px_lg, 107 107 a.text_center, 108 + a.leading_snug, 108 109 t.atoms.text_contrast_medium, 109 110 ]}> 110 111 {isNoPwi ? ( ··· 113 114 </Trans> 114 115 ) : ( 115 116 <> 116 - <Trans>This {screenDescription} has been flagged:</Trans> 117 - <Text style={[a.text_lg, a.font_semibold, t.atoms.text, a.ml_xs]}> 117 + <Trans>This {screenDescription} has been flagged:</Trans>{' '} 118 + <Text 119 + style={[ 120 + a.text_lg, 121 + a.font_semibold, 122 + a.leading_snug, 123 + t.atoms.text, 124 + a.ml_xs, 125 + ]}> 118 126 {desc.name}.{' '} 119 127 </Text> 120 128 <TouchableWithoutFeedback ··· 127 135 <Text 128 136 style={[ 129 137 a.text_lg, 138 + a.leading_snug, 130 139 { 131 140 color: t.palette.primary_500, 132 - // @ts-ignore web only -prf 141 + }, 142 + web({ 133 143 cursor: 'pointer', 134 - }, 144 + }), 135 145 ]}> 136 146 <Trans>Learn More</Trans> 137 147 </Text> 138 148 </TouchableWithoutFeedback> 139 - 140 149 <ModerationDetailsDialog control={control} modcause={blur} /> 141 150 </> 142 151 )}{' '}
+2 -2
src/screens/StarterPack/Wizard/StepFeeds.tsx
··· 8 8 import {DISCOVER_FEED_URI} from 'lib/constants' 9 9 import { 10 10 useGetPopularFeedsQuery, 11 + usePopularFeedsSearch, 11 12 useSavedFeeds, 12 - useSearchPopularFeedsQuery, 13 13 } from 'state/queries/feed' 14 14 import {SearchInput} from 'view/com/util/forms/SearchInput' 15 15 import {List} from 'view/com/util/List' ··· 59 59 : undefined 60 60 61 61 const {data: searchedFeeds, isFetching: isFetchingSearchedFeeds} = 62 - useSearchPopularFeedsQuery({q: throttledQuery}) 62 + usePopularFeedsSearch({query: throttledQuery}) 63 63 64 64 const isLoading = 65 65 !isFetchedSavedFeeds || isLoadingPopularFeeds || isFetchingSearchedFeeds
+33 -20
src/state/queries/feed.ts
··· 5 5 AppBskyGraphDefs, 6 6 AppBskyUnspeccedGetPopularFeedGenerators, 7 7 AtUri, 8 + moderateFeedGenerator, 8 9 RichText, 9 10 } from '@atproto/api' 10 11 import { ··· 26 27 import {usePreferencesQuery} from '#/state/queries/preferences' 27 28 import {useAgent, useSession} from '#/state/session' 28 29 import {router} from '#/routes' 30 + import {useModerationOpts} from '../preferences/moderation-opts' 29 31 import {FeedDescriptor} from './post-feed' 30 32 import {precacheResolvedUri} from './resolve-uri' 31 33 ··· 207 209 const limit = options?.limit || 10 208 210 const {data: preferences} = usePreferencesQuery() 209 211 const queryClient = useQueryClient() 212 + const moderationOpts = useModerationOpts() 210 213 211 214 // Make sure this doesn't invalidate unless really needed. 212 215 const selectArgs = useMemo( 213 216 () => ({ 214 217 hasSession, 215 218 savedFeeds: preferences?.savedFeeds || [], 219 + moderationOpts, 216 220 }), 217 - [hasSession, preferences?.savedFeeds], 221 + [hasSession, preferences?.savedFeeds, moderationOpts], 218 222 ) 219 223 const lastPageCountRef = useRef(0) 220 224 ··· 225 229 QueryKey, 226 230 string | undefined 227 231 >({ 232 + enabled: Boolean(moderationOpts), 228 233 queryKey: createGetPopularFeedsQueryKey(options), 229 234 queryFn: async ({pageParam}) => { 230 235 const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({ ··· 246 251 ( 247 252 data: InfiniteData<AppBskyUnspeccedGetPopularFeedGenerators.OutputSchema>, 248 253 ) => { 249 - const {savedFeeds, hasSession: hasSessionInner} = selectArgs 254 + const { 255 + savedFeeds, 256 + hasSession: hasSessionInner, 257 + moderationOpts, 258 + } = selectArgs 250 259 return { 251 260 ...data, 252 261 pages: data.pages.map(page => { ··· 264 273 return f.value === feed.uri 265 274 }), 266 275 ) 267 - return !alreadySaved 276 + const decision = moderateFeedGenerator(feed, moderationOpts!) 277 + return !alreadySaved && !decision.ui('contentList').filter 268 278 }), 269 279 } 270 280 }), ··· 304 314 305 315 export function useSearchPopularFeedsMutation() { 306 316 const agent = useAgent() 317 + const moderationOpts = useModerationOpts() 318 + 307 319 return useMutation({ 308 320 mutationFn: async (query: string) => { 309 321 const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({ ··· 311 323 query: query, 312 324 }) 313 325 314 - return res.data.feeds 315 - }, 316 - }) 317 - } 318 - 319 - export function useSearchPopularFeedsQuery({q}: {q: string}) { 320 - const agent = useAgent() 321 - return useQuery({ 322 - queryKey: ['searchPopularFeeds', q], 323 - queryFn: async () => { 324 - const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({ 325 - limit: 15, 326 - query: q, 327 - }) 326 + if (moderationOpts) { 327 + return res.data.feeds.filter(feed => { 328 + const decision = moderateFeedGenerator(feed, moderationOpts) 329 + return !decision.ui('contentList').filter 330 + }) 331 + } 328 332 329 333 return res.data.feeds 330 334 }, 331 - placeholderData: keepPreviousData, 332 335 }) 333 336 } 334 337 ··· 346 349 enabled?: boolean 347 350 }) { 348 351 const agent = useAgent() 352 + const moderationOpts = useModerationOpts() 353 + const enabledInner = enabled ?? Boolean(moderationOpts) 354 + 349 355 return useQuery({ 350 - enabled, 356 + enabled: enabledInner, 351 357 queryKey: createPopularFeedsSearchQueryKey(query), 352 358 queryFn: async () => { 353 359 const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({ 354 - limit: 10, 360 + limit: 15, 355 361 query: query, 356 362 }) 357 363 358 364 return res.data.feeds 365 + }, 366 + placeholderData: keepPreviousData, 367 + select(data) { 368 + return data.filter(feed => { 369 + const decision = moderateFeedGenerator(feed, moderationOpts!) 370 + return !decision.ui('contentList').filter 371 + }) 359 372 }, 360 373 }) 361 374 }
+20 -2
src/state/queries/profile-feedgens.ts
··· 1 - import {AppBskyFeedGetActorFeeds} from '@atproto/api' 1 + import {AppBskyFeedGetActorFeeds, moderateFeedGenerator} from '@atproto/api' 2 2 import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query' 3 3 4 4 import {useAgent} from '#/state/session' 5 + import {useModerationOpts} from '../preferences/moderation-opts' 5 6 6 7 const PAGE_SIZE = 50 7 8 type RQPageParam = string | undefined ··· 14 15 did: string, 15 16 opts?: {enabled?: boolean}, 16 17 ) { 17 - const enabled = opts?.enabled !== false 18 + const moderationOpts = useModerationOpts() 19 + const enabled = opts?.enabled !== false && Boolean(moderationOpts) 18 20 const agent = useAgent() 19 21 return useInfiniteQuery< 20 22 AppBskyFeedGetActorFeeds.OutputSchema, ··· 38 40 initialPageParam: undefined, 39 41 getNextPageParam: lastPage => lastPage.cursor, 40 42 enabled, 43 + select(data) { 44 + return { 45 + ...data, 46 + pages: data.pages.map(page => { 47 + return { 48 + ...page, 49 + feeds: page.feeds 50 + // filter by labels 51 + .filter(list => { 52 + const decision = moderateFeedGenerator(list, moderationOpts!) 53 + return !decision.ui('contentList').filter 54 + }), 55 + } 56 + }), 57 + } 58 + }, 41 59 }) 42 60 }
+27 -10
src/state/queries/profile-lists.ts
··· 1 - import {AppBskyGraphGetLists} from '@atproto/api' 1 + import {AppBskyGraphGetLists, moderateUserList} from '@atproto/api' 2 2 import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query' 3 3 4 4 import {useAgent} from '#/state/session' 5 + import {useModerationOpts} from '../preferences/moderation-opts' 5 6 6 7 const PAGE_SIZE = 30 7 8 type RQPageParam = string | undefined ··· 10 11 export const RQKEY = (did: string) => [RQKEY_ROOT, did] 11 12 12 13 export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) { 13 - const enabled = opts?.enabled !== false 14 + const moderationOpts = useModerationOpts() 15 + const enabled = opts?.enabled !== false && Boolean(moderationOpts) 14 16 const agent = useAgent() 15 17 return useInfiniteQuery< 16 18 AppBskyGraphGetLists.OutputSchema, ··· 27 29 cursor: pageParam, 28 30 }) 29 31 30 - // Starter packs use a reference list, which we do not want to show on profiles. At some point we could probably 31 - // just filter this out on the backend instead of in the client. 32 - return { 33 - ...res.data, 34 - lists: res.data.lists.filter( 35 - l => l.purpose !== 'app.bsky.graph.defs#referencelist', 36 - ), 37 - } 32 + return res.data 38 33 }, 39 34 initialPageParam: undefined, 40 35 getNextPageParam: lastPage => lastPage.cursor, 41 36 enabled, 37 + select(data) { 38 + return { 39 + ...data, 40 + pages: data.pages.map(page => { 41 + return { 42 + ...page, 43 + lists: page.lists 44 + /* 45 + * Starter packs use a reference list, which we do not want to 46 + * show on profiles. At some point we could probably just filter 47 + * this out on the backend instead of in the client. 48 + */ 49 + .filter(l => l.purpose !== 'app.bsky.graph.defs#referencelist') 50 + // filter by labels 51 + .filter(list => { 52 + const decision = moderateUserList(list, moderationOpts!) 53 + return !decision.ui('contentList').filter 54 + }), 55 + } 56 + }), 57 + } 58 + }, 42 59 }) 43 60 }
+43 -40
src/view/com/feeds/ProfileFeedgens.tsx
··· 129 129 // rendering 130 130 // = 131 131 132 - const renderItem = ({item, index}: ListRenderItemInfo<any>) => { 133 - if (item === EMPTY) { 134 - return ( 135 - <EmptyState 136 - icon="hashtag" 137 - message={_(msg`You have no feeds.`)} 138 - testID="listsEmpty" 139 - /> 140 - ) 141 - } else if (item === ERROR_ITEM) { 142 - return ( 143 - <ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} /> 144 - ) 145 - } else if (item === LOAD_MORE_ERROR_ITEM) { 146 - return ( 147 - <LoadMoreRetryBtn 148 - label={_( 149 - msg`There was an issue fetching your lists. Tap here to try again.`, 150 - )} 151 - onPress={onPressRetryLoadMore} 152 - /> 153 - ) 154 - } else if (item === LOADING) { 155 - return <FeedLoadingPlaceholder /> 156 - } 157 - if (preferences) { 158 - return ( 159 - <View 160 - style={[ 161 - (index !== 0 || isWeb) && a.border_t, 162 - t.atoms.border_contrast_low, 163 - a.px_lg, 164 - a.py_lg, 165 - ]}> 166 - <FeedCard.Default view={item} /> 167 - </View> 168 - ) 169 - } 170 - return null 171 - } 132 + const renderItem = React.useCallback( 133 + ({item, index}: ListRenderItemInfo<any>) => { 134 + if (item === EMPTY) { 135 + return ( 136 + <EmptyState 137 + icon="hashtag" 138 + message={_(msg`You have no feeds.`)} 139 + testID="listsEmpty" 140 + /> 141 + ) 142 + } else if (item === ERROR_ITEM) { 143 + return ( 144 + <ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} /> 145 + ) 146 + } else if (item === LOAD_MORE_ERROR_ITEM) { 147 + return ( 148 + <LoadMoreRetryBtn 149 + label={_( 150 + msg`There was an issue fetching your lists. Tap here to try again.`, 151 + )} 152 + onPress={onPressRetryLoadMore} 153 + /> 154 + ) 155 + } else if (item === LOADING) { 156 + return <FeedLoadingPlaceholder /> 157 + } 158 + if (preferences) { 159 + return ( 160 + <View 161 + style={[ 162 + (index !== 0 || isWeb) && a.border_t, 163 + t.atoms.border_contrast_low, 164 + a.px_lg, 165 + a.py_lg, 166 + ]}> 167 + <FeedCard.Default view={item} /> 168 + </View> 169 + ) 170 + } 171 + return null 172 + }, 173 + [_, t, error, refetch, onPressRetryLoadMore, preferences], 174 + ) 172 175 173 176 React.useEffect(() => { 174 177 if (enabled && scrollElRef.current) {
+2 -7
src/view/com/lists/ProfileLists.tsx
··· 75 75 items = items.concat([EMPTY]) 76 76 } else if (data?.pages) { 77 77 for (const page of data?.pages) { 78 - items = items.concat( 79 - page.lists.map(l => ({ 80 - ...l, 81 - _reactKey: l.uri, 82 - })), 83 - ) 78 + items = items.concat(page.lists) 84 79 } 85 80 } 86 81 if (isError && !isEmpty) { ··· 192 187 testID={testID ? `${testID}-flatlist` : undefined} 193 188 ref={scrollElRef} 194 189 data={items} 195 - keyExtractor={(item: any) => item._reactKey} 190 + keyExtractor={(item: any) => item._reactKey || item.uri} 196 191 renderItem={renderItemInner} 197 192 refreshing={isPTRing} 198 193 onRefresh={onRefresh}
+38 -12
src/view/com/util/post-embeds/index.tsx
··· 15 15 AppBskyEmbedRecordWithMedia, 16 16 AppBskyFeedDefs, 17 17 AppBskyGraphDefs, 18 + moderateFeedGenerator, 19 + moderateUserList, 18 20 ModerationDecision, 19 21 } from '@atproto/api' 20 22 21 23 import {ImagesLightbox, useLightboxControls} from '#/state/lightbox' 22 24 import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' 25 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 23 26 import {usePalette} from 'lib/hooks/usePalette' 24 27 import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' 25 28 import {atoms as a} from '#/alf' ··· 51 54 style?: StyleProp<ViewStyle> 52 55 allowNestedQuotes?: boolean 53 56 }) { 54 - const pal = usePalette('default') 55 57 const {openLightbox} = useLightboxControls() 56 58 const largeAltBadge = useLargeAltBadgeEnabled() 57 59 ··· 72 74 73 75 if (AppBskyEmbedRecord.isView(embed)) { 74 76 // custom feed embed (i.e. generator view) 75 - // = 76 77 if (AppBskyFeedDefs.isGeneratorView(embed.record)) { 77 - // TODO moderation 78 - return ( 79 - <FeedSourceCard 80 - feedUri={embed.record.uri} 81 - style={[pal.view, pal.border, styles.customFeedOuter]} 82 - showLikes 83 - /> 84 - ) 78 + return <MaybeFeedCard view={embed.record} /> 85 79 } 86 80 87 81 // list embed 88 82 if (AppBskyGraphDefs.isListView(embed.record)) { 89 - // TODO moderation 90 - return <ListEmbed item={embed.record} /> 83 + return <MaybeListCard view={embed.record} /> 91 84 } 92 85 93 86 if (AppBskyGraphDefs.isStarterPackViewBasic(embed.record)) { ··· 183 176 } 184 177 185 178 return <View /> 179 + } 180 + 181 + function MaybeFeedCard({view}: {view: AppBskyFeedDefs.GeneratorView}) { 182 + const pal = usePalette('default') 183 + const moderationOpts = useModerationOpts() 184 + const moderation = React.useMemo(() => { 185 + return moderationOpts 186 + ? moderateFeedGenerator(view, moderationOpts) 187 + : undefined 188 + }, [view, moderationOpts]) 189 + 190 + return ( 191 + <ContentHider modui={moderation?.ui('contentList')}> 192 + <FeedSourceCard 193 + feedUri={view.uri} 194 + style={[pal.view, pal.border, styles.customFeedOuter]} 195 + showLikes 196 + /> 197 + </ContentHider> 198 + ) 199 + } 200 + 201 + function MaybeListCard({view}: {view: AppBskyGraphDefs.ListView}) { 202 + const moderationOpts = useModerationOpts() 203 + const moderation = React.useMemo(() => { 204 + return moderationOpts ? moderateUserList(view, moderationOpts) : undefined 205 + }, [view, moderationOpts]) 206 + 207 + return ( 208 + <ContentHider modui={moderation?.ui('contentList')}> 209 + <ListEmbed item={view} /> 210 + </ContentHider> 211 + ) 186 212 } 187 213 188 214 const styles = StyleSheet.create({
+81 -46
src/view/screens/ProfileList.tsx
··· 1 1 import React, {useCallback, useMemo} from 'react' 2 2 import {Pressable, StyleSheet, View} from 'react-native' 3 - import {AppBskyGraphDefs, AtUri, RichText as RichTextAPI} from '@atproto/api' 3 + import { 4 + AppBskyGraphDefs, 5 + AtUri, 6 + moderateUserList, 7 + ModerationOpts, 8 + RichText as RichTextAPI, 9 + } from '@atproto/api' 4 10 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 11 import {msg, Trans} from '@lingui/macro' 6 12 import {useLingui} from '@lingui/react' ··· 14 20 import {isNative, isWeb} from '#/platform/detection' 15 21 import {listenSoftReset} from '#/state/events' 16 22 import {useModalControls} from '#/state/modals' 23 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 17 24 import { 18 25 useListBlockMutation, 19 26 useListDeleteMutation, ··· 62 69 import {CenteredView} from 'view/com/util/Views' 63 70 import {atoms as a, useTheme} from '#/alf' 64 71 import {useDialogControl} from '#/components/Dialog' 72 + import {ScreenHider} from '#/components/moderation/ScreenHider' 65 73 import * as Prompt from '#/components/Prompt' 66 74 import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' 67 75 import {RichText} from '#/components/RichText' ··· 81 89 AtUri.make(handleOrDid, 'app.bsky.graph.list', rkey).toString(), 82 90 ) 83 91 const {data: list, error: listError} = useListQuery(resolvedUri?.uri) 92 + const moderationOpts = useModerationOpts() 84 93 85 94 if (resolveError) { 86 95 return ( ··· 101 110 ) 102 111 } 103 112 104 - return resolvedUri && list ? ( 105 - <ProfileListScreenLoaded {...props} uri={resolvedUri.uri} list={list} /> 113 + return resolvedUri && list && moderationOpts ? ( 114 + <ProfileListScreenLoaded 115 + {...props} 116 + uri={resolvedUri.uri} 117 + list={list} 118 + moderationOpts={moderationOpts} 119 + /> 106 120 ) : ( 107 121 <LoadingScreen /> 108 122 ) ··· 112 126 route, 113 127 uri, 114 128 list, 115 - }: Props & {uri: string; list: AppBskyGraphDefs.ListView}) { 129 + moderationOpts, 130 + }: Props & { 131 + uri: string 132 + list: AppBskyGraphDefs.ListView 133 + moderationOpts: ModerationOpts 134 + }) { 116 135 const {_} = useLingui() 117 136 const queryClient = useQueryClient() 118 137 const {openComposer} = useComposerControls() ··· 123 142 const {openModal} = useModalControls() 124 143 const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist' 125 144 const isScreenFocused = useIsFocused() 145 + 146 + const moderation = React.useMemo(() => { 147 + return moderateUserList(list, moderationOpts) 148 + }, [list, moderationOpts]) 126 149 127 150 useSetTitle(list.name) 128 151 ··· 161 184 162 185 if (isCurateList) { 163 186 return ( 187 + <ScreenHider 188 + screenDescription={'list'} 189 + modui={moderation.ui('contentView')}> 190 + <View style={s.hContentRegion}> 191 + <PagerWithHeader 192 + items={SECTION_TITLES_CURATE} 193 + isHeaderReady={true} 194 + renderHeader={renderHeader} 195 + onCurrentPageSelected={onCurrentPageSelected}> 196 + {({headerHeight, scrollElRef, isFocused}) => ( 197 + <FeedSection 198 + ref={feedSectionRef} 199 + feed={`list|${uri}`} 200 + scrollElRef={scrollElRef as ListRef} 201 + headerHeight={headerHeight} 202 + isFocused={isScreenFocused && isFocused} 203 + /> 204 + )} 205 + {({headerHeight, scrollElRef}) => ( 206 + <AboutSection 207 + ref={aboutSectionRef} 208 + scrollElRef={scrollElRef as ListRef} 209 + list={list} 210 + onPressAddUser={onPressAddUser} 211 + headerHeight={headerHeight} 212 + /> 213 + )} 214 + </PagerWithHeader> 215 + <FAB 216 + testID="composeFAB" 217 + onPress={() => openComposer({})} 218 + icon={ 219 + <ComposeIcon2 220 + strokeWidth={1.5} 221 + size={29} 222 + style={{color: 'white'}} 223 + /> 224 + } 225 + accessibilityRole="button" 226 + accessibilityLabel={_(msg`New post`)} 227 + accessibilityHint="" 228 + /> 229 + </View> 230 + </ScreenHider> 231 + ) 232 + } 233 + return ( 234 + <ScreenHider 235 + screenDescription={_(msg`list`)} 236 + modui={moderation.ui('contentView')}> 164 237 <View style={s.hContentRegion}> 165 238 <PagerWithHeader 166 - items={SECTION_TITLES_CURATE} 239 + items={SECTION_TITLES_MOD} 167 240 isHeaderReady={true} 168 - renderHeader={renderHeader} 169 - onCurrentPageSelected={onCurrentPageSelected}> 170 - {({headerHeight, scrollElRef, isFocused}) => ( 171 - <FeedSection 172 - ref={feedSectionRef} 173 - feed={`list|${uri}`} 174 - scrollElRef={scrollElRef as ListRef} 175 - headerHeight={headerHeight} 176 - isFocused={isScreenFocused && isFocused} 177 - /> 178 - )} 241 + renderHeader={renderHeader}> 179 242 {({headerHeight, scrollElRef}) => ( 180 243 <AboutSection 181 - ref={aboutSectionRef} 244 + list={list} 182 245 scrollElRef={scrollElRef as ListRef} 183 - list={list} 184 246 onPressAddUser={onPressAddUser} 185 247 headerHeight={headerHeight} 186 248 /> ··· 201 263 accessibilityHint="" 202 264 /> 203 265 </View> 204 - ) 205 - } 206 - return ( 207 - <View style={s.hContentRegion}> 208 - <PagerWithHeader 209 - items={SECTION_TITLES_MOD} 210 - isHeaderReady={true} 211 - renderHeader={renderHeader}> 212 - {({headerHeight, scrollElRef}) => ( 213 - <AboutSection 214 - list={list} 215 - scrollElRef={scrollElRef as ListRef} 216 - onPressAddUser={onPressAddUser} 217 - headerHeight={headerHeight} 218 - /> 219 - )} 220 - </PagerWithHeader> 221 - <FAB 222 - testID="composeFAB" 223 - onPress={() => openComposer({})} 224 - icon={ 225 - <ComposeIcon2 strokeWidth={1.5} size={29} style={{color: 'white'}} /> 226 - } 227 - accessibilityRole="button" 228 - accessibilityLabel={_(msg`New post`)} 229 - accessibilityHint="" 230 - /> 231 - </View> 266 + </ScreenHider> 232 267 ) 233 268 } 234 269