Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork

Add state management

+2 -1
.eslintrc.js
··· 10 10 '@typescript-eslint/no-shadow': 'off', 11 11 'no-shadow': 'off', 12 12 'no-undef': 'off', 13 + semi: [2, 'never'], 13 14 }, 14 15 }, 15 16 ], 16 - }; 17 + }
+2 -1
.prettierrc.js
··· 1 1 module.exports = { 2 + semi: false, 2 3 arrowParens: 'avoid', 3 4 bracketSameLine: true, 4 5 bracketSpacing: false, 5 6 singleQuote: true, 6 7 trailingComma: 'all', 7 - }; 8 + }
+2 -2
README.md
··· 7 7 - [React Native](https://reactnative.dev) 8 8 - [React Native for Web](https://necolas.github.io/react-native-web/) 9 9 - [React Navigation](https://reactnative.dev/docs/navigation#react-navigation) 10 - - (todo) [MobX](https://mobx.js.org/README.html) and [MobX State Tree](https://mobx-state-tree.js.org/) 11 - - (todo) [Async Storage](https://github.com/react-native-async-storage/async-storage) 10 + - [MobX](https://mobx.js.org/README.html) and [MobX State Tree](https://mobx-state-tree.js.org/) 11 + - [Async Storage](https://github.com/react-native-async-storage/async-storage) 12 12 13 13 ## Build instructions 14 14
+7 -7
__tests__/App-test.tsx
··· 2 2 * @format 3 3 */ 4 4 5 - import 'react-native'; 6 - import React from 'react'; 7 - import App from '../src/App'; 5 + import 'react-native' 6 + import React from 'react' 7 + import App from '../src/App' 8 8 9 9 // Note: test renderer must be required after react-native. 10 - import renderer from 'react-test-renderer'; 10 + import renderer from 'react-test-renderer' 11 11 12 12 it('renders correctly', () => { 13 13 renderer.act(() => { 14 - renderer.create(<App />); 15 - }); 16 - }); 14 + renderer.create(<App />) 15 + }) 16 + })
+1 -1
babel.config.js
··· 1 1 module.exports = { 2 2 presets: ['module:metro-react-native-babel-preset'], 3 - }; 3 + }
+4 -4
index.native.js
··· 2 2 * @format 3 3 */ 4 4 5 - import {AppRegistry} from 'react-native'; 6 - import App from './src/App'; 7 - import {name as appName} from './src/app.json'; 5 + import {AppRegistry} from 'react-native' 6 + import App from './src/App' 7 + import {name as appName} from './src/app.json' 8 8 9 - AppRegistry.registerComponent(appName, () => App); 9 + AppRegistry.registerComponent(appName, () => App)
+1 -1
metro.config.js
··· 14 14 }, 15 15 }), 16 16 }, 17 - }; 17 + }
+3
package.json
··· 11 11 "lint": "eslint . --ext .js,.jsx,.ts,.tsx" 12 12 }, 13 13 "dependencies": { 14 + "@react-native-async-storage/async-storage": "^1.17.6", 14 15 "@react-navigation/native": "^6.0.10", 15 16 "@react-navigation/native-stack": "^6.6.2", 17 + "mobx": "^6.6.0", 18 + "mobx-state-tree": "^5.1.5", 16 19 "react": "17.0.2", 17 20 "react-dom": "17.0.2", 18 21 "react-native": "0.68.2",
+80
src/App.native.tsx
··· 1 + import React, {useState, useEffect} from 'react' 2 + import { 3 + SafeAreaView, 4 + ScrollView, 5 + StatusBar, 6 + Text, 7 + Button, 8 + useColorScheme, 9 + View, 10 + } from 'react-native' 11 + import {NavigationContainer} from '@react-navigation/native' 12 + import { 13 + createNativeStackNavigator, 14 + NativeStackScreenProps, 15 + } from '@react-navigation/native-stack' 16 + import {RootStore, setupState, RootStoreProvider} from './state' 17 + 18 + type RootStackParamList = { 19 + Home: undefined 20 + Profile: {name: string} 21 + } 22 + const Stack = createNativeStackNavigator() 23 + 24 + const HomeScreen = ({ 25 + navigation, 26 + }: NativeStackScreenProps<RootStackParamList, 'Home'>) => { 27 + const isDarkMode = useColorScheme() === 'dark' 28 + 29 + return ( 30 + <SafeAreaView> 31 + <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} /> 32 + <ScrollView contentInsetAdjustmentBehavior="automatic"> 33 + <View> 34 + <Text>Native</Text> 35 + <Button 36 + title="Go to Jane's profile" 37 + onPress={() => navigation.navigate('Profile', {name: 'Jane'})} 38 + /> 39 + </View> 40 + </ScrollView> 41 + </SafeAreaView> 42 + ) 43 + } 44 + 45 + const ProfileScreen = ({ 46 + route, 47 + }: NativeStackScreenProps<RootStackParamList, 'Profile'>) => { 48 + return <Text>This is {route.params.name}'s profile</Text> 49 + } 50 + 51 + function App() { 52 + const [rootStore, setRootStore] = useState<RootStore | undefined>(undefined) 53 + 54 + // init 55 + useEffect(() => { 56 + setupState().then(setRootStore) 57 + }, []) 58 + 59 + // show nothing prior to init 60 + if (!rootStore) { 61 + return null 62 + } 63 + 64 + return ( 65 + <RootStoreProvider value={rootStore}> 66 + <NavigationContainer> 67 + <Stack.Navigator> 68 + <Stack.Screen 69 + name="Home" 70 + component={HomeScreen} 71 + options={{title: 'Welcome'}} 72 + /> 73 + <Stack.Screen name="Profile" component={ProfileScreen} /> 74 + </Stack.Navigator> 75 + </NavigationContainer> 76 + </RootStoreProvider> 77 + ) 78 + } 79 + 80 + export default App
-112
src/App.tsx
··· 1 - /** 2 - * Sample React Native App 3 - * https://github.com/facebook/react-native 4 - * 5 - * Generated with the TypeScript template 6 - * https://github.com/react-native-community/react-native-template-typescript 7 - * 8 - * @format 9 - */ 10 - 11 - import React from 'react'; 12 - import { 13 - SafeAreaView, 14 - ScrollView, 15 - StatusBar, 16 - StyleSheet, 17 - Text, 18 - Button, 19 - useColorScheme, 20 - View, 21 - } from 'react-native'; 22 - import {NavigationContainer} from '@react-navigation/native'; 23 - import { 24 - createNativeStackNavigator, 25 - NativeStackScreenProps, 26 - } from '@react-navigation/native-stack'; 27 - 28 - type RootStackParamList = { 29 - Home: undefined; 30 - Profile: {name: string}; 31 - }; 32 - const Stack = createNativeStackNavigator(); 33 - 34 - const Section: React.FC<{ 35 - title: string; 36 - }> = ({children, title}) => { 37 - return ( 38 - <View style={styles.sectionContainer}> 39 - <Text style={styles.sectionTitle}>{title}</Text> 40 - <Text style={styles.sectionDescription}>{children}</Text> 41 - </View> 42 - ); 43 - }; 44 - 45 - const HomeScreen = ({ 46 - navigation, 47 - }: NativeStackScreenProps<RootStackParamList, 'Home'>) => { 48 - const isDarkMode = useColorScheme() === 'dark'; 49 - 50 - return ( 51 - <SafeAreaView> 52 - <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} /> 53 - <ScrollView contentInsetAdjustmentBehavior="automatic"> 54 - <View> 55 - <Section title="Step One"> 56 - Edit <Text style={styles.highlight}>App.tsx</Text> to change this 57 - screen and then come back to see your edits. 58 - <Button 59 - title="Go to Jane's profile" 60 - onPress={() => navigation.navigate('Profile', {name: 'Jane'})} 61 - /> 62 - </Section> 63 - <Section title="Learn More"> 64 - Read the docs to discover what to do next: 65 - </Section> 66 - </View> 67 - </ScrollView> 68 - </SafeAreaView> 69 - ); 70 - }; 71 - 72 - const ProfileScreen = ({ 73 - route, 74 - }: NativeStackScreenProps<RootStackParamList, 'Profile'>) => { 75 - return <Text>This is {route.params.name}'s profile</Text>; 76 - }; 77 - 78 - const App = () => { 79 - return ( 80 - <NavigationContainer> 81 - <Stack.Navigator> 82 - <Stack.Screen 83 - name="Home" 84 - component={HomeScreen} 85 - options={{title: 'Welcome'}} 86 - /> 87 - <Stack.Screen name="Profile" component={ProfileScreen} /> 88 - </Stack.Navigator> 89 - </NavigationContainer> 90 - ); 91 - }; 92 - 93 - const styles = StyleSheet.create({ 94 - sectionContainer: { 95 - marginTop: 32, 96 - paddingHorizontal: 24, 97 - }, 98 - sectionTitle: { 99 - fontSize: 24, 100 - fontWeight: '600', 101 - }, 102 - sectionDescription: { 103 - marginTop: 8, 104 - fontSize: 18, 105 - fontWeight: '400', 106 - }, 107 - highlight: { 108 - fontWeight: '700', 109 - }, 110 - }); 111 - 112 - export default App;
+80
src/App.web.tsx
··· 1 + import React, {useState, useEffect} from 'react' 2 + import { 3 + SafeAreaView, 4 + ScrollView, 5 + StatusBar, 6 + Text, 7 + Button, 8 + useColorScheme, 9 + View, 10 + } from 'react-native' 11 + import {NavigationContainer} from '@react-navigation/native' 12 + import { 13 + createNativeStackNavigator, 14 + NativeStackScreenProps, 15 + } from '@react-navigation/native-stack' 16 + import {RootStore, setupState, RootStoreProvider} from './state' 17 + 18 + type RootStackParamList = { 19 + Home: undefined 20 + Profile: {name: string} 21 + } 22 + const Stack = createNativeStackNavigator() 23 + 24 + const HomeScreen = ({ 25 + navigation, 26 + }: NativeStackScreenProps<RootStackParamList, 'Home'>) => { 27 + const isDarkMode = useColorScheme() === 'dark' 28 + 29 + return ( 30 + <SafeAreaView> 31 + <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} /> 32 + <ScrollView contentInsetAdjustmentBehavior="automatic"> 33 + <View> 34 + <Text>Web</Text> 35 + <Button 36 + title="Go to Jane's profile" 37 + onPress={() => navigation.navigate('Profile', {name: 'Jane'})} 38 + /> 39 + </View> 40 + </ScrollView> 41 + </SafeAreaView> 42 + ) 43 + } 44 + 45 + const ProfileScreen = ({ 46 + route, 47 + }: NativeStackScreenProps<RootStackParamList, 'Profile'>) => { 48 + return <Text>This is {route.params.name}'s profile</Text> 49 + } 50 + 51 + function App() { 52 + const [rootStore, setRootStore] = useState<RootStore | undefined>(undefined) 53 + 54 + // init 55 + useEffect(() => { 56 + setupState().then(setRootStore) 57 + }, []) 58 + 59 + // show nothing prior to init 60 + if (!rootStore) { 61 + return null 62 + } 63 + 64 + return ( 65 + <RootStoreProvider value={rootStore}> 66 + <NavigationContainer> 67 + <Stack.Navigator> 68 + <Stack.Screen 69 + name="Home" 70 + component={HomeScreen} 71 + options={{title: 'Welcome'}} 72 + /> 73 + <Stack.Screen name="Profile" component={ProfileScreen} /> 74 + </Stack.Navigator> 75 + </NavigationContainer> 76 + </RootStoreProvider> 77 + ) 78 + } 79 + 80 + export default App
+4 -4
src/index.js
··· 2 2 * @format 3 3 */ 4 4 5 - import {AppRegistry} from 'react-native'; 6 - import App from './App'; 5 + import {AppRegistry} from 'react-native' 6 + import App from './App' 7 7 8 - AppRegistry.registerComponent('App', () => App); 8 + AppRegistry.registerComponent('App', () => App) 9 9 10 10 AppRegistry.runApplication('App', { 11 11 rootTag: document.getElementById('root'), 12 - }); 12 + })
+27
src/state/env.ts
··· 1 + /** 2 + * The environment is a place where services and shared dependencies between 3 + * models live. They are made available to every model via dependency injection. 4 + */ 5 + 6 + import {getEnv, IStateTreeNode} from 'mobx-state-tree' 7 + 8 + export class Environment { 9 + constructor() {} 10 + 11 + async setup() {} 12 + } 13 + 14 + /** 15 + * Extension to the MST models that adds the environment property. 16 + * Usage: 17 + * 18 + * .extend(withEnvironment) 19 + * 20 + */ 21 + export const withEnvironment = (self: IStateTreeNode) => ({ 22 + views: { 23 + get environment() { 24 + return getEnv<Environment>(self) 25 + }, 26 + }, 27 + })
+30
src/state/index.ts
··· 1 + import {onSnapshot} from 'mobx-state-tree' 2 + import {RootStoreModel, RootStore} from './models/root-store' 3 + import {Environment} from './env' 4 + import * as storage from './storage' 5 + 6 + const ROOT_STATE_STORAGE_KEY = 'root' 7 + 8 + export async function setupState() { 9 + let rootStore: RootStore 10 + let data: any 11 + 12 + const env = new Environment() 13 + try { 14 + data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {} 15 + rootStore = RootStoreModel.create(data, env) 16 + } catch (e) { 17 + console.error('Failed to load state from storage', e) 18 + rootStore = RootStoreModel.create({}, env) 19 + } 20 + 21 + // track changes & save to storage 22 + onSnapshot(rootStore, snapshot => 23 + storage.save(ROOT_STATE_STORAGE_KEY, snapshot), 24 + ) 25 + 26 + return rootStore 27 + } 28 + 29 + export {useStores, RootStoreModel, RootStoreProvider} from './models/root-store' 30 + export type {RootStore} from './models/root-store'
+16
src/state/models/root-store.ts
··· 1 + /** 2 + * The root store is the base of all modeled state. 3 + */ 4 + 5 + import {Instance, SnapshotOut, types} from 'mobx-state-tree' 6 + import {createContext, useContext} from 'react' 7 + 8 + export const RootStoreModel = types.model('RootStore').props({}) 9 + 10 + export interface RootStore extends Instance<typeof RootStoreModel> {} 11 + export interface RootStoreSnapshot extends SnapshotOut<typeof RootStoreModel> {} 12 + 13 + // react context & hook utilities 14 + const RootStoreContext = createContext<RootStore>({} as RootStore) 15 + export const RootStoreProvider = RootStoreContext.Provider 16 + export const useStores = () => useContext(RootStoreContext)
+52
src/state/storage.ts
··· 1 + import AsyncStorage from '@react-native-async-storage/async-storage' 2 + 3 + export async function loadString(key: string): Promise<string | null> { 4 + try { 5 + return await AsyncStorage.getItem(key) 6 + } catch { 7 + // not sure why this would fail... even reading the RN docs I'm unclear 8 + return null 9 + } 10 + } 11 + 12 + export async function saveString(key: string, value: string): Promise<boolean> { 13 + try { 14 + await AsyncStorage.setItem(key, value) 15 + return true 16 + } catch { 17 + return false 18 + } 19 + } 20 + 21 + export async function load(key: string): Promise<any | null> { 22 + try { 23 + const str = await AsyncStorage.getItem(key) 24 + if (typeof str !== 'string') { 25 + return null 26 + } 27 + return JSON.parse(str) 28 + } catch { 29 + return null 30 + } 31 + } 32 + 33 + export async function save(key: string, value: any): Promise<boolean> { 34 + try { 35 + await AsyncStorage.setItem(key, JSON.stringify(value)) 36 + return true 37 + } catch { 38 + return false 39 + } 40 + } 41 + 42 + export async function remove(key: string): Promise<void> { 43 + try { 44 + await AsyncStorage.removeItem(key) 45 + } catch {} 46 + } 47 + 48 + export async function clear(): Promise<void> { 49 + try { 50 + await AsyncStorage.clear() 51 + } catch {} 52 + }
+29
yarn.lock
··· 1797 1797 schema-utils "^3.0.0" 1798 1798 source-map "^0.7.3" 1799 1799 1800 + "@react-native-async-storage/async-storage@^1.17.6": 1801 + version "1.17.6" 1802 + resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.17.6.tgz#ddb3520d051f71698c8a0e79e8959a7bf6d9f43b" 1803 + integrity sha512-XXnoheQI3vQTQmjphdXNLTmtiKZeRqvI8kPQ25X5Eae7nZjdYEEGN+0z8N2qyelbUIQwKgmW0aagJk56q7DyNg== 1804 + dependencies: 1805 + merge-options "^3.0.4" 1806 + 1800 1807 "@react-native-community/cli-debugger-ui@^7.0.3": 1801 1808 version "7.0.3" 1802 1809 resolved "https://registry.yarnpkg.com/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-7.0.3.tgz#3eeeacc5a43513cbcae56e5e965d77726361bcb4" ··· 6634 6641 resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" 6635 6642 integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== 6636 6643 6644 + is-plain-obj@^2.1.0: 6645 + version "2.1.0" 6646 + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" 6647 + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== 6648 + 6637 6649 is-plain-obj@^3.0.0: 6638 6650 version "3.0.0" 6639 6651 resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" ··· 8128 8140 resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 8129 8141 integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== 8130 8142 8143 + merge-options@^3.0.4: 8144 + version "3.0.4" 8145 + resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7" 8146 + integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ== 8147 + dependencies: 8148 + is-plain-obj "^2.1.0" 8149 + 8131 8150 merge-stream@^2.0.0: 8132 8151 version "2.0.0" 8133 8152 resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" ··· 8505 8524 integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== 8506 8525 dependencies: 8507 8526 minimist "^1.2.6" 8527 + 8528 + mobx-state-tree@^5.1.5: 8529 + version "5.1.5" 8530 + resolved "https://registry.yarnpkg.com/mobx-state-tree/-/mobx-state-tree-5.1.5.tgz#7344d61072705747abb98d23ad21302e38200105" 8531 + integrity sha512-jugIic0PYWW+nzzYfp4RUy9dec002Z778OC6KzoOyBHnqxupK9iPCsUJYkHjmNRHjZ8E4Z7qQpsKV3At/ntGVw== 8532 + 8533 + mobx@^6.6.0: 8534 + version "6.6.0" 8535 + resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.6.0.tgz#617ca1f3b745a781fa89c5eb94a773e3cbeff8ae" 8536 + integrity sha512-MNTKevLH/6DShLZcmSL351+JgiJPO56A4GUpoiDQ3/yZ0mAtclNLdHK9q4BcQhibx8/JSDupfTpbX2NZPemlRg== 8508 8537 8509 8538 ms@2.0.0: 8510 8539 version "2.0.0"