Merge branch 'ansh/app-812-add-custom-feed-discovery-to-onboarding' into main
commit
f9cab178b9
|
@ -260,6 +260,7 @@ function TabsNavigator() {
|
|||
|
||||
function HomeTabNavigator() {
|
||||
const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark)
|
||||
|
||||
return (
|
||||
<HomeTab.Navigator
|
||||
screenOptions={{
|
||||
|
|
|
@ -122,6 +122,8 @@ interface TrackPropertiesMap {
|
|||
// ONBOARDING events
|
||||
'Onboarding:Begin': {}
|
||||
'Onboarding:Complete': {}
|
||||
'Onboarding:Skipped': {}
|
||||
'Onboarding:Reset': {}
|
||||
}
|
||||
|
||||
interface ScreenPropertiesMap {
|
||||
|
|
|
@ -148,3 +148,110 @@ export const HITSLOP_10 = createHitslop(10)
|
|||
export const HITSLOP_20 = createHitslop(20)
|
||||
export const HITSLOP_30 = createHitslop(30)
|
||||
export const BACK_HITSLOP = HITSLOP_30
|
||||
|
||||
export const RECOMMENDED_FEEDS = [
|
||||
{
|
||||
did: 'did:plc:hsqwcidfez66lwm3gxhfv5in',
|
||||
rkey: 'aaaf2pqeodmpy',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:gekdk2nd47gkk3utfz2xf7cn',
|
||||
rkey: 'aaap4tbjcfe5y',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:5rw2on4i56btlcajojaxwcat',
|
||||
rkey: 'aaao6g552b33o',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:jfhpnnst6flqway4eaeqzj2a',
|
||||
rkey: 'for-science',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:7q4nnnxawajbfaq7to5dpbsy',
|
||||
rkey: 'bsky-news',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:jcoy7v3a2t4rcfdh6i4kza25',
|
||||
rkey: 'astro',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:tenurhgjptubkk5zf5qhi3og',
|
||||
rkey: 'h-nba',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:vpkhqolt662uhesyj6nxm7ys',
|
||||
rkey: 'devfeed',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:cndfx4udwgvpjaakvxvh7wm5',
|
||||
rkey: 'flipboard-tech',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:w4xbfzo7kqfes5zb7r6qv3rw',
|
||||
rkey: 'blacksky',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:lptjvw6ut224kwrj7ub3sqbe',
|
||||
rkey: 'aaaotfjzjplna',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:gkvpokm7ec5j5yxls6xk4e3z',
|
||||
rkey: 'formula-one',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:q6gjnaw2blty4crticxkmujt',
|
||||
rkey: 'positivifeed',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:l72uci4styb4jucsgcrrj5ap',
|
||||
rkey: 'aaao5dzfm36u4',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:k3jkadxv5kkjgs6boyon7m6n',
|
||||
rkey: 'aaaavlyvqzst2',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:nkahctfdi6bxk72umytfwghw',
|
||||
rkey: 'aaado2uvfsc6w',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:epihigio3d7un7u3gpqiy5gv',
|
||||
rkey: 'aaaekwsc7zsvs',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:qiknc4t5rq7yngvz7g4aezq7',
|
||||
rkey: 'aaaejxlobe474',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:mlq4aycufcuolr7ax6sezpc4',
|
||||
rkey: 'aaaoudweck6uy',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:rcez5hcvq3vzlu5x7xrjyccg',
|
||||
rkey: 'aaadzjxbcddzi',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:lnxbuzaenlwjrncx6sc4cfdr',
|
||||
rkey: 'aaab2vesjtszc',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:x3cya3wkt4n6u4ihmvpsc5if',
|
||||
rkey: 'aaacynbxwimok',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:abv47bjgzjgoh3yrygwoi36x',
|
||||
rkey: 'aaagt6amuur5e',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:ffkgesg3jsv2j7aagkzrtcvt',
|
||||
rkey: 'aaacjerk7gwek',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:geoqe3qls5mwezckxxsewys2',
|
||||
rkey: 'aaai43yetqshu',
|
||||
},
|
||||
{
|
||||
did: 'did:plc:2wqomm3tjqbgktbrfwgvrw34',
|
||||
rkey: 'authors',
|
||||
},
|
||||
]
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import {useMediaQuery} from 'react-responsive'
|
||||
import {isNative} from 'platform/detection'
|
||||
|
||||
export function useWebMediaQueries() {
|
||||
const isDesktop = useMediaQuery({
|
||||
query: '(min-width: 1230px)',
|
||||
query: '(min-width: 1224px)',
|
||||
})
|
||||
return {isDesktop}
|
||||
const isTabletOrMobile = useMediaQuery({query: '(max-width: 1224px)'})
|
||||
const isMobile = useMediaQuery({query: '(max-width: 800px)'})
|
||||
if (isNative) {
|
||||
return {isMobile: true, isTabletOrMobile: true, isDesktop: false}
|
||||
}
|
||||
return {isMobile, isTabletOrMobile, isDesktop}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import {makeAutoObservable} from 'mobx'
|
||||
import {RootStoreModel} from '../root-store'
|
||||
import {hasProp} from 'lib/type-guards'
|
||||
import {track} from 'lib/analytics/analytics'
|
||||
|
||||
export const OnboardingScreenSteps = {
|
||||
Welcome: 'Welcome',
|
||||
RecommendedFeeds: 'RecommendedFeeds',
|
||||
Home: 'Home',
|
||||
} as const
|
||||
|
||||
type OnboardingStep =
|
||||
(typeof OnboardingScreenSteps)[keyof typeof OnboardingScreenSteps]
|
||||
const OnboardingStepsArray = Object.values(OnboardingScreenSteps)
|
||||
export class OnboardingModel {
|
||||
// state
|
||||
step: OnboardingStep = 'Home' // default state to skip onboarding, only enabled for new users by calling start()
|
||||
|
||||
constructor(public rootStore: RootStoreModel) {
|
||||
makeAutoObservable(this, {
|
||||
rootStore: false,
|
||||
hydrate: false,
|
||||
serialize: false,
|
||||
})
|
||||
}
|
||||
|
||||
serialize(): unknown {
|
||||
return {
|
||||
step: this.step,
|
||||
}
|
||||
}
|
||||
|
||||
hydrate(v: unknown) {
|
||||
if (typeof v === 'object' && v !== null) {
|
||||
if (
|
||||
hasProp(v, 'step') &&
|
||||
typeof v.step === 'string' &&
|
||||
OnboardingStepsArray.includes(v.step as OnboardingStep)
|
||||
) {
|
||||
this.step = v.step as OnboardingStep
|
||||
}
|
||||
} else {
|
||||
// if there is no valid state, we'll just reset
|
||||
this.reset()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the next screen in the onboarding process based on the current step or screen name provided.
|
||||
* @param {OnboardingStep} [currentScreenName]
|
||||
* @returns name of next screen in the onboarding process
|
||||
*/
|
||||
next(currentScreenName?: OnboardingStep) {
|
||||
currentScreenName = currentScreenName || this.step
|
||||
if (currentScreenName === 'Welcome') {
|
||||
this.step = 'RecommendedFeeds'
|
||||
return this.step
|
||||
} else if (this.step === 'RecommendedFeeds') {
|
||||
this.finish()
|
||||
return this.step
|
||||
} else {
|
||||
// if we get here, we're in an invalid state, let's just go Home
|
||||
return 'Home'
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
this.step = 'Welcome'
|
||||
track('Onboarding:Begin')
|
||||
}
|
||||
|
||||
finish() {
|
||||
this.step = 'Home'
|
||||
track('Onboarding:Complete')
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.step = 'Welcome'
|
||||
track('Onboarding:Reset')
|
||||
}
|
||||
|
||||
skip() {
|
||||
this.step = 'Home'
|
||||
track('Onboarding:Skipped')
|
||||
}
|
||||
|
||||
get isComplete() {
|
||||
return this.step === 'Home'
|
||||
}
|
||||
|
||||
get isActive() {
|
||||
return !this.isComplete
|
||||
}
|
||||
}
|
|
@ -67,6 +67,19 @@ export class CustomFeedModel {
|
|||
}
|
||||
}
|
||||
|
||||
async pin() {
|
||||
try {
|
||||
await this.rootStore.preferences.addPinnedFeed(this.uri)
|
||||
} catch (error) {
|
||||
this.rootStore.log.error('Failed to pin feed', error)
|
||||
} finally {
|
||||
track('CustomFeed:Pin', {
|
||||
name: this.data.displayName,
|
||||
uri: this.uri,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async unsave() {
|
||||
try {
|
||||
await this.rootStore.preferences.removeSavedFeed(this.uri)
|
||||
|
|
|
@ -27,6 +27,7 @@ import {reset as resetNavigation} from '../../Navigation'
|
|||
// remove after backend testing finishes
|
||||
// -prf
|
||||
import {applyDebugHeader} from 'lib/api/debug-appview-proxy-header'
|
||||
import {OnboardingModel} from './discovery/onboarding'
|
||||
|
||||
export const appInfo = z.object({
|
||||
build: z.string(),
|
||||
|
@ -44,6 +45,7 @@ export class RootStoreModel {
|
|||
shell = new ShellUiModel(this)
|
||||
preferences = new PreferencesModel(this)
|
||||
me = new MeModel(this)
|
||||
onboarding = new OnboardingModel(this)
|
||||
invitedUsers = new InvitedUsers(this)
|
||||
handleResolutions = new HandleResolutionsCache()
|
||||
profiles = new ProfilesCache(this)
|
||||
|
@ -70,6 +72,7 @@ export class RootStoreModel {
|
|||
appInfo: this.appInfo,
|
||||
session: this.session.serialize(),
|
||||
me: this.me.serialize(),
|
||||
onboarding: this.onboarding.serialize(),
|
||||
shell: this.shell.serialize(),
|
||||
preferences: this.preferences.serialize(),
|
||||
invitedUsers: this.invitedUsers.serialize(),
|
||||
|
@ -88,6 +91,9 @@ export class RootStoreModel {
|
|||
if (hasProp(v, 'me')) {
|
||||
this.me.hydrate(v.me)
|
||||
}
|
||||
if (hasProp(v, 'onboarding')) {
|
||||
this.onboarding.hydrate(v.onboarding)
|
||||
}
|
||||
if (hasProp(v, 'session')) {
|
||||
this.session.hydrate(v.session)
|
||||
}
|
||||
|
|
|
@ -109,10 +109,10 @@ export class CreateAccountModel {
|
|||
this.setError('')
|
||||
this.setIsProcessing(true)
|
||||
|
||||
// open the onboarding modal after the session is created
|
||||
// open the onboarding screens after the session is created
|
||||
const sessionReadySub = this.rootStore.onSessionReady(() => {
|
||||
sessionReadySub.remove()
|
||||
this.rootStore.shell.openModal({name: 'onboarding'})
|
||||
this.rootStore.onboarding.start()
|
||||
})
|
||||
|
||||
try {
|
||||
|
|
|
@ -136,10 +136,6 @@ export interface PostLanguagesSettingsModal {
|
|||
name: 'post-languages-settings'
|
||||
}
|
||||
|
||||
export interface OnboardingModal {
|
||||
name: 'onboarding'
|
||||
}
|
||||
|
||||
export type Modal =
|
||||
// Account
|
||||
| AddAppPasswordModal
|
||||
|
@ -171,9 +167,6 @@ export type Modal =
|
|||
| WaitlistModal
|
||||
| InviteCodesModal
|
||||
|
||||
// Onboarding
|
||||
| OnboardingModal
|
||||
|
||||
// Generic
|
||||
| ConfirmModal
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react'
|
||||
import {SafeAreaView} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
||||
import {s} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useStores} from 'state/index'
|
||||
import {Welcome} from './onboarding/Welcome'
|
||||
import {RecommendedFeeds} from './onboarding/RecommendedFeeds'
|
||||
|
||||
export const Onboarding = observer(() => {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
|
||||
React.useEffect(() => {
|
||||
store.shell.setMinimalShellMode(true)
|
||||
}, [store])
|
||||
|
||||
const next = () => store.onboarding.next()
|
||||
const skip = () => store.onboarding.skip()
|
||||
|
||||
return (
|
||||
<SafeAreaView testID="onboardingView" style={[s.hContentRegion, pal.view]}>
|
||||
<ErrorBoundary>
|
||||
{store.onboarding.step === 'Welcome' && (
|
||||
<Welcome skip={skip} next={next} />
|
||||
)}
|
||||
{store.onboarding.step === 'RecommendedFeeds' && (
|
||||
<RecommendedFeeds next={next} />
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
</SafeAreaView>
|
||||
)
|
||||
})
|
|
@ -1,66 +0,0 @@
|
|||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {Welcome} from './Welcome'
|
||||
import {useStores} from 'state/index'
|
||||
import {track} from 'lib/analytics/analytics'
|
||||
|
||||
enum OnboardingStep {
|
||||
WELCOME = 'WELCOME',
|
||||
// SELECT_INTERESTS = 'SELECT_INTERESTS',
|
||||
COMPLETE = 'COMPLETE',
|
||||
}
|
||||
type OnboardingState = {
|
||||
currentStep: OnboardingStep
|
||||
}
|
||||
type Action = {type: 'NEXT_STEP'}
|
||||
const initialState: OnboardingState = {
|
||||
currentStep: OnboardingStep.WELCOME,
|
||||
}
|
||||
const reducer = (state: OnboardingState, action: Action): OnboardingState => {
|
||||
switch (action.type) {
|
||||
case 'NEXT_STEP':
|
||||
switch (state.currentStep) {
|
||||
case OnboardingStep.WELCOME:
|
||||
track('Onboarding:Begin')
|
||||
return {...state, currentStep: OnboardingStep.COMPLETE}
|
||||
case OnboardingStep.COMPLETE:
|
||||
track('Onboarding:Complete')
|
||||
return state
|
||||
default:
|
||||
return state
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export const Onboarding = () => {
|
||||
const pal = usePalette('default')
|
||||
const rootStore = useStores()
|
||||
const [state, dispatch] = React.useReducer(reducer, initialState)
|
||||
const next = React.useCallback(
|
||||
() => dispatch({type: 'NEXT_STEP'}),
|
||||
[dispatch],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.currentStep === OnboardingStep.COMPLETE) {
|
||||
// navigate to home
|
||||
rootStore.shell.closeModal()
|
||||
}
|
||||
}, [state.currentStep, rootStore.shell])
|
||||
|
||||
return (
|
||||
<View style={[pal.view, styles.container]}>
|
||||
{state.currentStep === OnboardingStep.WELCOME && <Welcome next={next} />}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
})
|
|
@ -0,0 +1,176 @@
|
|||
import React from 'react'
|
||||
import {FlatList, StyleSheet, View} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {TabletOrDesktop, Mobile} from 'view/com/util/layouts/Breakpoints'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||
import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
import {RecommendedFeedsItem} from './RecommendedFeedsItem'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {RECOMMENDED_FEEDS} from 'lib/constants'
|
||||
|
||||
type Props = {
|
||||
next: () => void
|
||||
}
|
||||
export const RecommendedFeeds = observer(({next}: Props) => {
|
||||
const pal = usePalette('default')
|
||||
const {isTabletOrMobile} = useWebMediaQueries()
|
||||
|
||||
const title = (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
pal.textLight,
|
||||
tdStyles.title1,
|
||||
isTabletOrMobile && tdStyles.title1Small,
|
||||
]}>
|
||||
Choose your
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pal.link,
|
||||
tdStyles.title2,
|
||||
isTabletOrMobile && tdStyles.title2Small,
|
||||
]}>
|
||||
Recomended
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pal.link,
|
||||
tdStyles.title2,
|
||||
isTabletOrMobile && tdStyles.title2Small,
|
||||
]}>
|
||||
Feeds
|
||||
</Text>
|
||||
<Text type="2xl-medium" style={[pal.textLight, tdStyles.description]}>
|
||||
Feeds are created by users to curate content. Choose some feeds that you
|
||||
find interesting.
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: 20,
|
||||
}}>
|
||||
<Button onPress={next} testID="continueBtn">
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 2,
|
||||
gap: 6,
|
||||
}}>
|
||||
<Text
|
||||
type="2xl-medium"
|
||||
style={{color: '#fff', position: 'relative', top: -1}}>
|
||||
Done
|
||||
</Text>
|
||||
<FontAwesomeIcon icon="angle-right" color="#fff" size={14} />
|
||||
</View>
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabletOrDesktop>
|
||||
<TitleColumnLayout
|
||||
testID="recommendedFeedsScreen"
|
||||
title={title}
|
||||
horizontal
|
||||
titleStyle={isTabletOrMobile ? undefined : {minWidth: 470}}
|
||||
contentStyle={{paddingHorizontal: 0}}>
|
||||
<FlatList
|
||||
data={RECOMMENDED_FEEDS}
|
||||
renderItem={({item}) => <RecommendedFeedsItem {...item} />}
|
||||
keyExtractor={item => item.did + item.rkey}
|
||||
style={{flex: 1}}
|
||||
/>
|
||||
</TitleColumnLayout>
|
||||
</TabletOrDesktop>
|
||||
<Mobile>
|
||||
<View style={[mStyles.container]} testID="recommendedFeedsScreen">
|
||||
<ViewHeader
|
||||
title="Recommended Feeds"
|
||||
showBackButton={false}
|
||||
showOnDesktop
|
||||
/>
|
||||
<Text type="lg-medium" style={[pal.text, mStyles.header]}>
|
||||
Check out some recommended feeds. Tap + to add them to your list of
|
||||
pinned feeds.
|
||||
</Text>
|
||||
|
||||
<FlatList
|
||||
data={RECOMMENDED_FEEDS}
|
||||
renderItem={({item}) => <RecommendedFeedsItem {...item} />}
|
||||
keyExtractor={item => item.did + item.rkey}
|
||||
style={{flex: 1}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onPress={next}
|
||||
label="Continue"
|
||||
testID="continueBtn"
|
||||
style={mStyles.button}
|
||||
labelStyle={mStyles.buttonText}
|
||||
/>
|
||||
</View>
|
||||
</Mobile>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
const tdStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
marginHorizontal: 16,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
title1: {
|
||||
fontSize: 36,
|
||||
fontWeight: '800',
|
||||
textAlign: 'right',
|
||||
},
|
||||
title1Small: {
|
||||
fontSize: 24,
|
||||
},
|
||||
title2: {
|
||||
fontSize: 58,
|
||||
fontWeight: '800',
|
||||
textAlign: 'right',
|
||||
},
|
||||
title2Small: {
|
||||
fontSize: 36,
|
||||
},
|
||||
description: {
|
||||
maxWidth: 400,
|
||||
marginTop: 10,
|
||||
marginLeft: 'auto',
|
||||
textAlign: 'right',
|
||||
},
|
||||
})
|
||||
|
||||
const mStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
header: {
|
||||
marginBottom: 16,
|
||||
marginHorizontal: 16,
|
||||
},
|
||||
button: {
|
||||
marginBottom: 16,
|
||||
marginHorizontal: 16,
|
||||
marginTop: 16,
|
||||
},
|
||||
buttonText: {
|
||||
textAlign: 'center',
|
||||
fontSize: 18,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
})
|
|
@ -0,0 +1,142 @@
|
|||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
import {HeartIcon} from 'lib/icons'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useCustomFeed} from 'lib/hooks/useCustomFeed'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {makeRecordUri} from 'lib/strings/url-helpers'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
|
||||
export const RecommendedFeedsItem = observer(
|
||||
({did, rkey}: {did: string; rkey: string}) => {
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const pal = usePalette('default')
|
||||
const uri = makeRecordUri(did, 'app.bsky.feed.generator', rkey)
|
||||
const item = useCustomFeed(uri)
|
||||
if (!item) return null
|
||||
const onToggle = async () => {
|
||||
if (item.isSaved) {
|
||||
try {
|
||||
await item.unsave()
|
||||
} catch (e) {
|
||||
Toast.show('There was an issue contacting your server')
|
||||
console.error('Failed to unsave feed', {e})
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await item.save()
|
||||
await item.pin()
|
||||
} catch (e) {
|
||||
Toast.show('There was an issue contacting your server')
|
||||
console.error('Failed to pin feed', {e})
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<View testID={`feed-${item.displayName}`}>
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
{
|
||||
flex: isMobile ? 1 : undefined,
|
||||
flexDirection: 'row',
|
||||
gap: 18,
|
||||
maxWidth: isMobile ? undefined : 670,
|
||||
borderRightWidth: isMobile ? undefined : 1,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: isMobile ? 12 : 24,
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
]}>
|
||||
<View style={{marginTop: 2}}>
|
||||
<UserAvatar type="algo" size={42} avatar={item.data.avatar} />
|
||||
</View>
|
||||
<View style={{flex: isMobile ? 1 : undefined}}>
|
||||
<Text
|
||||
type="2xl-bold"
|
||||
numberOfLines={1}
|
||||
style={[pal.text, {fontSize: 19}]}>
|
||||
{item.displayName}
|
||||
</Text>
|
||||
|
||||
<Text style={[pal.textLight, {marginBottom: 8}]} numberOfLines={1}>
|
||||
by {sanitizeHandle(item.data.creator.handle, '@')}
|
||||
</Text>
|
||||
|
||||
{item.data.description ? (
|
||||
<Text
|
||||
type="xl"
|
||||
style={[
|
||||
pal.text,
|
||||
{
|
||||
flex: isMobile ? 1 : undefined,
|
||||
maxWidth: 550,
|
||||
marginBottom: 18,
|
||||
},
|
||||
]}
|
||||
numberOfLines={6}>
|
||||
{item.data.description}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<View style={{flexDirection: 'row', alignItems: 'center', gap: 12}}>
|
||||
<Button
|
||||
type="inverted"
|
||||
style={{paddingVertical: 6}}
|
||||
onPress={onToggle}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingRight: 2,
|
||||
gap: 6,
|
||||
}}>
|
||||
{item.isSaved ? (
|
||||
<>
|
||||
<FontAwesomeIcon
|
||||
icon="check"
|
||||
size={16}
|
||||
color={pal.colors.textInverted}
|
||||
/>
|
||||
<Text type="lg-medium" style={pal.textInverted}>
|
||||
Added
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FontAwesomeIcon
|
||||
icon="plus"
|
||||
size={16}
|
||||
color={pal.colors.textInverted}
|
||||
/>
|
||||
<Text type="lg-medium" style={pal.textInverted}>
|
||||
Add
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</Button>
|
||||
|
||||
<View style={{flexDirection: 'row', gap: 4}}>
|
||||
<HeartIcon
|
||||
size={16}
|
||||
strokeWidth={2.5}
|
||||
style={[pal.textLight, {position: 'relative', top: 2}]}
|
||||
/>
|
||||
<Text type="lg-medium" style={[pal.text, pal.textLight]}>
|
||||
{item.data.likeCount || 0}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
|
@ -1,92 +1,10 @@
|
|||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {s} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
import 'react'
|
||||
import {withBreakpoints} from 'view/com/util/layouts/withBreakpoints'
|
||||
import {WelcomeDesktop} from './WelcomeDesktop'
|
||||
import {WelcomeMobile} from './WelcomeMobile'
|
||||
|
||||
export const Welcome = ({next}: {next: () => void}) => {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<View style={[styles.container]}>
|
||||
<View testID="welcomeScreen">
|
||||
<Text style={[pal.text, styles.title]}>Welcome to </Text>
|
||||
<Text style={[pal.text, pal.link, styles.title]}>Bluesky</Text>
|
||||
|
||||
<View style={styles.spacer} />
|
||||
|
||||
<View style={[styles.row]}>
|
||||
<FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} />
|
||||
<View style={[styles.rowText]}>
|
||||
<Text type="lg-bold" style={[pal.text]}>
|
||||
Bluesky is public.
|
||||
</Text>
|
||||
<Text type="lg-thin" style={[pal.text, s.pt2]}>
|
||||
Your posts, likes, and blocks are public. Mutes are private.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.row]}>
|
||||
<FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} />
|
||||
<View style={[styles.rowText]}>
|
||||
<Text type="lg-bold" style={[pal.text]}>
|
||||
Bluesky is open.
|
||||
</Text>
|
||||
<Text type="lg-thin" style={[pal.text, s.pt2]}>
|
||||
Never lose access to your followers and data.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.row]}>
|
||||
<FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} />
|
||||
<View style={[styles.rowText]}>
|
||||
<Text type="lg-bold" style={[pal.text]}>
|
||||
Bluesky is flexible.
|
||||
</Text>
|
||||
<Text type="lg-thin" style={[pal.text, s.pt2]}>
|
||||
Choose the algorithms that power your experience with custom
|
||||
feeds.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
onPress={next}
|
||||
label="Continue"
|
||||
testID="continueBtn"
|
||||
labelStyle={styles.buttonText}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
marginVertical: 60,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
title: {
|
||||
fontSize: 48,
|
||||
fontWeight: '800',
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
columnGap: 20,
|
||||
alignItems: 'center',
|
||||
marginVertical: 20,
|
||||
},
|
||||
rowText: {
|
||||
flex: 1,
|
||||
},
|
||||
spacer: {
|
||||
height: 20,
|
||||
},
|
||||
buttonText: {
|
||||
textAlign: 'center',
|
||||
fontSize: 18,
|
||||
marginVertical: 4,
|
||||
},
|
||||
})
|
||||
export const Welcome = withBreakpoints(
|
||||
WelcomeMobile,
|
||||
WelcomeDesktop,
|
||||
WelcomeDesktop,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {useMediaQuery} from 'react-responsive'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {s} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
|
||||
type Props = {
|
||||
next: () => void
|
||||
skip: () => void
|
||||
}
|
||||
|
||||
export const WelcomeDesktop = observer(({next}: Props) => {
|
||||
const pal = usePalette('default')
|
||||
const horizontal = useMediaQuery({
|
||||
query: '(min-width: 1230px)',
|
||||
})
|
||||
const title = (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
pal.textLight,
|
||||
{
|
||||
fontSize: 36,
|
||||
fontWeight: '800',
|
||||
textAlign: horizontal ? 'right' : 'left',
|
||||
},
|
||||
]}>
|
||||
Welcome to
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pal.link,
|
||||
{
|
||||
fontSize: 72,
|
||||
fontWeight: '800',
|
||||
textAlign: horizontal ? 'right' : 'left',
|
||||
},
|
||||
]}>
|
||||
Bluesky
|
||||
</Text>
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<TitleColumnLayout
|
||||
testID="welcomeOnboarding"
|
||||
title={title}
|
||||
horizontal={horizontal}
|
||||
titleStyle={horizontal ? {paddingBottom: 160} : undefined}>
|
||||
<View style={[styles.row]}>
|
||||
<FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} />
|
||||
<View style={[styles.rowText]}>
|
||||
<Text type="xl-bold" style={[pal.text]}>
|
||||
Bluesky is public.
|
||||
</Text>
|
||||
<Text type="xl" style={[pal.text, s.pt2]}>
|
||||
Your posts, likes, and blocks are public. Mutes are private.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.row]}>
|
||||
<FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} />
|
||||
<View style={[styles.rowText]}>
|
||||
<Text type="xl-bold" style={[pal.text]}>
|
||||
Bluesky is open.
|
||||
</Text>
|
||||
<Text type="xl" style={[pal.text, s.pt2]}>
|
||||
Never lose access to your followers and data.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.row]}>
|
||||
<FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} />
|
||||
<View style={[styles.rowText]}>
|
||||
<Text type="xl-bold" style={[pal.text]}>
|
||||
Bluesky is flexible.
|
||||
</Text>
|
||||
<Text type="xl" style={[pal.text, s.pt2]}>
|
||||
Choose the algorithms that power your experience with custom feeds.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.spacer} />
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<Button onPress={next} testID="continueBtn">
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 2,
|
||||
gap: 6,
|
||||
}}>
|
||||
<Text
|
||||
type="2xl-medium"
|
||||
style={{color: '#fff', position: 'relative', top: -1}}>
|
||||
Next
|
||||
</Text>
|
||||
<FontAwesomeIcon icon="angle-right" color="#fff" size={14} />
|
||||
</View>
|
||||
</Button>
|
||||
</View>
|
||||
</TitleColumnLayout>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
columnGap: 20,
|
||||
alignItems: 'center',
|
||||
marginVertical: 20,
|
||||
},
|
||||
rowText: {
|
||||
flex: 1,
|
||||
},
|
||||
spacer: {
|
||||
height: 20,
|
||||
},
|
||||
})
|
|
@ -0,0 +1,123 @@
|
|||
import React from 'react'
|
||||
import {Pressable, StyleSheet, View} from 'react-native'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {s} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||
import {isDesktopWeb} from 'platform/detection'
|
||||
|
||||
type Props = {
|
||||
next: () => void
|
||||
skip: () => void
|
||||
}
|
||||
|
||||
export const WelcomeMobile = observer(({next, skip}: Props) => {
|
||||
const pal = usePalette('default')
|
||||
|
||||
return (
|
||||
<View style={[styles.container]} testID="welcomeOnboarding">
|
||||
<ViewHeader
|
||||
showOnDesktop
|
||||
showBorder={false}
|
||||
showBackButton={false}
|
||||
title=""
|
||||
renderButton={() => {
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
style={[s.flexRow, s.alignCenter]}
|
||||
onPress={skip}>
|
||||
<Text style={[pal.link]}>Skip</Text>
|
||||
<FontAwesomeIcon
|
||||
icon={'chevron-right'}
|
||||
size={14}
|
||||
color={pal.colors.link}
|
||||
/>
|
||||
</Pressable>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<View>
|
||||
<Text style={[pal.text, styles.title]}>
|
||||
Welcome to{' '}
|
||||
<Text style={[pal.text, pal.link, styles.title]}>Bluesky</Text>
|
||||
</Text>
|
||||
<View style={styles.spacer} />
|
||||
<View style={[styles.row]}>
|
||||
<FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} />
|
||||
<View style={[styles.rowText]}>
|
||||
<Text type="lg-bold" style={[pal.text]}>
|
||||
Bluesky is public.
|
||||
</Text>
|
||||
<Text type="lg-thin" style={[pal.text, s.pt2]}>
|
||||
Your posts, likes, and blocks are public. Mutes are private.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.row]}>
|
||||
<FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} />
|
||||
<View style={[styles.rowText]}>
|
||||
<Text type="lg-bold" style={[pal.text]}>
|
||||
Bluesky is open.
|
||||
</Text>
|
||||
<Text type="lg-thin" style={[pal.text, s.pt2]}>
|
||||
Never lose access to your followers and data.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.row]}>
|
||||
<FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} />
|
||||
<View style={[styles.rowText]}>
|
||||
<Text type="lg-bold" style={[pal.text]}>
|
||||
Bluesky is flexible.
|
||||
</Text>
|
||||
<Text type="lg-thin" style={[pal.text, s.pt2]}>
|
||||
Choose the algorithms that power your experience with custom
|
||||
feeds.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
onPress={next}
|
||||
label="Continue"
|
||||
testID="continueBtn"
|
||||
labelStyle={styles.buttonText}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
marginBottom: isDesktopWeb ? 30 : 60,
|
||||
marginHorizontal: 16,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
title: {
|
||||
fontSize: 42,
|
||||
fontWeight: '800',
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
columnGap: 20,
|
||||
alignItems: 'center',
|
||||
marginVertical: 20,
|
||||
},
|
||||
rowText: {
|
||||
flex: 1,
|
||||
},
|
||||
spacer: {
|
||||
height: 20,
|
||||
},
|
||||
buttonText: {
|
||||
textAlign: 'center',
|
||||
fontSize: 18,
|
||||
marginVertical: 4,
|
||||
},
|
||||
})
|
|
@ -9,6 +9,7 @@ import {observer} from 'mobx-react-lite'
|
|||
import {useStores} from 'state/index'
|
||||
import {CenteredView} from '../util/Views'
|
||||
import {LoggedOut} from './LoggedOut'
|
||||
import {Onboarding} from './Onboarding'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {STATUS_PAGE_URL} from 'lib/constants'
|
||||
|
@ -24,6 +25,9 @@ export const withAuthRequired = <P extends object>(
|
|||
if (!store.session.hasSession) {
|
||||
return <LoggedOut />
|
||||
}
|
||||
if (store.onboarding.isActive) {
|
||||
return <Onboarding />
|
||||
}
|
||||
return <Component {...props} />
|
||||
})
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@ import * as AddAppPassword from './AddAppPasswords'
|
|||
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||
import * as OnboardingModal from './OnboardingModal'
|
||||
import * as ModerationDetailsModal from './ModerationDetails'
|
||||
|
||||
const DEFAULT_SNAPPOINTS = ['90%']
|
||||
|
@ -130,9 +129,6 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
|||
} else if (activeModal?.name === 'post-languages-settings') {
|
||||
snapPoints = PostLanguagesSettingsModal.snapPoints
|
||||
element = <PostLanguagesSettingsModal.Component />
|
||||
} else if (activeModal?.name === 'onboarding') {
|
||||
snapPoints = OnboardingModal.snapPoints
|
||||
element = <OnboardingModal.Component />
|
||||
} else if (activeModal?.name === 'moderation-details') {
|
||||
snapPoints = ModerationDetailsModal.snapPoints
|
||||
element = <ModerationDetailsModal.Component {...activeModal} />
|
||||
|
|
|
@ -26,7 +26,6 @@ import * as AddAppPassword from './AddAppPasswords'
|
|||
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||
import * as OnboardingModal from './OnboardingModal'
|
||||
import * as ModerationDetailsModal from './ModerationDetails'
|
||||
|
||||
export const ModalsContainer = observer(function ModalsContainer() {
|
||||
|
@ -105,8 +104,6 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||
element = <AltTextImageModal.Component {...modal} />
|
||||
} else if (modal.name === 'edit-image') {
|
||||
element = <EditImageModal.Component {...modal} />
|
||||
} else if (modal.name === 'onboarding') {
|
||||
element = <OnboardingModal.Component />
|
||||
} else if (modal.name === 'moderation-details') {
|
||||
element = <ModerationDetailsModal.Component {...modal} />
|
||||
} else {
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import React from 'react'
|
||||
import {Onboarding} from '../auth/onboarding/Onboarding'
|
||||
|
||||
export const snapPoints = ['90%']
|
||||
|
||||
export function Component() {
|
||||
return <Onboarding />
|
||||
}
|
|
@ -17,6 +17,7 @@ const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
|
|||
export const ViewHeader = observer(function ({
|
||||
title,
|
||||
canGoBack,
|
||||
showBackButton = true,
|
||||
hideOnScroll,
|
||||
showOnDesktop,
|
||||
showBorder,
|
||||
|
@ -24,6 +25,7 @@ export const ViewHeader = observer(function ({
|
|||
}: {
|
||||
title: string
|
||||
canGoBack?: boolean
|
||||
showBackButton?: boolean
|
||||
hideOnScroll?: boolean
|
||||
showOnDesktop?: boolean
|
||||
showBorder?: boolean
|
||||
|
@ -49,7 +51,13 @@ export const ViewHeader = observer(function ({
|
|||
|
||||
if (isDesktopWeb) {
|
||||
if (showOnDesktop) {
|
||||
return <DesktopWebHeader title={title} renderButton={renderButton} />
|
||||
return (
|
||||
<DesktopWebHeader
|
||||
title={title}
|
||||
renderButton={renderButton}
|
||||
showBorder={showBorder}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null
|
||||
} else {
|
||||
|
@ -59,30 +67,32 @@ export const ViewHeader = observer(function ({
|
|||
|
||||
return (
|
||||
<Container hideOnScroll={hideOnScroll || false} showBorder={showBorder}>
|
||||
<TouchableOpacity
|
||||
testID="viewHeaderDrawerBtn"
|
||||
onPress={canGoBack ? onPressBack : onPressMenu}
|
||||
hitSlop={BACK_HITSLOP}
|
||||
style={canGoBack ? styles.backBtn : styles.backBtnWide}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={canGoBack ? 'Back' : 'Menu'}
|
||||
accessibilityHint={
|
||||
canGoBack ? '' : 'Access navigation links and settings'
|
||||
}>
|
||||
{canGoBack ? (
|
||||
<FontAwesomeIcon
|
||||
size={18}
|
||||
icon="angle-left"
|
||||
style={[styles.backIcon, pal.text]}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
size={18}
|
||||
icon="bars"
|
||||
style={[styles.backIcon, pal.textLight]}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
{showBackButton ? (
|
||||
<TouchableOpacity
|
||||
testID="viewHeaderDrawerBtn"
|
||||
onPress={canGoBack ? onPressBack : onPressMenu}
|
||||
hitSlop={BACK_HITSLOP}
|
||||
style={canGoBack ? styles.backBtn : styles.backBtnWide}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={canGoBack ? 'Back' : 'Menu'}
|
||||
accessibilityHint={
|
||||
canGoBack ? '' : 'Access navigation links and settings'
|
||||
}>
|
||||
{canGoBack ? (
|
||||
<FontAwesomeIcon
|
||||
size={18}
|
||||
icon="angle-left"
|
||||
style={[styles.backIcon, pal.text]}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
size={18}
|
||||
icon="bars"
|
||||
style={[styles.backIcon, pal.textLight]}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
<View style={styles.titleContainer} pointerEvents="none">
|
||||
<Text type="title" style={[pal.text, styles.title]}>
|
||||
{title}
|
||||
|
@ -90,9 +100,9 @@ export const ViewHeader = observer(function ({
|
|||
</View>
|
||||
{renderButton ? (
|
||||
renderButton()
|
||||
) : (
|
||||
) : showBackButton ? (
|
||||
<View style={canGoBack ? styles.backBtn : styles.backBtnWide} />
|
||||
)}
|
||||
) : null}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
@ -101,13 +111,23 @@ export const ViewHeader = observer(function ({
|
|||
function DesktopWebHeader({
|
||||
title,
|
||||
renderButton,
|
||||
showBorder = true,
|
||||
}: {
|
||||
title: string
|
||||
renderButton?: () => JSX.Element
|
||||
showBorder?: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<CenteredView style={[styles.header, styles.desktopHeader, pal.border]}>
|
||||
<CenteredView
|
||||
style={[
|
||||
styles.header,
|
||||
styles.desktopHeader,
|
||||
pal.border,
|
||||
{
|
||||
borderBottomWidth: showBorder ? 1 : 0,
|
||||
},
|
||||
]}>
|
||||
<View style={styles.titleContainer} pointerEvents="none">
|
||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||
{title}
|
||||
|
@ -195,13 +215,11 @@ const styles = StyleSheet.create({
|
|||
width: '100%',
|
||||
},
|
||||
desktopHeader: {
|
||||
borderBottomWidth: 1,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
border: {
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
|
||||
titleContainer: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react'
|
||||
|
||||
export const Desktop = ({}: React.PropsWithChildren<{}>) => null
|
||||
export const TabletOrDesktop = ({}: React.PropsWithChildren<{}>) => null
|
||||
export const Tablet = ({}: React.PropsWithChildren<{}>) => null
|
||||
export const TabletOrMobile = ({children}: React.PropsWithChildren<{}>) =>
|
||||
children
|
||||
export const Mobile = ({children}: React.PropsWithChildren<{}>) => children
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react'
|
||||
import MediaQuery from 'react-responsive'
|
||||
|
||||
export const Desktop = ({children}: React.PropsWithChildren<{}>) => (
|
||||
<MediaQuery minWidth={1224}>{children}</MediaQuery>
|
||||
)
|
||||
export const TabletOrDesktop = ({children}: React.PropsWithChildren<{}>) => (
|
||||
<MediaQuery minWidth={800}>{children}</MediaQuery>
|
||||
)
|
||||
export const Tablet = ({children}: React.PropsWithChildren<{}>) => (
|
||||
<MediaQuery minWidth={800} maxWidth={1224}>
|
||||
{children}
|
||||
</MediaQuery>
|
||||
)
|
||||
export const TabletOrMobile = ({children}: React.PropsWithChildren<{}>) => (
|
||||
<MediaQuery maxWidth={1224}>{children}</MediaQuery>
|
||||
)
|
||||
export const Mobile = ({children}: React.PropsWithChildren<{}>) => (
|
||||
<MediaQuery maxWidth={800}>{children}</MediaQuery>
|
||||
)
|
|
@ -0,0 +1,69 @@
|
|||
import React from 'react'
|
||||
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
|
||||
|
||||
interface Props {
|
||||
testID?: string
|
||||
title: JSX.Element
|
||||
horizontal: boolean
|
||||
titleStyle?: StyleProp<ViewStyle>
|
||||
contentStyle?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
export function TitleColumnLayout({
|
||||
testID,
|
||||
title,
|
||||
horizontal,
|
||||
children,
|
||||
titleStyle,
|
||||
contentStyle,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const pal = usePalette('default')
|
||||
const titleBg = useColorSchemeStyle(pal.viewLight, pal.view)
|
||||
const contentBg = useColorSchemeStyle(pal.view, {
|
||||
backgroundColor: pal.colors.background,
|
||||
borderColor: pal.colors.border,
|
||||
borderLeftWidth: 1,
|
||||
})
|
||||
|
||||
const layoutStyles = horizontal ? styles2Column : styles1Column
|
||||
return (
|
||||
<View testID={testID} style={layoutStyles.container}>
|
||||
<View style={[layoutStyles.title, titleBg, titleStyle]}>{title}</View>
|
||||
<View style={[layoutStyles.content, contentBg, contentStyle]}>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles2Column = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
height: '100%',
|
||||
},
|
||||
title: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 40,
|
||||
paddingBottom: 80,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
content: {
|
||||
flex: 2,
|
||||
paddingHorizontal: 40,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
})
|
||||
|
||||
const styles1Column = StyleSheet.create({
|
||||
container: {},
|
||||
title: {
|
||||
paddingHorizontal: 40,
|
||||
paddingVertical: 40,
|
||||
},
|
||||
content: {
|
||||
paddingHorizontal: 40,
|
||||
paddingVertical: 40,
|
||||
},
|
||||
})
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react'
|
||||
import {isNative} from 'platform/detection'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
|
||||
export const withBreakpoints =
|
||||
<P extends object>(
|
||||
Mobile: React.ComponentType<P>,
|
||||
Tablet: React.ComponentType<P>,
|
||||
Desktop: React.ComponentType<P>,
|
||||
): React.FC<P> =>
|
||||
(props: P) => {
|
||||
const {isMobile, isTabletOrMobile} = useWebMediaQueries()
|
||||
|
||||
if (isMobile || isNative) {
|
||||
return <Mobile {...props} />
|
||||
}
|
||||
if (isTabletOrMobile) {
|
||||
return <Tablet {...props} />
|
||||
}
|
||||
return <Desktop {...props} />
|
||||
}
|
|
@ -92,6 +92,7 @@ import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay'
|
|||
import {faPause} from '@fortawesome/free-solid-svg-icons/faPause'
|
||||
import {faThumbtack} from '@fortawesome/free-solid-svg-icons/faThumbtack'
|
||||
import {faList} from '@fortawesome/free-solid-svg-icons/faList'
|
||||
import {faChevronRight} from '@fortawesome/free-solid-svg-icons/faChevronRight'
|
||||
|
||||
export function setup() {
|
||||
library.add(
|
||||
|
@ -187,5 +188,6 @@ export function setup() {
|
|||
faPlay,
|
||||
faPause,
|
||||
faList,
|
||||
faChevronRight,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ const POLL_FREQ = 30e3 // 30sec
|
|||
|
||||
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
|
||||
export const HomeScreen = withAuthRequired(
|
||||
observer((_opts: Props) => {
|
||||
observer(({}: Props) => {
|
||||
const store = useStores()
|
||||
const pagerRef = React.useRef<PagerRef>(null)
|
||||
const [selectedPage, setSelectedPage] = React.useState(0)
|
||||
|
|
|
@ -162,6 +162,11 @@ export const SettingsScreen = withAuthRequired(
|
|||
Toast.show('Preferences reset')
|
||||
}, [store])
|
||||
|
||||
const onPressResetOnboarding = React.useCallback(async () => {
|
||||
store.onboarding.reset()
|
||||
Toast.show('Onboarding reset')
|
||||
}, [store])
|
||||
|
||||
const onPressBuildInfo = React.useCallback(() => {
|
||||
Clipboard.setString(
|
||||
`Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`,
|
||||
|
@ -533,6 +538,16 @@ export const SettingsScreen = withAuthRequired(
|
|||
Reset preferences state
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[pal.view, styles.linkCardNoIcon]}
|
||||
onPress={onPressResetOnboarding}
|
||||
accessibilityRole="button"
|
||||
accessibilityHint="Reset onboarding"
|
||||
accessibilityLabel="Resets the onboarding state">
|
||||
<Text type="lg" style={pal.text}>
|
||||
Reset onboarding state
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
) : null}
|
||||
<View style={[styles.footer]}>
|
||||
|
|
|
@ -20,7 +20,6 @@ import {NavigationProp} from 'lib/routes/types'
|
|||
const ShellInner = observer(() => {
|
||||
const store = useStores()
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
|
||||
const navigator = useNavigation<NavigationProp>()
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -29,6 +28,9 @@ const ShellInner = observer(() => {
|
|||
})
|
||||
}, [navigator, store.shell])
|
||||
|
||||
const showBottomBar = !isDesktop && !store.onboarding.isActive
|
||||
const showSideNavs =
|
||||
isDesktop && store.session.hasSession && !store.onboarding.isActive
|
||||
return (
|
||||
<>
|
||||
<View style={s.hContentRegion}>
|
||||
|
@ -36,7 +38,7 @@ const ShellInner = observer(() => {
|
|||
<FlatNavigator />
|
||||
</ErrorBoundary>
|
||||
</View>
|
||||
{isDesktop && store.session.hasSession && (
|
||||
{showSideNavs && (
|
||||
<>
|
||||
<DesktopLeftNav />
|
||||
<DesktopRightNav />
|
||||
|
@ -51,7 +53,7 @@ const ShellInner = observer(() => {
|
|||
onPost={store.shell.composerOpts?.onPost}
|
||||
mention={store.shell.composerOpts?.mention}
|
||||
/>
|
||||
{!isDesktop && <BottomBarWeb />}
|
||||
{showBottomBar && <BottomBarWeb />}
|
||||
<ModalsContainer />
|
||||
<Lightbox />
|
||||
{!isDesktop && store.shell.isDrawerOpen && (
|
||||
|
|
Loading…
Reference in New Issue