Merge remote-tracking branch 'origin/main' into hailey/embed-pack

This commit is contained in:
Hailey
2024-06-26 17:21:03 -07:00
27 changed files with 6243 additions and 4450 deletions
@@ -0,0 +1,51 @@
import React, {useCallback} from 'react'
import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {isNative} from 'platform/detection'
import {Feed} from 'view/com/posts/Feed'
import {EmptyState} from 'view/com/util/EmptyState'
import {ListRef} from 'view/com/util/List'
import {SectionRef} from '#/screens/Profile/Sections/types'
interface ProfilesListProps {
listUri: string
headerHeight: number
scrollElRef: ListRef
}
export const PostsList = React.forwardRef<SectionRef, ProfilesListProps>(
function PostsListImpl({listUri, headerHeight, scrollElRef}, ref) {
const feed: FeedDescriptor = `list|${listUri}|as_following`
const {_} = useLingui()
const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({
animated: isNative,
offset: -headerHeight,
})
}, [scrollElRef, headerHeight])
React.useImperativeHandle(ref, () => ({
scrollToTop: onScrollToTop,
}))
const renderPostsEmpty = useCallback(() => {
return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
}, [_])
return (
<View>
<Feed
feed={feed}
pollInterval={60e3}
scrollElRef={scrollElRef}
renderEmptyState={renderPostsEmpty}
headerOffset={headerHeight}
/>
</View>
)
},
)
@@ -232,11 +232,13 @@ function Empty() {
t.atoms.text_contrast_medium, t.atoms.text_contrast_medium,
{color: 'white'}, {color: 'white'},
]}> ]}>
You haven't created a starter pack yet! <Trans>You haven't created a starter pack yet!</Trans>
</Text> </Text>
<Text style={[a.text_md, {color: 'white'}]}> <Text style={[a.text_md, {color: 'white'}]}>
Starter packs let you easily share your favorite feeds and people with <Trans>
your friends. Starter packs let you easily share your favorite feeds and people
with your friends.
</Trans>
</Text> </Text>
</View> </View>
<View style={[a.flex_row, a.gap_md, {marginLeft: 'auto'}]}> <View style={[a.flex_row, a.gap_md, {marginLeft: 'auto'}]}>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+25 -1
View File
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {Linking, View} from 'react-native'
import {useSafeAreaFrame} from 'react-native-safe-area-context' import {useSafeAreaFrame} from 'react-native-safe-area-context'
import {ComAtprotoLabelDefs} from '@atproto/api' import {ComAtprotoLabelDefs} from '@atproto/api'
import {LABELS} from '@atproto/api' import {LABELS} from '@atproto/api'
@@ -10,6 +10,7 @@ import {useFocusEffect} from '@react-navigation/native'
import {getLabelingServiceTitle} from '#/lib/moderation' import {getLabelingServiceTitle} from '#/lib/moderation'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isIOS} from '#/platform/detection'
import { import {
useMyLabelersQuery, useMyLabelersQuery,
usePreferencesQuery, usePreferencesQuery,
@@ -202,6 +203,8 @@ export function ModerationScreenInner({
[setAdultContentPref], [setAdultContentPref],
) )
const disabledOnIOS = isIOS && !adultContentEnabled
return ( return (
<ScrollView <ScrollView
contentContainerStyle={[ contentContainerStyle={[
@@ -324,12 +327,14 @@ export function ModerationScreenInner({
a.flex_row, a.flex_row,
a.align_center, a.align_center,
a.justify_between, a.justify_between,
disabledOnIOS && {opacity: 0.5},
]}> ]}>
<Text style={[a.font_semibold, t.atoms.text_contrast_high]}> <Text style={[a.font_semibold, t.atoms.text_contrast_high]}>
<Trans>Enable adult content</Trans> <Trans>Enable adult content</Trans>
</Text> </Text>
<Toggle.Item <Toggle.Item
label={_(msg`Toggle to enable or disable adult content`)} label={_(msg`Toggle to enable or disable adult content`)}
disabled={disabledOnIOS}
name="adultContent" name="adultContent"
value={adultContentEnabled} value={adultContentEnabled}
onChange={onToggleAdultContentEnabled}> onChange={onToggleAdultContentEnabled}>
@@ -345,6 +350,25 @@ export function ModerationScreenInner({
</View> </View>
</Toggle.Item> </Toggle.Item>
</View> </View>
{disabledOnIOS && (
<View style={[a.pb_lg, a.px_lg]}>
<Text>
<Trans>
Adult content can only be enabled via the Web at{' '}
<InlineLinkText
to=""
onPress={evt => {
evt.preventDefault()
Linking.openURL('https://bsky.app/')
return false
}}>
bsky.app
</InlineLinkText>
.
</Trans>
</Text>
</View>
)}
<Divider /> <Divider />
</> </>
)} )}
@@ -1,16 +1,15 @@
import React from 'react' import React from 'react'
import {View, ViewStyle, TextStyle} from 'react-native' import {TextStyle, View, ViewStyle} from 'react-native'
import {useTheme, atoms as a, native} from '#/alf' import {capitalize} from '#/lib/strings/capitalize'
import {useInterestsDisplayNames} from '#/screens/Onboarding/state'
import {atoms as a, native, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {capitalize} from '#/lib/strings/capitalize'
import {Context} from '#/screens/Onboarding/state'
export function InterestButton({interest}: {interest: string}) { export function InterestButton({interest}: {interest: string}) {
const t = useTheme() const t = useTheme()
const {interestsDisplayNames} = React.useContext(Context) const interestsDisplayNames = useInterestsDisplayNames()
const ctx = Toggle.useItemContext() const ctx = Toggle.useItemContext()
const styles = React.useMemo(() => { const styles = React.useMemo(() => {
@@ -15,7 +15,11 @@ import {
OnboardingControls, OnboardingControls,
TitleText, TitleText,
} from '#/screens/Onboarding/Layout' } from '#/screens/Onboarding/Layout'
import {ApiResponseMap, Context} from '#/screens/Onboarding/state' import {
ApiResponseMap,
Context,
useInterestsDisplayNames,
} from '#/screens/Onboarding/state'
import {InterestButton} from '#/screens/Onboarding/StepInterests/InterestButton' import {InterestButton} from '#/screens/Onboarding/StepInterests/InterestButton'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -33,8 +37,9 @@ export function StepInterests() {
const t = useTheme() const t = useTheme()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const {track} = useAnalytics() const {track} = useAnalytics()
const interestsDisplayNames = useInterestsDisplayNames()
const {state, dispatch, interestsDisplayNames} = React.useContext(Context) const {state, dispatch} = React.useContext(Context)
const [saving, setSaving] = React.useState(false) const [saving, setSaving] = React.useState(false)
const [interests, setInterests] = React.useState<string[]>( const [interests, setInterests] = React.useState<string[]>(
state.interestsStepResults.selectedInterests.map(i => i), state.interestsStepResults.selectedInterests.map(i => i),
+32 -27
View File
@@ -1,4 +1,6 @@
import React from 'react' import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger' import {logger} from '#/logger'
import {AvatarColor, Emoji} from '#/screens/Onboarding/StepProfile/types' import {AvatarColor, Emoji} from '#/screens/Onboarding/StepProfile/types'
@@ -68,31 +70,36 @@ export type ApiResponseMap = {
} }
} }
export const INTEREST_TO_DISPLAY_NAME_DEFAULTS: { export function useInterestsDisplayNames() {
[key: string]: string const {_} = useLingui()
} = {
news: 'News', return React.useMemo<Record<string, string>>(() => {
journalism: 'Journalism', return {
nature: 'Nature', // Keep this alphabetized
art: 'Art', animals: _(msg`Animals`),
comics: 'Comics', art: _(msg`Art`),
writers: 'Writers', books: _(msg`Books`),
culture: 'Culture', comedy: _(msg`Comedy`),
sports: 'Sports', comics: _(msg`Comics`),
pets: 'Pets', culture: _(msg`Culture`),
animals: 'Animals', dev: _(msg`Software Dev`),
books: 'Books', education: _(msg`Education`),
education: 'Education', food: _(msg`Food`),
climate: 'Climate', gaming: _(msg`Video Games`),
science: 'Science', journalism: _(msg`Journalism`),
politics: 'Politics', movies: _(msg`Movies`),
fitness: 'Fitness', nature: _(msg`Nature`),
tech: 'Tech', news: _(msg`News`),
dev: 'Software Dev', pets: _(msg`Pets`),
comedy: 'Comedy', photography: _(msg`Photography`),
gaming: 'Video Games', politics: _(msg`Politics`),
food: 'Food', science: _(msg`Science`),
cooking: 'Cooking', sports: _(msg`Sports`),
tech: _(msg`Tech`),
tv: _(msg`TV`),
writers: _(msg`Writers`),
}
}, [_])
} }
export const initialState: OnboardingState = { export const initialState: OnboardingState = {
@@ -120,11 +127,9 @@ export const initialState: OnboardingState = {
export const Context = React.createContext<{ export const Context = React.createContext<{
state: OnboardingState state: OnboardingState
dispatch: React.Dispatch<OnboardingAction> dispatch: React.Dispatch<OnboardingAction>
interestsDisplayNames: {[key: string]: string}
}>({ }>({
state: {...initialState}, state: {...initialState},
dispatch: () => {}, dispatch: () => {},
interestsDisplayNames: INTEREST_TO_DISPLAY_NAME_DEFAULTS,
}) })
export function reducer( export function reducer(
+22 -6
View File
@@ -55,6 +55,7 @@ import * as Prompt from '#/components/Prompt'
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {FeedsList} from '#/components/StarterPack/Main/FeedsList' import {FeedsList} from '#/components/StarterPack/Main/FeedsList'
import {PostsList} from '#/components/StarterPack/Main/PostsList'
import {ProfilesList} from '#/components/StarterPack/Main/ProfilesList' import {ProfilesList} from '#/components/StarterPack/Main/ProfilesList'
import {QrCodeDialog} from '#/components/StarterPack/QrCodeDialog' import {QrCodeDialog} from '#/components/StarterPack/QrCodeDialog'
import {ShareDialog} from '#/components/StarterPack/ShareDialog' import {ShareDialog} from '#/components/StarterPack/ShareDialog'
@@ -132,9 +133,14 @@ function StarterPackScreenInner({
> >
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
}) { }) {
const showPeopleTab = Boolean(starterPack.list)
const showFeedsTab = Boolean(starterPack.feeds?.length)
const showPostsTab = Boolean(starterPack.list)
const tabs = [ const tabs = [
...(starterPack.list ? ['People'] : []), ...(showPeopleTab ? ['People'] : []),
...(starterPack.feeds?.length ? ['Feeds'] : []), ...(showFeedsTab ? ['Feeds'] : []),
...(showPostsTab ? ['Posts'] : []),
] ]
const qrCodeDialogControl = useDialogControl() const qrCodeDialogControl = useDialogControl()
@@ -180,10 +186,9 @@ function StarterPackScreenInner({
onOpenShareDialog={onOpenShareDialog} onOpenShareDialog={onOpenShareDialog}
/> />
)}> )}>
{starterPack.list != null {showPeopleTab
? ({headerHeight, scrollElRef}) => ( ? ({headerHeight, scrollElRef}) => (
<ProfilesList <ProfilesList
key={0}
// Validated above // Validated above
listUri={starterPack!.list!.uri} listUri={starterPack!.list!.uri}
headerHeight={headerHeight} headerHeight={headerHeight}
@@ -194,10 +199,9 @@ function StarterPackScreenInner({
/> />
) )
: null} : null}
{starterPack.feeds != null {showFeedsTab
? ({headerHeight, scrollElRef}) => ( ? ({headerHeight, scrollElRef}) => (
<FeedsList <FeedsList
key={1}
// @ts-expect-error ? // @ts-expect-error ?
feeds={starterPack?.feeds} feeds={starterPack?.feeds}
headerHeight={headerHeight} headerHeight={headerHeight}
@@ -206,6 +210,18 @@ function StarterPackScreenInner({
/> />
) )
: null} : null}
{showPostsTab
? ({headerHeight, scrollElRef}) => (
<PostsList
// Validated above
listUri={starterPack!.list!.uri}
headerHeight={headerHeight}
// @ts-expect-error
scrollElRef={scrollElRef}
moderationOpts={moderationOpts}
/>
)
: null}
</PagerWithHeader> </PagerWithHeader>
</View> </View>
+64 -31
View File
@@ -393,7 +393,6 @@ function Footer({
state.currentStep === 'Profiles' state.currentStep === 'Profiles'
? [profile, ...state.profiles] ? [profile, ...state.profiles]
: state.feeds : state.feeds
const initialNamesIndex = state.currentStep === 'Profiles' ? 1 : 0
const isEditEnabled = const isEditEnabled =
(state.currentStep === 'Profiles' && items.length > 1) || (state.currentStep === 'Profiles' && items.length > 1) ||
@@ -445,65 +444,99 @@ function Footer({
))} ))}
</View> </View>
{items.length === 0 ? ( {
<View style={[a.gap_sm]}> state.currentStep === 'Profiles' ? (
<Text style={[a.font_bold, a.text_center, textStyles]}>
<Trans>Add some feeds to your starter pack!</Trans>
</Text>
<Text style={[a.text_center, textStyles]}> <Text style={[a.text_center, textStyles]}>
<Trans>Search for feeds that you want to suggest to others.</Trans> {
</Text> items.length < 2 ? (
</View>
) : (
<Text style={[a.text_center, textStyles]}>
{state.currentStep === 'Profiles' && items.length === 1 ? (
<Trans> <Trans>
It's just you right now! Add more people to your starter pack by It's just you right now! Add more people to your starter pack
searching above. by searching above.
</Trans>
) : items.length === 1 ? (
<Trans>
<Text style={[a.font_bold, textStyles]}>
{getName(items[initialNamesIndex])}
</Text>{' '}
is included in your starter pack
</Trans> </Trans>
) : items.length === 2 ? ( ) : items.length === 2 ? (
<Trans> <Trans>
<Text style={[a.font_bold, textStyles]}>You</Text> and <Text style={[a.font_bold, textStyles]}>You</Text> and
<Text> </Text> <Text> </Text>
<Text style={[a.font_bold, textStyles]}> <Text style={[a.font_bold, textStyles]}>
{getName(items[initialNamesIndex])}{' '} {getName(items[1] /* [0] is self, skip it */)}{' '}
</Text> </Text>
are included in your starter pack are included in your starter pack
</Trans> </Trans>
) : state.currentStep === 'Profiles' ? ( ) : items.length > 2 ? (
<Trans context="profiles"> <Trans context="profiles">
<Text style={[a.font_bold, textStyles]}> <Text style={[a.font_bold, textStyles]}>
{getName(items[initialNamesIndex])},{' '} {getName(items[1] /* [0] is self, skip it */)},{' '}
</Text> </Text>
<Text style={[a.font_bold, textStyles]}> <Text style={[a.font_bold, textStyles]}>
{getName(items[initialNamesIndex + 1])},{' '} {getName(items[2])},{' '}
</Text> </Text>
and{' '} and{' '}
<Plural value={items.length - 2} one="# other" other="# others" />{' '} <Plural
value={items.length - 2}
one="# other"
other="# others"
/>{' '}
are included in your starter pack are included in your starter pack
</Trans> </Trans>
) : null /* Should not happen. */
}
</Text>
) : state.currentStep === 'Feeds' ? (
items.length === 0 ? (
<View style={[a.gap_sm]}>
<Text style={[a.font_bold, a.text_center, textStyles]}>
<Trans>Add some feeds to your starter pack!</Trans>
</Text>
<Text style={[a.text_center, textStyles]}>
<Trans>
Search for feeds that you want to suggest to others.
</Trans>
</Text>
</View>
) : ( ) : (
<Text style={[a.text_center, textStyles]}>
{
items.length === 1 ? (
<Trans>
<Text style={[a.font_bold, textStyles]}>
{getName(items[0])}
</Text>{' '}
is included in your starter pack
</Trans>
) : items.length === 2 ? (
<Trans>
<Text style={[a.font_bold, textStyles]}>
{getName(items[0])}
</Text>{' '}
and
<Text> </Text>
<Text style={[a.font_bold, textStyles]}>
{getName(items[1])}{' '}
</Text>
are included in your starter pack
</Trans>
) : items.length > 2 ? (
<Trans context="feeds"> <Trans context="feeds">
<Text style={[a.font_bold, textStyles]}> <Text style={[a.font_bold, textStyles]}>
{getName(items[initialNamesIndex])},{' '} {getName(items[0])},{' '}
</Text> </Text>
<Text style={[a.font_bold, textStyles]}> <Text style={[a.font_bold, textStyles]}>
{getName(items[initialNamesIndex + 1])},{' '} {getName(items[1])},{' '}
</Text> </Text>
and{' '} and{' '}
<Plural value={items.length - 2} one="# other" other="# others" />{' '} <Plural
value={items.length - 2}
one="# other"
other="# others"
/>{' '}
are included in your starter pack are included in your starter pack
</Trans> </Trans>
)} ) : null /* Should not happen. */
}
</Text> </Text>
)} )
) : null /* Should not happen. */
}
<View <View
style={[ style={[
+28 -3
View File
@@ -19,7 +19,34 @@ export function useFeedTuners(feedDesc: FeedDescriptor) {
] ]
} }
if (feedDesc.startsWith('list')) { if (feedDesc.startsWith('list')) {
return [FeedTuner.dedupReposts] const feedTuners = []
if (feedDesc.endsWith('|as_following')) {
// Same as Following tuners below, copypaste for now.
if (preferences?.feedViewPrefs.hideReposts) {
feedTuners.push(FeedTuner.removeReposts)
} else {
feedTuners.push(FeedTuner.dedupReposts)
}
if (preferences?.feedViewPrefs.hideReplies) {
feedTuners.push(FeedTuner.removeReplies)
} else {
feedTuners.push(
FeedTuner.thresholdRepliesOnly({
userDid: currentAccount?.did || '',
minLikes: preferences?.feedViewPrefs.hideRepliesByLikeCount || 0,
followedOnly:
!!preferences?.feedViewPrefs.hideRepliesByUnfollowed,
}),
)
}
if (preferences?.feedViewPrefs.hideQuotePosts) {
feedTuners.push(FeedTuner.removeQuotePosts)
}
} else {
feedTuners.push(FeedTuner.dedupReposts)
}
return feedTuners
} }
if (feedDesc === 'following') { if (feedDesc === 'following') {
const feedTuners = [] const feedTuners = []
@@ -29,7 +56,6 @@ export function useFeedTuners(feedDesc: FeedDescriptor) {
} else { } else {
feedTuners.push(FeedTuner.dedupReposts) feedTuners.push(FeedTuner.dedupReposts)
} }
if (preferences?.feedViewPrefs.hideReplies) { if (preferences?.feedViewPrefs.hideReplies) {
feedTuners.push(FeedTuner.removeReplies) feedTuners.push(FeedTuner.removeReplies)
} else { } else {
@@ -41,7 +67,6 @@ export function useFeedTuners(feedDesc: FeedDescriptor) {
}), }),
) )
} }
if (preferences?.feedViewPrefs.hideQuotePosts) { if (preferences?.feedViewPrefs.hideQuotePosts) {
feedTuners.push(FeedTuner.removeQuotePosts) feedTuners.push(FeedTuner.removeQuotePosts)
} }
+2
View File
@@ -49,6 +49,7 @@ type AuthorFilter =
| 'posts_with_media' | 'posts_with_media'
type FeedUri = string type FeedUri = string
type ListUri = string type ListUri = string
type ListFilter = 'as_following' // Applies current Following settings. Currently client-side.
export type FeedDescriptor = export type FeedDescriptor =
| 'following' | 'following'
@@ -56,6 +57,7 @@ export type FeedDescriptor =
| `feedgen|${FeedUri}` | `feedgen|${FeedUri}`
| `likes|${ActorDid}` | `likes|${ActorDid}`
| `list|${ListUri}` | `list|${ListUri}`
| `list|${ListUri}|${ListFilter}`
export interface FeedParams { export interface FeedParams {
disableTuner?: boolean disableTuner?: boolean
mergeFeedEnabled?: boolean mergeFeedEnabled?: boolean