Add onboarding (WIP)
parent
b4097e25d6
commit
d228a5f4f5
|
@ -220,6 +220,8 @@ PODS:
|
||||||
- React-jsinspector (0.68.2)
|
- React-jsinspector (0.68.2)
|
||||||
- React-logger (0.68.2):
|
- React-logger (0.68.2):
|
||||||
- glog
|
- glog
|
||||||
|
- react-native-pager-view (6.0.2):
|
||||||
|
- React-Core
|
||||||
- react-native-safe-area-context (4.3.4):
|
- react-native-safe-area-context (4.3.4):
|
||||||
- RCT-Folly
|
- RCT-Folly
|
||||||
- RCTRequired
|
- RCTRequired
|
||||||
|
@ -357,6 +359,7 @@ DEPENDENCIES:
|
||||||
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
|
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
|
||||||
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
|
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
|
||||||
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
||||||
|
- react-native-pager-view (from `../node_modules/react-native-pager-view`)
|
||||||
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
||||||
- react-native-splash-screen (from `../node_modules/react-native-splash-screen`)
|
- react-native-splash-screen (from `../node_modules/react-native-splash-screen`)
|
||||||
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
|
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
|
||||||
|
@ -423,6 +426,8 @@ EXTERNAL SOURCES:
|
||||||
:path: "../node_modules/react-native/ReactCommon/jsinspector"
|
:path: "../node_modules/react-native/ReactCommon/jsinspector"
|
||||||
React-logger:
|
React-logger:
|
||||||
:path: "../node_modules/react-native/ReactCommon/logger"
|
:path: "../node_modules/react-native/ReactCommon/logger"
|
||||||
|
react-native-pager-view:
|
||||||
|
:path: "../node_modules/react-native-pager-view"
|
||||||
react-native-safe-area-context:
|
react-native-safe-area-context:
|
||||||
:path: "../node_modules/react-native-safe-area-context"
|
:path: "../node_modules/react-native-safe-area-context"
|
||||||
react-native-splash-screen:
|
react-native-splash-screen:
|
||||||
|
@ -489,6 +494,7 @@ SPEC CHECKSUMS:
|
||||||
React-jsiexecutor: b7b553412f2ec768fe6c8f27cd6bafdb9d8719e6
|
React-jsiexecutor: b7b553412f2ec768fe6c8f27cd6bafdb9d8719e6
|
||||||
React-jsinspector: c5989c77cb89ae6a69561095a61cce56a44ae8e8
|
React-jsinspector: c5989c77cb89ae6a69561095a61cce56a44ae8e8
|
||||||
React-logger: a0833912d93b36b791b7a521672d8ee89107aff1
|
React-logger: a0833912d93b36b791b7a521672d8ee89107aff1
|
||||||
|
react-native-pager-view: 592421df0259bf7a7a4fe85b74c24f3f39905605
|
||||||
react-native-safe-area-context: dfe5aa13bee37a0c7e8059d14f72ffc076d120e9
|
react-native-safe-area-context: dfe5aa13bee37a0c7e8059d14f72ffc076d120e9
|
||||||
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
|
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
|
||||||
React-perflogger: a18b4f0bd933b8b24ecf9f3c54f9bf65180f3fe6
|
React-perflogger: a18b4f0bd933b8b24ecf9f3c54f9bf65180f3fe6
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"react-native-gesture-handler": "^2.5.0",
|
"react-native-gesture-handler": "^2.5.0",
|
||||||
"react-native-inappbrowser-reborn": "^3.6.3",
|
"react-native-inappbrowser-reborn": "^3.6.3",
|
||||||
"react-native-linear-gradient": "^2.6.2",
|
"react-native-linear-gradient": "^2.6.2",
|
||||||
|
"react-native-pager-view": "^6.0.2",
|
||||||
"react-native-progress": "^5.0.0",
|
"react-native-progress": "^5.0.0",
|
||||||
"react-native-reanimated": "^2.9.1",
|
"react-native-reanimated": "^2.9.1",
|
||||||
"react-native-root-siblings": "^4.1.1",
|
"react-native-root-siblings": "^4.1.1",
|
||||||
|
@ -40,6 +41,7 @@
|
||||||
"react-native-screens": "^3.13.1",
|
"react-native-screens": "^3.13.1",
|
||||||
"react-native-splash-screen": "^3.3.0",
|
"react-native-splash-screen": "^3.3.0",
|
||||||
"react-native-svg": "^12.4.0",
|
"react-native-svg": "^12.4.0",
|
||||||
|
"react-native-tab-view": "^3.3.0",
|
||||||
"react-native-url-polyfill": "^1.3.0",
|
"react-native-url-polyfill": "^1.3.0",
|
||||||
"react-native-web": "^0.17.7"
|
"react-native-web": "^0.17.7"
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,13 +23,14 @@ export async function setupState() {
|
||||||
console.error('Failed to load state from storage', e)
|
console.error('Failed to load state from storage', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await rootStore.session.setup()
|
||||||
|
|
||||||
// track changes & save to storage
|
// track changes & save to storage
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
const snapshot = rootStore.serialize()
|
const snapshot = rootStore.serialize()
|
||||||
storage.save(ROOT_STATE_STORAGE_KEY, snapshot)
|
storage.save(ROOT_STATE_STORAGE_KEY, snapshot)
|
||||||
})
|
})
|
||||||
|
|
||||||
await rootStore.session.setup()
|
|
||||||
await rootStore.fetchStateUpdate()
|
await rootStore.fetchStateUpdate()
|
||||||
console.log(rootStore.me)
|
console.log(rootStore.me)
|
||||||
|
|
||||||
|
|
|
@ -8,3 +8,7 @@ export function hasProp<K extends PropertyKey>(
|
||||||
): data is Record<K, unknown> {
|
): data is Record<K, unknown> {
|
||||||
return prop in data
|
return prop in data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isStrArray(v: unknown): v is string[] {
|
||||||
|
return Array.isArray(v) && v.every(item => typeof item === 'string')
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import {makeAutoObservable} from 'mobx'
|
||||||
|
import {isObj, hasProp} from '../lib/type-guards'
|
||||||
|
|
||||||
|
export const OnboardStage = {
|
||||||
|
Explainers: 'explainers',
|
||||||
|
Follows: 'follows',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OnboardStageOrder = [OnboardStage.Explainers, OnboardStage.Follows]
|
||||||
|
|
||||||
|
export class OnboardModel {
|
||||||
|
isOnboarding: boolean = true
|
||||||
|
stage: string = OnboardStageOrder[0]
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this, {
|
||||||
|
serialize: false,
|
||||||
|
hydrate: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(): unknown {
|
||||||
|
return {
|
||||||
|
isOnboarding: this.isOnboarding,
|
||||||
|
stage: this.stage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrate(v: unknown) {
|
||||||
|
if (isObj(v)) {
|
||||||
|
if (hasProp(v, 'isOnboarding') && typeof v.isOnboarding === 'boolean') {
|
||||||
|
this.isOnboarding = v.isOnboarding
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
hasProp(v, 'stage') &&
|
||||||
|
typeof v.stage === 'string' &&
|
||||||
|
OnboardStageOrder.includes(v.stage)
|
||||||
|
) {
|
||||||
|
this.stage = v.stage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.isOnboarding = true
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.isOnboarding = false
|
||||||
|
}
|
||||||
|
|
||||||
|
next() {
|
||||||
|
if (!this.isOnboarding) return
|
||||||
|
let i = OnboardStageOrder.indexOf(this.stage)
|
||||||
|
i++
|
||||||
|
if (i >= OnboardStageOrder.length) {
|
||||||
|
this.isOnboarding = false
|
||||||
|
} else {
|
||||||
|
this.stage = OnboardStageOrder[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,12 +11,14 @@ import {SessionModel} from './session'
|
||||||
import {NavigationModel} from './navigation'
|
import {NavigationModel} from './navigation'
|
||||||
import {ShellModel} from './shell'
|
import {ShellModel} from './shell'
|
||||||
import {MeModel} from './me'
|
import {MeModel} from './me'
|
||||||
|
import {OnboardModel} from './onboard'
|
||||||
|
|
||||||
export class RootStoreModel {
|
export class RootStoreModel {
|
||||||
session = new SessionModel(this)
|
session = new SessionModel(this)
|
||||||
nav = new NavigationModel()
|
nav = new NavigationModel()
|
||||||
shell = new ShellModel()
|
shell = new ShellModel()
|
||||||
me = new MeModel(this)
|
me = new MeModel(this)
|
||||||
|
onboard = new OnboardModel()
|
||||||
|
|
||||||
constructor(public api: SessionServiceClient) {
|
constructor(public api: SessionServiceClient) {
|
||||||
makeAutoObservable(this, {
|
makeAutoObservable(this, {
|
||||||
|
@ -53,6 +55,7 @@ export class RootStoreModel {
|
||||||
return {
|
return {
|
||||||
session: this.session.serialize(),
|
session: this.session.serialize(),
|
||||||
nav: this.nav.serialize(),
|
nav: this.nav.serialize(),
|
||||||
|
onboard: this.onboard.serialize(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +67,9 @@ export class RootStoreModel {
|
||||||
if (hasProp(v, 'nav')) {
|
if (hasProp(v, 'nav')) {
|
||||||
this.nav.hydrate(v.nav)
|
this.nav.hydrate(v.nav)
|
||||||
}
|
}
|
||||||
|
if (hasProp(v, 'onboard')) {
|
||||||
|
this.onboard.hydrate(v.onboard)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,17 +15,8 @@ interface SessionData {
|
||||||
did: string
|
did: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum OnboardingStage {
|
|
||||||
Init = 'init',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OnboardingState {
|
|
||||||
stage: OnboardingStage
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SessionModel {
|
export class SessionModel {
|
||||||
data: SessionData | null = null
|
data: SessionData | null = null
|
||||||
onboardingState: OnboardingState | null = null
|
|
||||||
|
|
||||||
constructor(public rootStore: RootStoreModel) {
|
constructor(public rootStore: RootStoreModel) {
|
||||||
makeAutoObservable(this, {
|
makeAutoObservable(this, {
|
||||||
|
@ -42,7 +33,6 @@ export class SessionModel {
|
||||||
serialize(): unknown {
|
serialize(): unknown {
|
||||||
return {
|
return {
|
||||||
data: this.data,
|
data: this.data,
|
||||||
onboardingState: this.onboardingState,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,18 +77,6 @@ export class SessionModel {
|
||||||
this.data = data
|
this.data = data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
this.data &&
|
|
||||||
hasProp(v, 'onboardingState') &&
|
|
||||||
isObj(v.onboardingState)
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
hasProp(v.onboardingState, 'stage') &&
|
|
||||||
typeof v.onboardingState === 'string'
|
|
||||||
) {
|
|
||||||
this.onboardingState = v.onboardingState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,7 +190,7 @@ export class SessionModel {
|
||||||
handle: res.data.handle,
|
handle: res.data.handle,
|
||||||
did: res.data.did,
|
did: res.data.did,
|
||||||
})
|
})
|
||||||
this.setOnboardingStage(OnboardingStage.Init)
|
this.rootStore.onboard.start()
|
||||||
this.configureApi()
|
this.configureApi()
|
||||||
this.rootStore.me.load().catch(e => {
|
this.rootStore.me.load().catch(e => {
|
||||||
console.error('Failed to fetch local user information', e)
|
console.error('Failed to fetch local user information', e)
|
||||||
|
@ -228,12 +206,4 @@ export class SessionModel {
|
||||||
}
|
}
|
||||||
this.rootStore.clearAll()
|
this.rootStore.clearAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnboardingStage(stage: OnboardingStage | null) {
|
|
||||||
if (stage === null) {
|
|
||||||
this.onboardingState = null
|
|
||||||
} else {
|
|
||||||
this.onboardingState = {stage}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
import {makeAutoObservable} from 'mobx'
|
||||||
|
import {RootStoreModel} from './root-store'
|
||||||
|
|
||||||
|
interface Response {
|
||||||
|
data: {
|
||||||
|
suggestions: ResponseSuggestedActor[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export type ResponseSuggestedActor = {
|
||||||
|
did: string
|
||||||
|
handle: string
|
||||||
|
displayName?: string
|
||||||
|
description?: string
|
||||||
|
createdAt?: string
|
||||||
|
indexedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SuggestedActor = ResponseSuggestedActor & {
|
||||||
|
_reactKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SuggestedActorsViewModel {
|
||||||
|
// state
|
||||||
|
isLoading = false
|
||||||
|
isRefreshing = false
|
||||||
|
hasLoaded = false
|
||||||
|
error = ''
|
||||||
|
|
||||||
|
// data
|
||||||
|
suggestions: SuggestedActor[] = []
|
||||||
|
|
||||||
|
constructor(public rootStore: RootStoreModel) {
|
||||||
|
makeAutoObservable(
|
||||||
|
this,
|
||||||
|
{
|
||||||
|
rootStore: false,
|
||||||
|
},
|
||||||
|
{autoBind: true},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasContent() {
|
||||||
|
return this.suggestions.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasError() {
|
||||||
|
return this.error !== ''
|
||||||
|
}
|
||||||
|
|
||||||
|
get isEmpty() {
|
||||||
|
return this.hasLoaded && !this.hasContent
|
||||||
|
}
|
||||||
|
|
||||||
|
// public api
|
||||||
|
// =
|
||||||
|
|
||||||
|
async setup() {
|
||||||
|
await this._fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh() {
|
||||||
|
await this._fetch(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadMore() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
// state transitions
|
||||||
|
// =
|
||||||
|
|
||||||
|
private _xLoading(isRefreshing = false) {
|
||||||
|
this.isLoading = true
|
||||||
|
this.isRefreshing = isRefreshing
|
||||||
|
this.error = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
private _xIdle(err: string = '') {
|
||||||
|
this.isLoading = false
|
||||||
|
this.isRefreshing = false
|
||||||
|
this.hasLoaded = true
|
||||||
|
this.error = err
|
||||||
|
}
|
||||||
|
|
||||||
|
// loader functions
|
||||||
|
// =
|
||||||
|
|
||||||
|
private async _fetch(isRefreshing = false) {
|
||||||
|
this._xLoading(isRefreshing)
|
||||||
|
try {
|
||||||
|
const debugRes = await this.rootStore.api.app.bsky.graph.getFollowers({
|
||||||
|
user: 'alice.test',
|
||||||
|
})
|
||||||
|
const res = {
|
||||||
|
data: {
|
||||||
|
suggestions: debugRes.data.followers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
this._replaceAll(res)
|
||||||
|
this._xIdle()
|
||||||
|
} catch (e: any) {
|
||||||
|
this._xIdle(e.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _replaceAll(res: Response) {
|
||||||
|
this.suggestions.length = 0
|
||||||
|
let counter = 0
|
||||||
|
for (const item of res.data.suggestions) {
|
||||||
|
this._append({
|
||||||
|
_reactKey: `item-${counter++}`,
|
||||||
|
description: 'Just another cool person using Bluesky',
|
||||||
|
...item,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _append(item: SuggestedActor) {
|
||||||
|
this.suggestions.push(item)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
import React, {useState} from 'react'
|
||||||
|
import {
|
||||||
|
Animated,
|
||||||
|
SafeAreaView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
useWindowDimensions,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
|
import {TabView, SceneMap, Route, TabBarProps} from 'react-native-tab-view'
|
||||||
|
import {UserGroupIcon} from '../../lib/icons'
|
||||||
|
import {useStores} from '../../../state'
|
||||||
|
import {s} from '../../lib/styles'
|
||||||
|
|
||||||
|
const Scenes = () => (
|
||||||
|
<View style={styles.explainer}>
|
||||||
|
<View style={styles.explainerIcon}>
|
||||||
|
<View style={s.flex1} />
|
||||||
|
<UserGroupIcon style={s.black} size="48" />
|
||||||
|
<View style={s.flex1} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.explainerHeading}>Scenes</Text>
|
||||||
|
<Text style={styles.explainerDesc}>
|
||||||
|
Scenes are invite-only groups of users. Follow them to see what's trending
|
||||||
|
with the scene's members.
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.explainerDesc}>[ TODO screenshot ]</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
|
||||||
|
const SCENE_MAP = {
|
||||||
|
scenes: Scenes,
|
||||||
|
}
|
||||||
|
const renderScene = SceneMap(SCENE_MAP)
|
||||||
|
|
||||||
|
export const FeatureExplainer = () => {
|
||||||
|
const layout = useWindowDimensions()
|
||||||
|
const store = useStores()
|
||||||
|
const [index, setIndex] = useState(0)
|
||||||
|
const routes = [{key: 'scenes', title: 'Scenes'}]
|
||||||
|
|
||||||
|
const onPressSkip = () => store.onboard.next()
|
||||||
|
const onPressNext = () => {
|
||||||
|
if (index >= routes.length - 1) {
|
||||||
|
store.onboard.next()
|
||||||
|
} else {
|
||||||
|
setIndex(index + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderTabBar = (props: TabBarProps<Route>) => {
|
||||||
|
const inputRange = props.navigationState.routes.map((x, i) => i)
|
||||||
|
return (
|
||||||
|
<View style={styles.tabBar}>
|
||||||
|
<View style={s.flex1} />
|
||||||
|
{props.navigationState.routes.map((route, i) => {
|
||||||
|
const opacity = props.position.interpolate({
|
||||||
|
inputRange,
|
||||||
|
outputRange: inputRange.map(inputIndex =>
|
||||||
|
inputIndex === i ? 1 : 0.5,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={i}
|
||||||
|
style={styles.tabItem}
|
||||||
|
onPress={() => setIndex(i)}>
|
||||||
|
<Animated.Text style={{opacity}}>°</Animated.Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<View style={s.flex1} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FirstExplainer = SCENE_MAP[routes[0]?.key as keyof typeof SCENE_MAP]
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
{routes.length > 1 ? (
|
||||||
|
<TabView
|
||||||
|
navigationState={{index, routes}}
|
||||||
|
renderScene={renderScene}
|
||||||
|
renderTabBar={renderTabBar}
|
||||||
|
onIndexChange={setIndex}
|
||||||
|
initialLayout={{width: layout.width}}
|
||||||
|
tabBarPosition="bottom"
|
||||||
|
/>
|
||||||
|
) : FirstExplainer ? (
|
||||||
|
<FirstExplainer />
|
||||||
|
) : (
|
||||||
|
<View />
|
||||||
|
)}
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<TouchableOpacity onPress={onPressSkip}>
|
||||||
|
<Text style={[s.blue3, s.f18]}>Skip</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={s.flex1} />
|
||||||
|
<TouchableOpacity onPress={onPressNext}>
|
||||||
|
<Text style={[s.blue3, s.f18]}>Next</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
tabBar: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
tabItem: {
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
|
||||||
|
explainer: {
|
||||||
|
flex: 1,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 16,
|
||||||
|
},
|
||||||
|
explainerIcon: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
explainerHeading: {
|
||||||
|
fontSize: 42,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
explainerDesc: {
|
||||||
|
fontSize: 18,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
|
||||||
|
footer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
paddingHorizontal: 32,
|
||||||
|
paddingBottom: 24,
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,202 @@
|
||||||
|
import React, {useMemo, useEffect} from 'react'
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
FlatList,
|
||||||
|
SafeAreaView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {observer} from 'mobx-react-lite'
|
||||||
|
import {ErrorScreen} from '../util/ErrorScreen'
|
||||||
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
|
import {useStores} from '../../../state'
|
||||||
|
import {
|
||||||
|
SuggestedActorsViewModel,
|
||||||
|
SuggestedActor,
|
||||||
|
} from '../../../state/models/suggested-actors-view'
|
||||||
|
import {s, colors, gradients} from '../../lib/styles'
|
||||||
|
|
||||||
|
export const Follows = observer(() => {
|
||||||
|
const store = useStores()
|
||||||
|
|
||||||
|
const view = useMemo<SuggestedActorsViewModel>(
|
||||||
|
() => new SuggestedActorsViewModel(store),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('Fetching suggested actors')
|
||||||
|
view
|
||||||
|
.setup()
|
||||||
|
.catch((err: any) => console.error('Failed to fetch suggestions', err))
|
||||||
|
}, [view])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!view.isLoading && !view.hasError && !view.hasContent) {
|
||||||
|
// no suggestions, bounce from this view
|
||||||
|
store.onboard.next()
|
||||||
|
}
|
||||||
|
}, [view, view.isLoading, view.hasError, view.hasContent])
|
||||||
|
|
||||||
|
const onPressTryAgain = () =>
|
||||||
|
view
|
||||||
|
.setup()
|
||||||
|
.catch((err: any) => console.error('Failed to fetch suggestions', err))
|
||||||
|
const onPressNext = () => store.onboard.next()
|
||||||
|
|
||||||
|
const renderItem = ({item}: {item: SuggestedActor}) => <User item={item} />
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
<Text style={styles.title}>Suggested follows</Text>
|
||||||
|
{view.isLoading ? (
|
||||||
|
<View>
|
||||||
|
<ActivityIndicator />
|
||||||
|
</View>
|
||||||
|
) : view.hasError ? (
|
||||||
|
<ErrorScreen
|
||||||
|
title="Failed to load suggestions"
|
||||||
|
message="There was an error while trying to load suggested follows."
|
||||||
|
details={view.error}
|
||||||
|
onPressTryAgain={onPressTryAgain}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View style={styles.suggestionsContainer}>
|
||||||
|
<FlatList
|
||||||
|
data={view.suggestions}
|
||||||
|
keyExtractor={item => item._reactKey}
|
||||||
|
renderItem={renderItem}
|
||||||
|
style={s.flex1}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<TouchableOpacity onPress={onPressNext}>
|
||||||
|
<Text style={[s.blue3, s.f18]}>Skip</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={s.flex1} />
|
||||||
|
<TouchableOpacity onPress={onPressNext}>
|
||||||
|
<Text style={[s.blue3, s.f18]}>Next</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const User = ({item}: {item: SuggestedActor}) => {
|
||||||
|
return (
|
||||||
|
<View style={styles.actor}>
|
||||||
|
<View style={styles.actorMeta}>
|
||||||
|
<View style={styles.actorAvi}>
|
||||||
|
<UserAvatar
|
||||||
|
size={40}
|
||||||
|
displayName={item.displayName}
|
||||||
|
handle={item.handle}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={styles.actorContent}>
|
||||||
|
<Text style={[s.f17, s.bold]} numberOfLines={1}>
|
||||||
|
{item.displayName}
|
||||||
|
</Text>
|
||||||
|
<Text style={[s.f14, s.gray5]} numberOfLines={1}>
|
||||||
|
@{item.handle}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.actorBtn}>
|
||||||
|
<TouchableOpacity>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[gradients.primary.start, gradients.primary.end]}
|
||||||
|
start={{x: 0, y: 0}}
|
||||||
|
end={{x: 1, y: 1}}
|
||||||
|
style={[styles.btn, styles.gradientBtn]}>
|
||||||
|
<FontAwesomeIcon icon="plus" style={[s.white, s.mr5]} size={15} />
|
||||||
|
<Text style={[s.white, s.fw600, s.f15]}>Follow</Text>
|
||||||
|
</LinearGradient>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{item.description ? (
|
||||||
|
<View style={styles.actorDetails}>
|
||||||
|
<Text style={[s.f15]} numberOfLines={4}>
|
||||||
|
{item.description}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : undefined}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
title: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingBottom: 12,
|
||||||
|
},
|
||||||
|
|
||||||
|
suggestionsContainer: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: colors.gray1,
|
||||||
|
},
|
||||||
|
|
||||||
|
actor: {
|
||||||
|
backgroundColor: colors.white,
|
||||||
|
borderRadius: 6,
|
||||||
|
margin: 2,
|
||||||
|
marginBottom: 0,
|
||||||
|
},
|
||||||
|
actorMeta: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
actorAvi: {
|
||||||
|
width: 60,
|
||||||
|
paddingLeft: 10,
|
||||||
|
paddingTop: 10,
|
||||||
|
paddingBottom: 10,
|
||||||
|
},
|
||||||
|
actorContent: {
|
||||||
|
flex: 1,
|
||||||
|
paddingRight: 10,
|
||||||
|
paddingTop: 10,
|
||||||
|
},
|
||||||
|
actorBtn: {
|
||||||
|
paddingRight: 10,
|
||||||
|
paddingTop: 10,
|
||||||
|
},
|
||||||
|
actorDetails: {
|
||||||
|
paddingLeft: 60,
|
||||||
|
paddingRight: 10,
|
||||||
|
paddingBottom: 10,
|
||||||
|
},
|
||||||
|
|
||||||
|
gradientBtn: {
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
paddingVertical: 6,
|
||||||
|
},
|
||||||
|
secondaryBtn: {
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
},
|
||||||
|
btn: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingVertical: 7,
|
||||||
|
borderRadius: 50,
|
||||||
|
backgroundColor: colors.gray1,
|
||||||
|
marginLeft: 6,
|
||||||
|
},
|
||||||
|
|
||||||
|
footer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
paddingHorizontal: 32,
|
||||||
|
paddingBottom: 24,
|
||||||
|
paddingTop: 16,
|
||||||
|
},
|
||||||
|
})
|
|
@ -2,7 +2,6 @@ import React from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Image,
|
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
|
|
|
@ -441,7 +441,6 @@ function cleanUsername(v: string): string {
|
||||||
|
|
||||||
export const Login = observer(
|
export const Login = observer(
|
||||||
(/*{navigation}: RootTabsScreenProps<'Login'>*/) => {
|
(/*{navigation}: RootTabsScreenProps<'Login'>*/) => {
|
||||||
// const store = useStores()
|
|
||||||
const [screenState, setScreenState] = useState<ScreenState>(
|
const [screenState, setScreenState] = useState<ScreenState>(
|
||||||
ScreenState.SigninOrCreateAccount,
|
ScreenState.SigninOrCreateAccount,
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import React, {useEffect} from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {observer} from 'mobx-react-lite'
|
||||||
|
import {FeatureExplainer} from '../com/onboard/FeatureExplainer'
|
||||||
|
import {Follows} from '../com/onboard/Follows'
|
||||||
|
import {OnboardStage, OnboardStageOrder} from '../../state/models/onboard'
|
||||||
|
import {useStores} from '../../state'
|
||||||
|
|
||||||
|
export const Onboard = observer(() => {
|
||||||
|
const store = useStores()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// sanity check - bounce out of onboarding if the stage is wrong somehow
|
||||||
|
if (!OnboardStageOrder.includes(store.onboard.stage)) {
|
||||||
|
store.onboard.stop()
|
||||||
|
}
|
||||||
|
}, [store.onboard.stage])
|
||||||
|
|
||||||
|
let Com
|
||||||
|
if (store.onboard.stage === OnboardStage.Explainers) {
|
||||||
|
Com = FeatureExplainer
|
||||||
|
} else if (store.onboard.stage === OnboardStage.Follows) {
|
||||||
|
Com = Follows
|
||||||
|
} else {
|
||||||
|
Com = View
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{flex: 1}}>
|
||||||
|
<Com />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
|
@ -27,6 +27,7 @@ import {useStores} from '../../../state'
|
||||||
import {NavigationModel} from '../../../state/models/navigation'
|
import {NavigationModel} from '../../../state/models/navigation'
|
||||||
import {match, MatchResult} from '../../routes'
|
import {match, MatchResult} from '../../routes'
|
||||||
import {Login} from '../../screens/Login'
|
import {Login} from '../../screens/Login'
|
||||||
|
import {Onboard} from '../../screens/Onboard'
|
||||||
import {Modal} from '../../com/modals/Modal'
|
import {Modal} from '../../com/modals/Modal'
|
||||||
import {MainMenu} from './MainMenu'
|
import {MainMenu} from './MainMenu'
|
||||||
import {TabsSelector} from './TabsSelector'
|
import {TabsSelector} from './TabsSelector'
|
||||||
|
@ -161,6 +162,15 @@ export const MobileShell: React.FC = observer(() => {
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (store.onboard.isOnboarding) {
|
||||||
|
return (
|
||||||
|
<View style={styles.outerContainer}>
|
||||||
|
<View style={styles.innerContainer}>
|
||||||
|
<Onboard />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.outerContainer}>
|
<View style={styles.outerContainer}>
|
||||||
|
|
17
yarn.lock
17
yarn.lock
|
@ -10177,6 +10177,11 @@ react-native-linear-gradient@^2.6.2:
|
||||||
resolved "https://registry.yarnpkg.com/react-native-linear-gradient/-/react-native-linear-gradient-2.6.2.tgz#56598a76832724b2afa7889747635b5c80948f38"
|
resolved "https://registry.yarnpkg.com/react-native-linear-gradient/-/react-native-linear-gradient-2.6.2.tgz#56598a76832724b2afa7889747635b5c80948f38"
|
||||||
integrity sha512-Z8Xxvupsex+9BBFoSYS87bilNPWcRfRsGC0cpJk72Nxb5p2nEkGSBv73xZbEHnW2mUFvP+huYxrVvjZkr/gRjQ==
|
integrity sha512-Z8Xxvupsex+9BBFoSYS87bilNPWcRfRsGC0cpJk72Nxb5p2nEkGSBv73xZbEHnW2mUFvP+huYxrVvjZkr/gRjQ==
|
||||||
|
|
||||||
|
react-native-pager-view@^6.0.2:
|
||||||
|
version "6.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.0.2.tgz#447b85fcb9f35225c4d6885c18689a7d30c181d9"
|
||||||
|
integrity sha512-XL3Qc9k7o0BykclGHtuRUz97FpF6rcKbP8LqszLeS2hKnINYcbUPYqg46EhbwVhFOUJE+XhT3idrSO1e/D6jtQ==
|
||||||
|
|
||||||
react-native-progress@^5.0.0:
|
react-native-progress@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-progress/-/react-native-progress-5.0.0.tgz#f5ac6ceaeee27f184c660b00f29419e82a9d0ab0"
|
resolved "https://registry.yarnpkg.com/react-native-progress/-/react-native-progress-5.0.0.tgz#f5ac6ceaeee27f184c660b00f29419e82a9d0ab0"
|
||||||
|
@ -10237,6 +10242,13 @@ react-native-svg@^12.4.0:
|
||||||
css-select "^5.1.0"
|
css-select "^5.1.0"
|
||||||
css-tree "^1.1.3"
|
css-tree "^1.1.3"
|
||||||
|
|
||||||
|
react-native-tab-view@^3.3.0:
|
||||||
|
version "3.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-tab-view/-/react-native-tab-view-3.3.0.tgz#3d24ae4f4c55cfd54cd1d2d1f8915b0b2c33a5da"
|
||||||
|
integrity sha512-xjAQe657Gp/de2QHb7ptksTg8Jcb+j3fLAdcYryzfavt/pe+HtKLpkCtQsxyIJpRrAO7YPxFsymi2N4MnNfePA==
|
||||||
|
dependencies:
|
||||||
|
use-latest-callback "^0.1.5"
|
||||||
|
|
||||||
react-native-url-polyfill@^1.3.0:
|
react-native-url-polyfill@^1.3.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-url-polyfill/-/react-native-url-polyfill-1.3.0.tgz#c1763de0f2a8c22cc3e959b654c8790622b6ef6a"
|
resolved "https://registry.yarnpkg.com/react-native-url-polyfill/-/react-native-url-polyfill-1.3.0.tgz#c1763de0f2a8c22cc3e959b654c8790622b6ef6a"
|
||||||
|
@ -11980,6 +11992,11 @@ url-parse@^1.5.3:
|
||||||
querystringify "^2.1.1"
|
querystringify "^2.1.1"
|
||||||
requires-port "^1.0.0"
|
requires-port "^1.0.0"
|
||||||
|
|
||||||
|
use-latest-callback@^0.1.5:
|
||||||
|
version "0.1.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.5.tgz#a4a836c08fa72f6608730b5b8f4bbd9c57c04f51"
|
||||||
|
integrity sha512-HtHatS2U4/h32NlkhupDsPlrbiD27gSH5swBdtXbCAlc6pfOFzaj0FehW/FO12rx8j2Vy4/lJScCiJyM01E+bQ==
|
||||||
|
|
||||||
"use-subscription@>=1.0.0 <1.6.0":
|
"use-subscription@>=1.0.0 <1.6.0":
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1"
|
resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1"
|
||||||
|
|
Loading…
Reference in New Issue