From e28093ec35ac67aba1c7c83cba4d2e9493cbcf70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferreiro?= Date: Mon, 12 Dec 2022 16:30:52 +0000 Subject: [PATCH] using register --- package.json | 1 + src/view/com/composer/Autocomplete.tsx | 5 +- src/view/com/composer/ComposePost.tsx | 482 ++++++++-------- src/view/com/composer/PhotoCarouselPicker.tsx | 161 +++--- src/view/com/composer/Prompt.tsx | 5 +- src/view/com/composer/SelectedPhoto.tsx | 89 +-- src/view/com/discover/SuggestedFollows.tsx | 195 +++---- src/view/com/notifications/Feed.tsx | 121 ++-- src/view/com/notifications/FeedItem.tsx | 342 ++++++------ src/view/com/notifications/InviteAccepter.tsx | 9 +- src/view/com/onboard/FeatureExplainer.tsx | 5 +- src/view/com/onboard/Follows.tsx | 49 +- src/view/com/post-thread/PostRepostedBy.tsx | 117 ++-- src/view/com/post-thread/PostThread.tsx | 145 ++--- src/view/com/post-thread/PostThreadItem.tsx | 497 +++++++++-------- src/view/com/post-thread/PostVotedBy.tsx | 123 +++-- src/view/com/post/Post.tsx | 321 +++++------ src/view/com/post/PostText.tsx | 81 ++- src/view/com/posts/Feed.tsx | 145 ++--- src/view/com/posts/FeedItem.tsx | 369 +++++++------ src/view/com/profile/ProfileCard.tsx | 5 +- src/view/com/profile/ProfileFollowers.tsx | 115 ++-- src/view/com/profile/ProfileFollows.tsx | 115 ++-- src/view/com/profile/ProfileHeader.tsx | 520 +++++++++--------- src/view/com/profile/ProfileMembers.tsx | 125 ++--- src/view/com/util/DropdownBtn.tsx | 91 +-- src/view/com/util/EmptyState.tsx | 44 +- src/view/com/util/ErrorMessage.tsx | 98 ++-- src/view/com/util/ErrorScreen.tsx | 75 +-- src/view/com/util/Link.tsx | 59 +- src/view/com/util/LoadingPlaceholder.tsx | 64 +-- src/view/com/util/Picker.tsx | 76 +-- src/view/com/util/PostCtrls.tsx | 5 +- src/view/com/util/PostEmbeds.tsx | 92 ++-- src/view/com/util/PostMeta.tsx | 5 +- src/view/com/util/RichText.tsx | 116 ++-- src/view/com/util/Selector.tsx | 172 +++--- src/view/com/util/UserAvatar.tsx | 187 +++---- src/view/com/util/UserBanner.tsx | 169 +++--- src/view/com/util/UserInfoText.tsx | 141 ++--- src/view/com/util/ViewHeader.tsx | 214 +++---- src/view/com/util/ViewSelector.tsx | 157 +++--- src/view/screens/Contacts.tsx | 5 +- src/view/screens/Home.tsx | 181 +++--- src/view/screens/Login.tsx | 7 + src/view/screens/NotFound.tsx | 5 +- src/view/screens/Notifications.tsx | 5 +- src/view/screens/Onboard.tsx | 47 +- src/view/screens/PostDownvotedBy.tsx | 35 +- src/view/screens/PostRepostedBy.tsx | 35 +- src/view/screens/PostThread.tsx | 91 +-- src/view/screens/PostUpvotedBy.tsx | 35 +- src/view/screens/Profile.tsx | 423 +++++++------- src/view/screens/ProfileFollowers.tsx | 33 +- src/view/screens/ProfileFollows.tsx | 33 +- src/view/screens/ProfileMembers.tsx | 33 +- src/view/screens/Search.tsx | 5 +- src/view/screens/Settings.tsx | 86 +-- src/view/shell/mobile/Composer.tsx | 101 ++-- yarn.lock | 5 + 60 files changed, 3620 insertions(+), 3452 deletions(-) diff --git a/package.json b/package.json index 5b67d044a5..47acf1248f 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "react-dom": "17.0.2", "react-native": "0.68.2", "react-native-appstate-hook": "^1.0.6", + "react-native-bundle-splitter": "^2.2.3", "react-native-gesture-handler": "^2.5.0", "react-native-image-crop-picker": "^0.38.1", "react-native-inappbrowser-reborn": "^3.6.3", diff --git a/src/view/com/composer/Autocomplete.tsx b/src/view/com/composer/Autocomplete.tsx index 3d71db635d..37f4cc615e 100644 --- a/src/view/com/composer/Autocomplete.tsx +++ b/src/view/com/composer/Autocomplete.tsx @@ -8,13 +8,14 @@ import { } from 'react-native' import {useAnimatedValue} from '../../lib/useAnimatedValue' import {colors} from '../../lib/styles' +import {register} from 'react-native-bundle-splitter' interface AutocompleteItem { handle: string displayName?: string } -export function Autocomplete({ +export const Autocomplete = register(function Autocomplete({ active, items, onSelect, @@ -52,7 +53,7 @@ export function Autocomplete({ ))} ) -} +}) const styles = StyleSheet.create({ outer: { diff --git a/src/view/com/composer/ComposePost.tsx b/src/view/com/composer/ComposePost.tsx index fb21b9f9dc..4d313bef3d 100644 --- a/src/view/com/composer/ComposePost.tsx +++ b/src/view/com/composer/ComposePost.tsx @@ -30,264 +30,268 @@ import {UserLocalPhotosModel} from '../../../state/models/user-local-photos' import {PhotoCarouselPicker} from './PhotoCarouselPicker' import {SelectedPhoto} from './SelectedPhoto' import {IMAGES_ENABLED} from '../../../build-flags' +import {register} from 'react-native-bundle-splitter' const MAX_TEXT_LENGTH = 256 const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH -export const ComposePost = observer(function ComposePost({ - replyTo, - onPost, - onClose, -}: { - replyTo?: ComposerOpts['replyTo'] - onPost?: ComposerOpts['onPost'] - onClose: () => void -}) { - const store = useStores() - const textInput = useRef(null) - const [isProcessing, setIsProcessing] = useState(false) - const [error, setError] = useState('') - const [text, setText] = useState('') - const [selectedPhotos, setSelectedPhotos] = useState([]) +export const ComposePost = register( + observer(function ComposePost({ + replyTo, + onPost, + onClose, + }: { + replyTo?: ComposerOpts['replyTo'] + onPost?: ComposerOpts['onPost'] + onClose: () => void + }) { + const store = useStores() + const textInput = useRef(null) + const [isProcessing, setIsProcessing] = useState(false) + const [error, setError] = useState('') + const [text, setText] = useState('') + const [selectedPhotos, setSelectedPhotos] = useState([]) - const autocompleteView = useMemo( - () => new UserAutocompleteViewModel(store), - [store], - ) - const localPhotos = useMemo( - () => new UserLocalPhotosModel(store), - [store], - ) + const autocompleteView = useMemo( + () => new UserAutocompleteViewModel(store), + [store], + ) + const localPhotos = useMemo( + () => new UserLocalPhotosModel(store), + [store], + ) - useEffect(() => { - autocompleteView.setup() - localPhotos.setup() - }, [autocompleteView, localPhotos]) + useEffect(() => { + autocompleteView.setup() + localPhotos.setup() + }, [autocompleteView, localPhotos]) - useEffect(() => { - // HACK - // wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view - // -prf - let to: NodeJS.Timeout | undefined - if (textInput.current) { - to = setTimeout(() => { - textInput.current?.focus() - }, 250) - } - return () => { - if (to) { - clearTimeout(to) + useEffect(() => { + // HACK + // wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view + // -prf + let to: NodeJS.Timeout | undefined + if (textInput.current) { + to = setTimeout(() => { + textInput.current?.focus() + }, 250) + } + return () => { + if (to) { + clearTimeout(to) + } + } + }, []) + + const onChangeText = (newText: string) => { + setText(newText) + + const prefix = extractTextAutocompletePrefix(newText) + if (typeof prefix === 'string') { + autocompleteView.setActive(true) + autocompleteView.setPrefix(prefix) + } else { + autocompleteView.setActive(false) } } - }, []) - - const onChangeText = (newText: string) => { - setText(newText) - - const prefix = extractTextAutocompletePrefix(newText) - if (typeof prefix === 'string') { - autocompleteView.setActive(true) - autocompleteView.setPrefix(prefix) - } else { + const onPressCancel = () => { + onClose() + } + const onPressPublish = async () => { + if (isProcessing) { + return + } + if (text.length > MAX_TEXT_LENGTH) { + return + } + setError('') + if (text.trim().length === 0) { + setError('Did you want to say anything?') + return false + } + setIsProcessing(true) + try { + const replyRef = replyTo + ? {uri: replyTo.uri, cid: replyTo.cid} + : undefined + await apilib.post(store, text, replyRef, autocompleteView.knownHandles) + } catch (e: any) { + console.error(`Failed to create post: ${e.toString()}`) + setError( + 'Post failed to upload. Please check your Internet connection and try again.', + ) + setIsProcessing(false) + return + } + onPost?.() + onClose() + Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`) + } + const onSelectAutocompleteItem = (item: string) => { + setText(replaceTextAutocompletePrefix(text, item)) autocompleteView.setActive(false) } - } - const onPressCancel = () => { - onClose() - } - const onPressPublish = async () => { - if (isProcessing) { - return - } - if (text.length > MAX_TEXT_LENGTH) { - return - } - setError('') - if (text.trim().length === 0) { - setError('Did you want to say anything?') - return false - } - setIsProcessing(true) - try { - const replyRef = replyTo - ? {uri: replyTo.uri, cid: replyTo.cid} - : undefined - await apilib.post(store, text, replyRef, autocompleteView.knownHandles) - } catch (e: any) { - console.error(`Failed to create post: ${e.toString()}`) - setError( - 'Post failed to upload. Please check your Internet connection and try again.', - ) - setIsProcessing(false) - return - } - onPost?.() - onClose() - Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`) - } - const onSelectAutocompleteItem = (item: string) => { - setText(replaceTextAutocompletePrefix(text, item)) - autocompleteView.setActive(false) - } - const canPost = text.length <= MAX_TEXT_LENGTH - const progressColor = text.length > DANGER_TEXT_LENGTH ? '#e60000' : undefined + const canPost = text.length <= MAX_TEXT_LENGTH + const progressColor = + text.length > DANGER_TEXT_LENGTH ? '#e60000' : undefined - const selectTextInputLayout = - selectedPhotos.length !== 0 - ? styles.textInputLayoutWithPhoto - : styles.textInputLayoutWithoutPhoto - const selectTextInputPlaceholder = replyTo - ? 'Write your reply' - : selectedPhotos.length !== 0 - ? 'Write a comment' - : "What's up?" + const selectTextInputLayout = + selectedPhotos.length !== 0 + ? styles.textInputLayoutWithPhoto + : styles.textInputLayoutWithoutPhoto + const selectTextInputPlaceholder = replyTo + ? 'Write your reply' + : selectedPhotos.length !== 0 + ? 'Write a comment' + : "What's up?" - const textDecorated = useMemo(() => { - let i = 0 - return detectLinkables(text).map(v => { - if (typeof v === 'string') { - return v - } else { - return ( - - {v.link} - - ) - } - }) - }, [text]) + const textDecorated = useMemo(() => { + let i = 0 + return detectLinkables(text).map(v => { + if (typeof v === 'string') { + return v + } else { + return ( + + {v.link} + + ) + } + }) + }, [text]) - return ( - - - - - Cancel - - - {isProcessing ? ( - - - - ) : canPost ? ( - - - - {replyTo ? 'Reply' : 'Post'} - - + return ( + + + + + Cancel - ) : ( - - Post - - )} - - {error !== '' && ( - - - - - {error} - - )} - {replyTo ? ( - - - - - - {replyTo.text} - - - - ) : undefined} - - - onChangeText(text)} - placeholder={selectTextInputPlaceholder} - style={styles.textInput}> - {textDecorated} - - - - {IMAGES_ENABLED && - localPhotos.photos != null && - text === '' && - selectedPhotos.length === 0 && ( - - )} - - - - {MAX_TEXT_LENGTH - text.length} - - - {text.length > DANGER_TEXT_LENGTH ? ( - + + {isProcessing ? ( + + + + ) : canPost ? ( + + + + {replyTo ? 'Reply' : 'Post'} + + + ) : ( - + + Post + )} - - - - - ) -}) + {error !== '' && ( + + + + + {error} + + )} + {replyTo ? ( + + + + + + {replyTo.text} + + + + ) : undefined} + + + onChangeText(text)} + placeholder={selectTextInputPlaceholder} + style={styles.textInput}> + {textDecorated} + + + + {IMAGES_ENABLED && + localPhotos.photos != null && + text === '' && + selectedPhotos.length === 0 && ( + + )} + + + + {MAX_TEXT_LENGTH - text.length} + + + {text.length > DANGER_TEXT_LENGTH ? ( + + ) : ( + + )} + + + + + + ) + }), +) const atPrefixRegex = /@([a-z0-9\.]*)$/i function extractTextAutocompletePrefix(text: string) { diff --git a/src/view/com/composer/PhotoCarouselPicker.tsx b/src/view/com/composer/PhotoCarouselPicker.tsx index f4af4c61e3..b681d9b5a1 100644 --- a/src/view/com/composer/PhotoCarouselPicker.tsx +++ b/src/view/com/composer/PhotoCarouselPicker.tsx @@ -7,96 +7,103 @@ import { openCamera, openCropper, } from 'react-native-image-crop-picker' +import {register} from 'react-native-bundle-splitter' -export const PhotoCarouselPicker = ({ - selectedPhotos, - setSelectedPhotos, - localPhotos, -}: { - selectedPhotos: string[] - setSelectedPhotos: React.Dispatch> - localPhotos: any -}) => { - const handleOpenCamera = useCallback(() => { - openCamera({ - mediaType: 'photo', - cropping: true, - width: 1000, - height: 1000, - }).then( - item => { - setSelectedPhotos([item.path, ...selectedPhotos]) - }, - _err => { - // ignore - }, - ) - }, [selectedPhotos, setSelectedPhotos]) - - const handleSelectPhoto = useCallback( - async (uri: string) => { - const img = await openCropper({ +export const PhotoCarouselPicker = register( + ({ + selectedPhotos, + setSelectedPhotos, + localPhotos, + }: { + selectedPhotos: string[] + setSelectedPhotos: React.Dispatch> + localPhotos: any + }) => { + const handleOpenCamera = useCallback(() => { + openCamera({ mediaType: 'photo', - path: uri, + cropping: true, width: 1000, height: 1000, - }) - setSelectedPhotos([img.path, ...selectedPhotos]) - }, - [selectedPhotos, setSelectedPhotos], - ) + }).then( + item => { + setSelectedPhotos([item.path, ...selectedPhotos]) + }, + _err => { + // ignore + }, + ) + }, [selectedPhotos, setSelectedPhotos]) - const handleOpenGallery = useCallback(() => { - openPicker({ - multiple: true, - maxFiles: 4, - mediaType: 'photo', - }).then(async items => { - const result = [] - - for await (const image of items) { + const handleSelectPhoto = useCallback( + async (uri: string) => { const img = await openCropper({ mediaType: 'photo', - path: image.path, + path: uri, width: 1000, height: 1000, }) - result.push(img.path) - } - setSelectedPhotos([...result, ...selectedPhotos]) - }) - }, [selectedPhotos, setSelectedPhotos]) + setSelectedPhotos([img.path, ...selectedPhotos]) + }, + [selectedPhotos, setSelectedPhotos], + ) - return ( - - - - - {localPhotos.photos.map((item: any, index: number) => ( + const handleOpenGallery = useCallback(() => { + openPicker({ + multiple: true, + maxFiles: 4, + mediaType: 'photo', + }).then(async items => { + const result = [] + + for await (const image of items) { + const img = await openCropper({ + mediaType: 'photo', + path: image.path, + width: 1000, + height: 1000, + }) + result.push(img.path) + } + setSelectedPhotos([...result, ...selectedPhotos]) + }) + }, [selectedPhotos, setSelectedPhotos]) + + return ( + handleSelectPhoto(item.node.image.uri)}> - + style={[styles.galleryButton, styles.photo]} + onPress={handleOpenCamera}> + - ))} - - - - - ) -} + {localPhotos.photos.map((item: any, index: number) => ( + handleSelectPhoto(item.node.image.uri)}> + + + ))} + + + + + ) + }, +) const styles = StyleSheet.create({ photosContainer: { diff --git a/src/view/com/composer/Prompt.tsx b/src/view/com/composer/Prompt.tsx index 55730dc70a..b974bfe0a9 100644 --- a/src/view/com/composer/Prompt.tsx +++ b/src/view/com/composer/Prompt.tsx @@ -3,8 +3,9 @@ import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' import {colors} from '../../lib/styles' import {useStores} from '../../../state' import {UserAvatar} from '../util/UserAvatar' +import {register} from 'react-native-bundle-splitter' -export function ComposePrompt({ +export const ComposePrompt = register(function ComposePrompt({ noAvi = false, text = "What's up?", btn = 'Post', @@ -41,7 +42,7 @@ export function ComposePrompt({ ) -} +}) const styles = StyleSheet.create({ container: { diff --git a/src/view/com/composer/SelectedPhoto.tsx b/src/view/com/composer/SelectedPhoto.tsx index 88209b3df6..ea3992b487 100644 --- a/src/view/com/composer/SelectedPhoto.tsx +++ b/src/view/com/composer/SelectedPhoto.tsx @@ -2,53 +2,56 @@ import React, {useCallback} from 'react' import {Image, StyleSheet, TouchableOpacity, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {colors} from '../../lib/styles' +import {register} from 'react-native-bundle-splitter' -export const SelectedPhoto = ({ - selectedPhotos, - setSelectedPhotos, -}: { - selectedPhotos: string[] - setSelectedPhotos: React.Dispatch> -}) => { - const imageStyle = - selectedPhotos.length === 1 - ? styles.image250 - : selectedPhotos.length === 2 - ? styles.image175 - : styles.image85 +export const SelectedPhoto = register( + ({ + selectedPhotos, + setSelectedPhotos, + }: { + selectedPhotos: string[] + setSelectedPhotos: React.Dispatch> + }) => { + const imageStyle = + selectedPhotos.length === 1 + ? styles.image250 + : selectedPhotos.length === 2 + ? styles.image175 + : styles.image85 - const handleRemovePhoto = useCallback( - item => { - setSelectedPhotos( - selectedPhotos.filter(filterItem => filterItem !== item), - ) - }, - [selectedPhotos, setSelectedPhotos], - ) + const handleRemovePhoto = useCallback( + item => { + setSelectedPhotos( + selectedPhotos.filter(filterItem => filterItem !== item), + ) + }, + [selectedPhotos, setSelectedPhotos], + ) - return selectedPhotos.length !== 0 ? ( - - {selectedPhotos.length !== 0 && - selectedPhotos.map((item, index) => ( - - handleRemovePhoto(item)} - style={styles.removePhotoButton}> - - + return selectedPhotos.length !== 0 ? ( + + {selectedPhotos.length !== 0 && + selectedPhotos.map((item, index) => ( + + handleRemovePhoto(item)} + style={styles.removePhotoButton}> + + - - - ))} - - ) : null -} + + + ))} + + ) : null + }, +) const styles = StyleSheet.create({ imageContainer: { diff --git a/src/view/com/discover/SuggestedFollows.tsx b/src/view/com/discover/SuggestedFollows.tsx index 315249159e..ea9791374d 100644 --- a/src/view/com/discover/SuggestedFollows.tsx +++ b/src/view/com/discover/SuggestedFollows.tsx @@ -22,112 +22,119 @@ import { SuggestedActor, } from '../../../state/models/suggested-actors-view' import {s, colors, gradients} from '../../lib/styles' +import {register} from 'react-native-bundle-splitter' -export const SuggestedFollows = observer( - ({ - onNoSuggestions, - asLinks, - }: { - onNoSuggestions?: () => void - asLinks?: boolean - }) => { - const store = useStores() - const [follows, setFollows] = useState>({}) +export const SuggestedFollows = register( + observer( + ({ + onNoSuggestions, + asLinks, + }: { + onNoSuggestions?: () => void + asLinks?: boolean + }) => { + const store = useStores() + const [follows, setFollows] = useState>({}) - const view = useMemo( - () => new SuggestedActorsViewModel(store), - [], - ) + const view = useMemo( + () => new SuggestedActorsViewModel(store), + [], + ) - useEffect(() => { - console.log('Fetching suggested actors') - view - .setup() - .catch((err: any) => console.error('Failed to fetch suggestions', err)) - }, [view]) + 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) { - onNoSuggestions?.() + useEffect(() => { + if (!view.isLoading && !view.hasError && !view.hasContent) { + onNoSuggestions?.() + } + }, [view, view.isLoading, view.hasError, view.hasContent]) + + const onPressTryAgain = () => + view + .setup() + .catch((err: any) => + console.error('Failed to fetch suggestions', err), + ) + + const onPressFollow = async (item: SuggestedActor) => { + try { + const res = await apilib.follow(store, item.did, item.declaration.cid) + setFollows({[item.did]: res.uri, ...follows}) + } catch (e) { + console.log(e) + Toast.show('An issue occurred, please try again.') + } } - }, [view, view.isLoading, view.hasError, view.hasContent]) - - const onPressTryAgain = () => - view - .setup() - .catch((err: any) => console.error('Failed to fetch suggestions', err)) - - const onPressFollow = async (item: SuggestedActor) => { - try { - const res = await apilib.follow(store, item.did, item.declaration.cid) - setFollows({[item.did]: res.uri, ...follows}) - } catch (e) { - console.log(e) - Toast.show('An issue occurred, please try again.') + const onPressUnfollow = async (item: SuggestedActor) => { + try { + await apilib.unfollow(store, follows[item.did]) + setFollows(_omit(follows, [item.did])) + } catch (e) { + console.log(e) + Toast.show('An issue occurred, please try again.') + } } - } - const onPressUnfollow = async (item: SuggestedActor) => { - try { - await apilib.unfollow(store, follows[item.did]) - setFollows(_omit(follows, [item.did])) - } catch (e) { - console.log(e) - Toast.show('An issue occurred, please try again.') - } - } - const renderItem = ({item}: {item: SuggestedActor}) => { - if (asLinks) { + const renderItem = ({item}: {item: SuggestedActor}) => { + if (asLinks) { + return ( + + + + ) + } return ( - - - + ) } return ( - - ) - } - return ( - - {view.isLoading ? ( - - - - ) : view.hasError ? ( - - ) : view.isEmpty ? ( - - ) : ( - - item._reactKey} - renderItem={renderItem} - style={s.flex1} + + {view.isLoading ? ( + + + + ) : view.hasError ? ( + - - )} - - ) - }, + ) : view.isEmpty ? ( + + ) : ( + + item._reactKey} + renderItem={renderItem} + style={s.flex1} + /> + + )} + + ) + }, + ), ) const User = ({ diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx index a6af0f88ac..5415564cd4 100644 --- a/src/view/com/notifications/Feed.tsx +++ b/src/view/com/notifications/Feed.tsx @@ -6,67 +6,70 @@ import {FeedItem} from './FeedItem' import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {ErrorMessage} from '../util/ErrorMessage' import {EmptyState} from '../util/EmptyState' +import {register} from 'react-native-bundle-splitter' const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} -export const Feed = observer(function Feed({ - view, - onPressTryAgain, -}: { - view: NotificationsViewModel - onPressTryAgain?: () => void -}) { - // TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf - // VirtualizedList: You have a large list that is slow to update - make sure your - // renderItem function renders components that follow React performance best practices - // like PureComponent, shouldComponentUpdate, etc - const renderItem = ({item}: {item: any}) => { - if (item === EMPTY_FEED_ITEM) { - return ( - - ) +export const Feed = register( + observer(function Feed({ + view, + onPressTryAgain, + }: { + view: NotificationsViewModel + onPressTryAgain?: () => void + }) { + // TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf + // VirtualizedList: You have a large list that is slow to update - make sure your + // renderItem function renders components that follow React performance best practices + // like PureComponent, shouldComponentUpdate, etc + const renderItem = ({item}: {item: any}) => { + if (item === EMPTY_FEED_ITEM) { + return ( + + ) + } + return } - return - } - const onRefresh = () => { - view.refresh().catch(err => console.error('Failed to refresh', err)) - } - const onEndReached = () => { - view.loadMore().catch(err => console.error('Failed to load more', err)) - } - let data - if (view.hasLoaded) { - if (view.isEmpty) { - data = [EMPTY_FEED_ITEM] - } else { - data = view.notifications + const onRefresh = () => { + view.refresh().catch(err => console.error('Failed to refresh', err)) } - } - return ( - - {view.isLoading && !data && } - {view.hasError && ( - - )} - {data && ( - item._reactKey} - renderItem={renderItem} - refreshing={view.isRefreshing} - onRefresh={onRefresh} - onEndReached={onEndReached} - /> - )} - - ) -}) + const onEndReached = () => { + view.loadMore().catch(err => console.error('Failed to load more', err)) + } + let data + if (view.hasLoaded) { + if (view.isEmpty) { + data = [EMPTY_FEED_ITEM] + } else { + data = view.notifications + } + } + return ( + + {view.isLoading && !data && } + {view.hasError && ( + + )} + {data && ( + item._reactKey} + renderItem={renderItem} + refreshing={view.isRefreshing} + onRefresh={onRefresh} + onEndReached={onEndReached} + /> + )} + + ) + }), +) diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index c61159c726..6b62d99cd3 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -13,185 +13,185 @@ import {ErrorMessage} from '../util/ErrorMessage' import {Post} from '../post/Post' import {Link} from '../util/Link' import {InviteAccepter} from './InviteAccepter' +import {register} from 'react-native-bundle-splitter' const MAX_AUTHORS = 8 -export const FeedItem = observer(function FeedItem({ - item, -}: { - item: NotificationsViewItemModel -}) { - const itemHref = useMemo(() => { - if (item.isUpvote || item.isRepost || item.isTrend) { - const urip = new AtUri(item.subjectUri) - return `/profile/${urip.host}/post/${urip.rkey}` - } else if (item.isFollow || item.isAssertion) { - return `/profile/${item.author.handle}` - } else if (item.isReply) { - const urip = new AtUri(item.uri) - return `/profile/${urip.host}/post/${urip.rkey}` - } - return '' - }, [item]) - const itemTitle = useMemo(() => { - if (item.isUpvote || item.isRepost) { - return 'Post' - } else if (item.isFollow || item.isAssertion) { - return item.author.handle - } else if (item.isReply) { - return 'Post' - } - }, [item]) +export const FeedItem = register( + observer(function FeedItem({item}: {item: NotificationsViewItemModel}) { + const itemHref = useMemo(() => { + if (item.isUpvote || item.isRepost || item.isTrend) { + const urip = new AtUri(item.subjectUri) + return `/profile/${urip.host}/post/${urip.rkey}` + } else if (item.isFollow || item.isAssertion) { + return `/profile/${item.author.handle}` + } else if (item.isReply) { + const urip = new AtUri(item.uri) + return `/profile/${urip.host}/post/${urip.rkey}` + } + return '' + }, [item]) + const itemTitle = useMemo(() => { + if (item.isUpvote || item.isRepost) { + return 'Post' + } else if (item.isFollow || item.isAssertion) { + return item.author.handle + } else if (item.isReply) { + return 'Post' + } + }, [item]) - if (item.additionalPost?.notFound) { - // don't render anything if the target post was deleted or unfindable - return - } + if (item.additionalPost?.notFound) { + // don't render anything if the target post was deleted or unfindable + return + } + + if (item.isReply || item.isMention) { + return ( + + + + ) + } + + let action = '' + let icon: Props['icon'] | 'UpIconSolid' + let iconStyle: Props['style'] = [] + if (item.isUpvote) { + action = 'upvoted your post' + icon = 'UpIconSolid' + iconStyle = [s.red3, {position: 'relative', top: -4}] + } else if (item.isRepost) { + action = 'reposted your post' + icon = 'retweet' + iconStyle = [s.green3] + } else if (item.isTrend) { + action = 'Your post is trending with' + icon = 'arrow-trend-up' + iconStyle = [s.red3] + } else if (item.isReply) { + action = 'replied to your post' + icon = ['far', 'comment'] + } else if (item.isFollow) { + action = 'followed you' + icon = 'user-plus' + iconStyle = [s.blue3] + } else if (item.isInvite) { + icon = 'users' + iconStyle = [s.blue3] + action = 'invited you to join their scene' + } else { + return <> + } + + let authors: {href: string; handle: string; displayName?: string}[] = [ + { + href: `/profile/${item.author.handle}`, + handle: item.author.handle, + displayName: item.author.displayName, + }, + ] + if (item.additional?.length) { + authors = authors.concat( + item.additional.map(item2 => ({ + href: `/profile/${item2.author.handle}`, + handle: item2.author.handle, + displayName: item2.author.displayName, + })), + ) + } - if (item.isReply || item.isMention) { return ( - - + + + + {icon === 'UpIconSolid' ? ( + + ) : ( + + )} + + + + {authors.slice(0, MAX_AUTHORS).map(author => ( + + + + ))} + {authors.length > MAX_AUTHORS ? ( + + +{authors.length - MAX_AUTHORS} + + ) : undefined} + + + {item.isTrend && ( + {action} + )} + + + {authors[0].displayName || authors[0].handle} + + + {authors.length > 1 ? ( + <> + and + + {authors.length - 1}{' '} + {pluralize(authors.length - 1, 'other')} + + + ) : undefined} + {!item.isTrend && ( + {action} + )} + + {ago(item.indexedAt)} + + + {item.isUpvote || item.isRepost || item.isTrend ? ( + + ) : ( + <> + )} + + + {item.isInvite && ( + + + + )} ) - } - - let action = '' - let icon: Props['icon'] | 'UpIconSolid' - let iconStyle: Props['style'] = [] - if (item.isUpvote) { - action = 'upvoted your post' - icon = 'UpIconSolid' - iconStyle = [s.red3, {position: 'relative', top: -4}] - } else if (item.isRepost) { - action = 'reposted your post' - icon = 'retweet' - iconStyle = [s.green3] - } else if (item.isTrend) { - action = 'Your post is trending with' - icon = 'arrow-trend-up' - iconStyle = [s.red3] - } else if (item.isReply) { - action = 'replied to your post' - icon = ['far', 'comment'] - } else if (item.isFollow) { - action = 'followed you' - icon = 'user-plus' - iconStyle = [s.blue3] - } else if (item.isInvite) { - icon = 'users' - iconStyle = [s.blue3] - action = 'invited you to join their scene' - } else { - return <> - } - - let authors: {href: string; handle: string; displayName?: string}[] = [ - { - href: `/profile/${item.author.handle}`, - handle: item.author.handle, - displayName: item.author.displayName, - }, - ] - if (item.additional?.length) { - authors = authors.concat( - item.additional.map(item2 => ({ - href: `/profile/${item2.author.handle}`, - handle: item2.author.handle, - displayName: item2.author.displayName, - })), - ) - } - - return ( - - - - {icon === 'UpIconSolid' ? ( - - ) : ( - - )} - - - - {authors.slice(0, MAX_AUTHORS).map(author => ( - - - - ))} - {authors.length > MAX_AUTHORS ? ( - - +{authors.length - MAX_AUTHORS} - - ) : undefined} - - - {item.isTrend && ( - {action} - )} - - - {authors[0].displayName || authors[0].handle} - - - {authors.length > 1 ? ( - <> - and - - {authors.length - 1} {pluralize(authors.length - 1, 'other')} - - - ) : undefined} - {!item.isTrend && ( - {action} - )} - - {ago(item.indexedAt)} - - - {item.isUpvote || item.isRepost || item.isTrend ? ( - - ) : ( - <> - )} - - - {item.isInvite && ( - - - - )} - - ) -}) + }), +) function AdditionalPostText({ additionalPost, diff --git a/src/view/com/notifications/InviteAccepter.tsx b/src/view/com/notifications/InviteAccepter.tsx index 8527440067..728392778e 100644 --- a/src/view/com/notifications/InviteAccepter.tsx +++ b/src/view/com/notifications/InviteAccepter.tsx @@ -9,8 +9,13 @@ import {useStores} from '../../../state' import {ProfileCard} from '../profile/ProfileCard' import * as Toast from '../util/Toast' import {s, colors, gradients} from '../../lib/styles' +import {register} from 'react-native-bundle-splitter' -export function InviteAccepter({item}: {item: NotificationsViewItemModel}) { +export const InviteAccepter = register(function InviteAccepter({ + item, +}: { + item: NotificationsViewItemModel +}) { const store = useStores() const [confirmationUri, setConfirmationUri] = useState('') const isMember = @@ -70,7 +75,7 @@ export function InviteAccepter({item}: {item: NotificationsViewItemModel}) { )} ) -} +}) const styles = StyleSheet.create({ container: { diff --git a/src/view/com/onboard/FeatureExplainer.tsx b/src/view/com/onboard/FeatureExplainer.tsx index d9f70cf4fb..bb7212514a 100644 --- a/src/view/com/onboard/FeatureExplainer.tsx +++ b/src/view/com/onboard/FeatureExplainer.tsx @@ -16,6 +16,7 @@ import {useStores} from '../../../state' import {s} from '../../lib/styles' import {SCENE_EXPLAINER, TABS_EXPLAINER} from '../../lib/assets' import {TABS_ENABLED} from '../../../build-flags' +import {register} from 'react-native-bundle-splitter' const Intro = () => ( @@ -79,7 +80,7 @@ const SCENE_MAP = { } const renderScene = SceneMap(SCENE_MAP) -export const FeatureExplainer = () => { +export const FeatureExplainer = register(() => { const layout = useWindowDimensions() const store = useStores() const [index, setIndex] = useState(0) @@ -153,7 +154,7 @@ export const FeatureExplainer = () => { ) -} +}) const styles = StyleSheet.create({ container: { diff --git a/src/view/com/onboard/Follows.tsx b/src/view/com/onboard/Follows.tsx index ab65cd45dd..d6715c9d1d 100644 --- a/src/view/com/onboard/Follows.tsx +++ b/src/view/com/onboard/Follows.tsx @@ -10,32 +10,35 @@ import {observer} from 'mobx-react-lite' import {SuggestedFollows} from '../discover/SuggestedFollows' import {useStores} from '../../../state' import {s} from '../../lib/styles' +import {register} from 'react-native-bundle-splitter' -export const Follows = observer(() => { - const store = useStores() +export const Follows = register( + observer(() => { + const store = useStores() - const onNoSuggestions = () => { - // no suggestions, bounce from this view - store.onboard.next() - } - const onPressNext = () => store.onboard.next() + const onNoSuggestions = () => { + // no suggestions, bounce from this view + store.onboard.next() + } + const onPressNext = () => store.onboard.next() - return ( - - Suggested follows - - - - Skip - - - - Next - - - - ) -}) + return ( + + Suggested follows + + + + Skip + + + + Next + + + + ) + }), +) const styles = StyleSheet.create({ container: { diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx index 1f238f5b39..39d39b5c91 100644 --- a/src/view/com/post-thread/PostRepostedBy.tsx +++ b/src/view/com/post-thread/PostRepostedBy.tsx @@ -10,76 +10,75 @@ import {ErrorMessage} from '../util/ErrorMessage' import {Link} from '../util/Link' import {useStores} from '../../../state' import {s, colors} from '../../lib/styles' +import {register} from 'react-native-bundle-splitter' -export const PostRepostedBy = observer(function PostRepostedBy({ - uri, -}: { - uri: string -}) { - const store = useStores() - const [view, setView] = useState() +export const PostRepostedBy = register( + observer(function PostRepostedBy({uri}: {uri: string}) { + const store = useStores() + const [view, setView] = useState() - useEffect(() => { - if (view?.params.uri === uri) { - console.log('Reposted by doing nothing') - return // no change needed? or trigger refresh? + useEffect(() => { + if (view?.params.uri === uri) { + console.log('Reposted by doing nothing') + return // no change needed? or trigger refresh? + } + console.log('Fetching Reposted by', uri) + const newView = new RepostedByViewModel(store, {uri}) + setView(newView) + newView + .setup() + .catch(err => console.error('Failed to fetch reposted by', err)) + }, [uri, view?.params.uri, store]) + + const onRefresh = () => { + view?.refresh() } - console.log('Fetching Reposted by', uri) - const newView = new RepostedByViewModel(store, {uri}) - setView(newView) - newView - .setup() - .catch(err => console.error('Failed to fetch reposted by', err)) - }, [uri, view?.params.uri, store]) - const onRefresh = () => { - view?.refresh() - } + // loading + // = + if ( + !view || + (view.isLoading && !view.isRefreshing) || + view.params.uri !== uri + ) { + return ( + + + + ) + } - // loading - // = - if ( - !view || - (view.isLoading && !view.isRefreshing) || - view.params.uri !== uri - ) { - return ( - - - + // error + // = + if (view.hasError) { + return ( + + + + ) + } + + // loaded + // = + const renderItem = ({item}: {item: RepostedByViewItemModel}) => ( + ) - } - - // error - // = - if (view.hasError) { return ( - item._reactKey} + renderItem={renderItem} /> ) - } - - // loaded - // = - const renderItem = ({item}: {item: RepostedByViewItemModel}) => ( - - ) - return ( - - item._reactKey} - renderItem={renderItem} - /> - - ) -}) + }), +) const RepostedByItem = ({item}: {item: RepostedByViewItemModel}) => { return ( diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index bbaa4efa2a..ab6946221f 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -8,85 +8,88 @@ import { import {useStores} from '../../../state' import {PostThreadItem} from './PostThreadItem' import {ErrorMessage} from '../util/ErrorMessage' +import {register} from 'react-native-bundle-splitter' -export const PostThread = observer(function PostThread({ - uri, - view, -}: { - uri: string - view: PostThreadViewModel -}) { - const ref = useRef(null) - const posts = view.thread ? Array.from(flattenThread(view.thread)) : [] - const onRefresh = () => { - view?.refresh().catch(err => console.error('Failed to refresh', err)) - } - const onLayout = () => { - const index = posts.findIndex(post => post._isHighlightedPost) - if (index !== -1) { - ref.current?.scrollToIndex({ - index, +export const PostThread = register( + observer(function PostThread({ + uri, + view, + }: { + uri: string + view: PostThreadViewModel + }) { + const ref = useRef(null) + const posts = view.thread ? Array.from(flattenThread(view.thread)) : [] + const onRefresh = () => { + view?.refresh().catch(err => console.error('Failed to refresh', err)) + } + const onLayout = () => { + const index = posts.findIndex(post => post._isHighlightedPost) + if (index !== -1) { + ref.current?.scrollToIndex({ + index, + animated: false, + viewOffset: 40, + }) + } + } + const onScrollToIndexFailed = (info: { + index: number + highestMeasuredFrameIndex: number + averageItemLength: number + }) => { + ref.current?.scrollToOffset({ animated: false, - viewOffset: 40, + offset: info.averageItemLength * info.index, }) } - } - const onScrollToIndexFailed = (info: { - index: number - highestMeasuredFrameIndex: number - averageItemLength: number - }) => { - ref.current?.scrollToOffset({ - animated: false, - offset: info.averageItemLength * info.index, - }) - } - // loading - // = - if ((view.isLoading && !view.isRefreshing) || view.params.uri !== uri) { - return ( - - - + // loading + // = + if ((view.isLoading && !view.isRefreshing) || view.params.uri !== uri) { + return ( + + + + ) + } + + // error + // = + if (view.hasError) { + return ( + + + + ) + } + + // loaded + // = + const renderItem = ({item}: {item: PostThreadViewPostModel}) => ( + ) - } - - // error - // = - if (view.hasError) { return ( - - - + item._reactKey} + renderItem={renderItem} + refreshing={view.isRefreshing} + onRefresh={onRefresh} + onLayout={onLayout} + onScrollToIndexFailed={onScrollToIndexFailed} + style={{flex: 1}} + contentContainerStyle={{paddingBottom: 200}} + /> ) - } - - // loaded - // = - const renderItem = ({item}: {item: PostThreadViewPostModel}) => ( - - ) - return ( - item._reactKey} - renderItem={renderItem} - refreshing={view.isRefreshing} - onRefresh={onRefresh} - onLayout={onLayout} - onScrollToIndexFailed={onScrollToIndexFailed} - style={{flex: 1}} - contentContainerStyle={{paddingBottom: 200}} - /> - ) -}) + }), +) function* flattenThread( post: PostThreadViewPostModel, diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 8408eb6c05..2c6d62ce00 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -18,95 +18,240 @@ import {PostMeta} from '../util/PostMeta' import {PostEmbeds} from '../util/PostEmbeds' import {PostCtrls} from '../util/PostCtrls' import {ComposePrompt} from '../composer/Prompt' +import {register} from 'react-native-bundle-splitter' const PARENT_REPLY_LINE_LENGTH = 8 const REPLYING_TO_LINE_LENGTH = 6 -export const PostThreadItem = observer(function PostThreadItem({ - item, - onPostReply, -}: { - item: PostThreadViewPostModel - onPostReply: () => void -}) { - const store = useStores() - const [deleted, setDeleted] = useState(false) - const record = item.record as unknown as PostType.Record - const hasEngagement = item.upvoteCount || item.repostCount +export const PostThreadItem = register( + observer(function PostThreadItem({ + item, + onPostReply, + }: { + item: PostThreadViewPostModel + onPostReply: () => void + }) { + const store = useStores() + const [deleted, setDeleted] = useState(false) + const record = item.record as unknown as PostType.Record + const hasEngagement = item.upvoteCount || item.repostCount - const itemHref = useMemo(() => { - const urip = new AtUri(item.uri) - return `/profile/${item.author.handle}/post/${urip.rkey}` - }, [item.uri, item.author.handle]) - const itemTitle = `Post by ${item.author.handle}` - const authorHref = `/profile/${item.author.handle}` - const authorTitle = item.author.handle - const upvotesHref = useMemo(() => { - const urip = new AtUri(item.uri) - return `/profile/${item.author.handle}/post/${urip.rkey}/upvoted-by` - }, [item.uri, item.author.handle]) - const upvotesTitle = 'Upvotes on this post' - const repostsHref = useMemo(() => { - const urip = new AtUri(item.uri) - return `/profile/${item.author.handle}/post/${urip.rkey}/reposted-by` - }, [item.uri, item.author.handle]) - const repostsTitle = 'Reposts of this post' + const itemHref = useMemo(() => { + const urip = new AtUri(item.uri) + return `/profile/${item.author.handle}/post/${urip.rkey}` + }, [item.uri, item.author.handle]) + const itemTitle = `Post by ${item.author.handle}` + const authorHref = `/profile/${item.author.handle}` + const authorTitle = item.author.handle + const upvotesHref = useMemo(() => { + const urip = new AtUri(item.uri) + return `/profile/${item.author.handle}/post/${urip.rkey}/upvoted-by` + }, [item.uri, item.author.handle]) + const upvotesTitle = 'Upvotes on this post' + const repostsHref = useMemo(() => { + const urip = new AtUri(item.uri) + return `/profile/${item.author.handle}/post/${urip.rkey}/reposted-by` + }, [item.uri, item.author.handle]) + const repostsTitle = 'Reposts of this post' - const onPressReply = () => { - store.shell.openComposer({ - replyTo: { - uri: item.uri, - cid: item.cid, - text: item.record.text as string, - author: { - handle: item.author.handle, - displayName: item.author.displayName, - avatar: item.author.avatar, + const onPressReply = () => { + store.shell.openComposer({ + replyTo: { + uri: item.uri, + cid: item.cid, + text: item.record.text as string, + author: { + handle: item.author.handle, + displayName: item.author.displayName, + avatar: item.author.avatar, + }, }, - }, - onPost: onPostReply, - }) - } - const onPressToggleRepost = () => { - item - .toggleRepost() - .catch(e => console.error('Failed to toggle repost', record, e)) - } - const onPressToggleUpvote = () => { - item - .toggleUpvote() - .catch(e => console.error('Failed to toggle upvote', record, e)) - } - const onCopyPostText = () => { - Clipboard.setString(record.text) - Toast.show('Copied to clipboard') - } - const onDeletePost = () => { - item.delete().then( - () => { - setDeleted(true) - Toast.show('Post deleted') - }, - e => { - console.error(e) - Toast.show('Failed to delete post, please try again') - }, - ) - } + onPost: onPostReply, + }) + } + const onPressToggleRepost = () => { + item + .toggleRepost() + .catch(e => console.error('Failed to toggle repost', record, e)) + } + const onPressToggleUpvote = () => { + item + .toggleUpvote() + .catch(e => console.error('Failed to toggle upvote', record, e)) + } + const onCopyPostText = () => { + Clipboard.setString(record.text) + Toast.show('Copied to clipboard') + } + const onDeletePost = () => { + item.delete().then( + () => { + setDeleted(true) + Toast.show('Post deleted') + }, + e => { + console.error(e) + Toast.show('Failed to delete post, please try again') + }, + ) + } - if (deleted) { - return ( - - - This post has been deleted. - - ) - } + if (deleted) { + return ( + + + This post has been deleted. + + ) + } - if (item._isHighlightedPost) { - return ( - <> - + if (item._isHighlightedPost) { + return ( + <> + + + + + + + + + + + + {item.author.displayName || item.author.handle} + + + + · {ago(item.indexedAt)} + + + + + + + + + + @{item.author.handle} + + + + + + + + + + + {item._isHighlightedPost && hasEngagement ? ( + + {item.repostCount ? ( + + + + {item.repostCount} + {' '} + {pluralize(item.repostCount, 'repost')} + + + ) : ( + <> + )} + {item.upvoteCount ? ( + + + + {item.upvoteCount} + {' '} + {pluralize(item.upvoteCount, 'upvote')} + + + ) : ( + <> + )} + + ) : ( + <> + )} + + + + + + + + ) + } else { + return ( + + {!item.replyingTo && item.record.reply && ( + + )} + {item.replies?.length !== 0 && } + {item.replyingTo ? ( + + + + + + + {item.replyingTo.text} + + + ) : undefined} @@ -119,94 +264,32 @@ export const PostThreadItem = observer(function PostThreadItem({ - - - - {item.author.displayName || item.author.handle} - - - - · {ago(item.indexedAt)} - - - - - - - - - - @{item.author.handle} - - - - - - - - - - - {item._isHighlightedPost && hasEngagement ? ( - - {item.repostCount ? ( - - - - {item.repostCount} - {' '} - {pluralize(item.repostCount, 'repost')} - - - ) : ( - <> - )} - {item.upvoteCount ? ( - - - - {item.upvoteCount} - {' '} - {pluralize(item.upvoteCount, 'upvote')} - - - ) : ( - <> - )} + + - ) : ( - <> - )} - + - - - - ) - } else { - return ( - - {!item.replyingTo && item.record.reply && ( - - )} - {item.replies?.length !== 0 && } - {item.replyingTo ? ( - - - - - - - {item.replyingTo.text} - - - ) : undefined} - - - - - - - - - - - - - - - - - ) - } -}) + + ) + } + }), +) const styles = StyleSheet.create({ outer: { diff --git a/src/view/com/post-thread/PostVotedBy.tsx b/src/view/com/post-thread/PostVotedBy.tsx index 7b798482a0..e3c3d72bb7 100644 --- a/src/view/com/post-thread/PostVotedBy.tsx +++ b/src/view/com/post-thread/PostVotedBy.tsx @@ -10,76 +10,81 @@ import {ErrorMessage} from '../util/ErrorMessage' import {UserAvatar} from '../util/UserAvatar' import {useStores} from '../../../state' import {s, colors} from '../../lib/styles' +import {register} from 'react-native-bundle-splitter' -export const PostVotedBy = observer(function PostVotedBy({ - uri, - direction, -}: { - uri: string - direction: 'up' | 'down' -}) { - const store = useStores() - const [view, setView] = useState() +export const PostVotedBy = register( + observer(function PostVotedBy({ + uri, + direction, + }: { + uri: string + direction: 'up' | 'down' + }) { + const store = useStores() + const [view, setView] = useState() - useEffect(() => { - if (view?.params.uri === uri) { - console.log('Voted by doing nothing') - return // no change needed? or trigger refresh? + useEffect(() => { + if (view?.params.uri === uri) { + console.log('Voted by doing nothing') + return // no change needed? or trigger refresh? + } + console.log('Fetching voted by', uri) + const newView = new VotesViewModel(store, {uri, direction}) + setView(newView) + newView + .setup() + .catch(err => console.error('Failed to fetch voted by', err)) + }, [uri, view?.params.uri, store]) + + const onRefresh = () => { + view?.refresh() } - console.log('Fetching voted by', uri) - const newView = new VotesViewModel(store, {uri, direction}) - setView(newView) - newView.setup().catch(err => console.error('Failed to fetch voted by', err)) - }, [uri, view?.params.uri, store]) - const onRefresh = () => { - view?.refresh() - } + // loading + // = + if ( + !view || + (view.isLoading && !view.isRefreshing) || + view.params.uri !== uri + ) { + return ( + + + + ) + } - // loading - // = - if ( - !view || - (view.isLoading && !view.isRefreshing) || - view.params.uri !== uri - ) { - return ( - - - + // error + // = + if (view.hasError) { + return ( + + + + ) + } + + // loaded + // = + const renderItem = ({item}: {item: VotesViewItemModel}) => ( + ) - } - - // error - // = - if (view.hasError) { return ( - item._reactKey} + renderItem={renderItem} /> ) - } - - // loaded - // = - const renderItem = ({item}: {item: VotesViewItemModel}) => ( - - ) - return ( - - item._reactKey} - renderItem={renderItem} - /> - - ) -}) + }), +) const LikedByItem = ({item}: {item: VotesViewItemModel}) => { return ( diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 97bec92eab..59fed7f2b7 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -22,173 +22,180 @@ import * as Toast from '../util/Toast' import {UserAvatar} from '../util/UserAvatar' import {useStores} from '../../../state' import {s, colors} from '../../lib/styles' +import {register} from 'react-native-bundle-splitter' -export const Post = observer(function Post({ - uri, - initView, - style, -}: { - uri: string - initView?: PostThreadViewModel - style?: StyleProp -}) { - const store = useStores() - const [view, setView] = useState(initView) - const [deleted, setDeleted] = useState(false) +export const Post = register( + observer(function Post({ + uri, + initView, + style, + }: { + uri: string + initView?: PostThreadViewModel + style?: StyleProp + }) { + const store = useStores() + const [view, setView] = useState(initView) + const [deleted, setDeleted] = useState(false) - useEffect(() => { - if (initView || view?.params.uri === uri) { - return // no change needed? or trigger refresh? + useEffect(() => { + if (initView || view?.params.uri === uri) { + return // no change needed? or trigger refresh? + } + const newView = new PostThreadViewModel(store, {uri, depth: 0}) + setView(newView) + newView.setup().catch(err => console.error('Failed to fetch post', err)) + }, [initView, uri, view?.params.uri, store]) + + // deleted + // = + if (deleted) { + return } - const newView = new PostThreadViewModel(store, {uri, depth: 0}) - setView(newView) - newView.setup().catch(err => console.error('Failed to fetch post', err)) - }, [initView, uri, view?.params.uri, store]) - // deleted - // = - if (deleted) { - return - } - - // loading - // = - if (!view || view.isLoading || view.params.uri !== uri) { - return ( - - - - ) - } - - // error - // = - if (view.hasError || !view.thread) { - return ( - - {view.error || 'Thread not found'} - - ) - } - - // loaded - // = - const item = view.thread - const record = view.thread?.record as unknown as PostType.Record - - const itemUrip = new AtUri(item.uri) - const itemHref = `/profile/${item.author.handle}/post/${itemUrip.rkey}` - const itemTitle = `Post by ${item.author.handle}` - const authorHref = `/profile/${item.author.handle}` - const authorTitle = item.author.handle - let replyAuthorDid = '' - let replyHref = '' - if (record.reply) { - const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri) - replyAuthorDid = urip.hostname - replyHref = `/profile/${urip.hostname}/post/${urip.rkey}` - } - const onPressReply = () => { - store.shell.openComposer({ - replyTo: { - uri: item.uri, - cid: item.cid, - text: item.record.text as string, - author: { - handle: item.author.handle, - displayName: item.author.displayName, - avatar: item.author.avatar, - }, - }, - }) - } - const onPressToggleRepost = () => { - item - .toggleRepost() - .catch(e => console.error('Failed to toggle repost', record, e)) - } - const onPressToggleUpvote = () => { - item - .toggleUpvote() - .catch(e => console.error('Failed to toggle upvote', record, e)) - } - const onCopyPostText = () => { - Clipboard.setString(record.text) - Toast.show('Copied to clipboard') - } - const onDeletePost = () => { - item.delete().then( - () => { - setDeleted(true) - Toast.show('Post deleted') - }, - e => { - console.error(e) - Toast.show('Failed to delete post, please try again') - }, - ) - } - - return ( - - - - - - + // loading + // = + if (!view || view.isLoading || view.params.uri !== uri) { + return ( + + - - - {replyHref !== '' && ( - - - Reply to - - + {view.error || 'Thread not found'} + + ) + } + + // loaded + // = + const item = view.thread + const record = view.thread?.record as unknown as PostType.Record + + const itemUrip = new AtUri(item.uri) + const itemHref = `/profile/${item.author.handle}/post/${itemUrip.rkey}` + const itemTitle = `Post by ${item.author.handle}` + const authorHref = `/profile/${item.author.handle}` + const authorTitle = item.author.handle + let replyAuthorDid = '' + let replyHref = '' + if (record.reply) { + const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri) + replyAuthorDid = urip.hostname + replyHref = `/profile/${urip.hostname}/post/${urip.rkey}` + } + const onPressReply = () => { + store.shell.openComposer({ + replyTo: { + uri: item.uri, + cid: item.cid, + text: item.record.text as string, + author: { + handle: item.author.handle, + displayName: item.author.displayName, + avatar: item.author.avatar, + }, + }, + }) + } + const onPressToggleRepost = () => { + item + .toggleRepost() + .catch(e => console.error('Failed to toggle repost', record, e)) + } + const onPressToggleUpvote = () => { + item + .toggleUpvote() + .catch(e => console.error('Failed to toggle upvote', record, e)) + } + const onCopyPostText = () => { + Clipboard.setString(record.text) + Toast.show('Copied to clipboard') + } + const onDeletePost = () => { + item.delete().then( + () => { + setDeleted(true) + Toast.show('Post deleted') + }, + e => { + console.error(e) + Toast.show('Failed to delete post, please try again') + }, + ) + } + + return ( + + + + + + + + + + {replyHref !== '' && ( + + - + Reply to + + + + + )} + + - )} - - - - - - ) -}) + + ) + }), +) const styles = StyleSheet.create({ outer: { diff --git a/src/view/com/post/PostText.tsx b/src/view/com/post/PostText.tsx index 5d6c45113c..3b81be1b88 100644 --- a/src/view/com/post/PostText.tsx +++ b/src/view/com/post/PostText.tsx @@ -5,53 +5,50 @@ import {LoadingPlaceholder} from '../util/LoadingPlaceholder' import {ErrorMessage} from '../util/ErrorMessage' import {PostModel} from '../../../state/models/post' import {useStores} from '../../../state' +import {register} from 'react-native-bundle-splitter' -export const PostText = observer(function PostText({ - uri, - style, -}: { - uri: string - style?: StyleProp -}) { - const store = useStores() - const [model, setModel] = useState() +export const PostText = register( + observer(function PostText({uri, style}: {uri: string; style?: StyleProp}) { + const store = useStores() + const [model, setModel] = useState() - useEffect(() => { - if (model?.uri === uri) { - return // no change needed? or trigger refresh? + useEffect(() => { + if (model?.uri === uri) { + return // no change needed? or trigger refresh? + } + const newModel = new PostModel(store, uri) + setModel(newModel) + newModel.setup().catch(err => console.error('Failed to fetch post', err)) + }, [uri, model?.uri, store]) + + // loading + // = + if (!model || model.isLoading || model.uri !== uri) { + return ( + + + + + + ) } - const newModel = new PostModel(store, uri) - setModel(newModel) - newModel.setup().catch(err => console.error('Failed to fetch post', err)) - }, [uri, model?.uri, store]) - // loading - // = - if (!model || model.isLoading || model.uri !== uri) { + // error + // = + if (model.hasError) { + return ( + + + + ) + } + + // loaded + // = return ( - - - + {model.text} ) - } - - // error - // = - if (model.hasError) { - return ( - - - - ) - } - - // loaded - // = - return ( - - {model.text} - - ) -}) + }), +) diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 9e261638b4..0e3dae51f8 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -7,80 +7,83 @@ import {ErrorMessage} from '../util/ErrorMessage' import {FeedModel} from '../../../state/models/feed-view' import {FeedItem} from './FeedItem' import {ComposePrompt} from '../composer/Prompt' +import {register} from 'react-native-bundle-splitter' const COMPOSE_PROMPT_ITEM = {_reactKey: '__prompt__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} -export const Feed = observer(function Feed({ - feed, - style, - scrollElRef, - onPressCompose, - onPressTryAgain, -}: { - feed: FeedModel - style?: StyleProp - scrollElRef?: MutableRefObject | null> - onPressCompose: () => void - onPressTryAgain?: () => void -}) { - // TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf - // VirtualizedList: You have a large list that is slow to update - make sure your - // renderItem function renders components that follow React performance best practices - // like PureComponent, shouldComponentUpdate, etc - const renderItem = ({item}: {item: any}) => { - if (item === COMPOSE_PROMPT_ITEM) { - return - } else if (item === EMPTY_FEED_ITEM) { - return ( - - ) - } else { - return +export const Feed = register( + observer(function Feed({ + feed, + style, + scrollElRef, + onPressCompose, + onPressTryAgain, + }: { + feed: FeedModel + style?: StyleProp + scrollElRef?: MutableRefObject | null> + onPressCompose: () => void + onPressTryAgain?: () => void + }) { + // TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf + // VirtualizedList: You have a large list that is slow to update - make sure your + // renderItem function renders components that follow React performance best practices + // like PureComponent, shouldComponentUpdate, etc + const renderItem = ({item}: {item: any}) => { + if (item === COMPOSE_PROMPT_ITEM) { + return + } else if (item === EMPTY_FEED_ITEM) { + return ( + + ) + } else { + return + } } - } - const onRefresh = () => { - feed.refresh().catch(err => console.error('Failed to refresh', err)) - } - const onEndReached = () => { - feed.loadMore().catch(err => console.error('Failed to load more', err)) - } - let data - if (feed.hasLoaded) { - if (feed.isEmpty) { - data = [COMPOSE_PROMPT_ITEM, EMPTY_FEED_ITEM] - } else { - data = [COMPOSE_PROMPT_ITEM].concat(feed.feed) + const onRefresh = () => { + feed.refresh().catch(err => console.error('Failed to refresh', err)) } - } - return ( - - {!data && } - {feed.isLoading && !data && } - {feed.hasError && ( - - )} - {feed.hasLoaded && data && ( - item._reactKey} - renderItem={renderItem} - refreshing={feed.isRefreshing} - contentContainerStyle={{paddingBottom: 100}} - onRefresh={onRefresh} - onEndReached={onEndReached} - /> - )} - - ) -}) + const onEndReached = () => { + feed.loadMore().catch(err => console.error('Failed to load more', err)) + } + let data + if (feed.hasLoaded) { + if (feed.isEmpty) { + data = [COMPOSE_PROMPT_ITEM, EMPTY_FEED_ITEM] + } else { + data = [COMPOSE_PROMPT_ITEM].concat(feed.feed) + } + } + return ( + + {!data && } + {feed.isLoading && !data && } + {feed.hasError && ( + + )} + {feed.hasLoaded && data && ( + item._reactKey} + renderItem={renderItem} + refreshing={feed.isRefreshing} + contentContainerStyle={{paddingBottom: 100}} + onRefresh={onRefresh} + onEndReached={onEndReached} + /> + )} + + ) + }), +) diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 62f1996732..861bbcf480 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -16,204 +16,209 @@ import * as Toast from '../util/Toast' import {UserAvatar} from '../util/UserAvatar' import {s, colors} from '../../lib/styles' import {useStores} from '../../../state' +import {register} from 'react-native-bundle-splitter' const TOP_REPLY_LINE_LENGTH = 12 const REPLYING_TO_LINE_LENGTH = 8 -export const FeedItem = observer(function FeedItem({ - item, -}: { - item: FeedItemModel -}) { - const store = useStores() - const [deleted, setDeleted] = useState(false) - const record = item.record as unknown as PostType.Record - const itemHref = useMemo(() => { - const urip = new AtUri(item.uri) - return `/profile/${item.author.handle}/post/${urip.rkey}` - }, [item.uri, item.author.handle]) - const itemTitle = `Post by ${item.author.handle}` - const authorHref = `/profile/${item.author.handle}` - const replyAuthorDid = useMemo(() => { - if (!record.reply) return '' - const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri) - return urip.hostname - }, [record.reply]) - const replyHref = useMemo(() => { - if (!record.reply) return '' - const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri) - return `/profile/${urip.hostname}/post/${urip.rkey}` - }, [record.reply]) +export const FeedItem = register( + observer(function FeedItem({item}: {item: FeedItemModel}) { + const store = useStores() + const [deleted, setDeleted] = useState(false) + const record = item.record as unknown as PostType.Record + const itemHref = useMemo(() => { + const urip = new AtUri(item.uri) + return `/profile/${item.author.handle}/post/${urip.rkey}` + }, [item.uri, item.author.handle]) + const itemTitle = `Post by ${item.author.handle}` + const authorHref = `/profile/${item.author.handle}` + const replyAuthorDid = useMemo(() => { + if (!record.reply) { + return '' + } + const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri) + return urip.hostname + }, [record.reply]) + const replyHref = useMemo(() => { + if (!record.reply) { + return '' + } + const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri) + return `/profile/${urip.hostname}/post/${urip.rkey}` + }, [record.reply]) - const onPressReply = () => { - store.shell.openComposer({ - replyTo: { - uri: item.uri, - cid: item.cid, - text: item.record.text as string, - author: { - handle: item.author.handle, - displayName: item.author.displayName, - avatar: item.author.avatar, + const onPressReply = () => { + store.shell.openComposer({ + replyTo: { + uri: item.uri, + cid: item.cid, + text: item.record.text as string, + author: { + handle: item.author.handle, + displayName: item.author.displayName, + avatar: item.author.avatar, + }, }, - }, - }) - } - const onPressToggleRepost = () => { - item - .toggleRepost() - .catch(e => console.error('Failed to toggle repost', record, e)) - } - const onPressToggleUpvote = () => { - item - .toggleUpvote() - .catch(e => console.error('Failed to toggle upvote', record, e)) - } - const onCopyPostText = () => { - Clipboard.setString(record.text) - Toast.show('Copied to clipboard') - } - const onDeletePost = () => { - item.delete().then( - () => { - setDeleted(true) - Toast.show('Post deleted') - }, - e => { - console.error(e) - Toast.show('Failed to delete post, please try again') - }, - ) - } + }) + } + const onPressToggleRepost = () => { + item + .toggleRepost() + .catch(e => console.error('Failed to toggle repost', record, e)) + } + const onPressToggleUpvote = () => { + item + .toggleUpvote() + .catch(e => console.error('Failed to toggle upvote', record, e)) + } + const onCopyPostText = () => { + Clipboard.setString(record.text) + Toast.show('Copied to clipboard') + } + const onDeletePost = () => { + item.delete().then( + () => { + setDeleted(true) + Toast.show('Post deleted') + }, + e => { + console.error(e) + Toast.show('Failed to delete post, please try again') + }, + ) + } - if (deleted) { - return - } + if (deleted) { + return + } - const outerStyles = [ - styles.outer, - item._isThreadChild ? styles.outerNoTop : undefined, - item._isThreadParent ? styles.outerNoBottom : undefined, - ] - return ( - - {item._isThreadChild && } - {item._isThreadParent && ( - - )} - {item.repostedBy && ( - - - - Reposted by {item.repostedBy.displayName || item.repostedBy.handle} - - - )} - {item.trendedBy && ( - - + {item._isThreadChild && } + {item._isThreadParent && ( + - - Trending with {item.trendedBy.displayName || item.trendedBy.handle} - - - )} - {item.additionalParentPost ? ( - - - - - - - - {item.additionalParentPost?.thread?.record.text} - - - - ) : undefined} - - + )} + {item.repostedBy && ( - + style={styles.includeReason} + href={`/profile/${item.repostedBy.handle}`} + title={item.repostedBy.displayName || item.repostedBy.handle}> + + + Reposted by{' '} + {item.repostedBy.displayName || item.repostedBy.handle} + - - - {!item._isThreadChild ? ( - + - ) : undefined} - {!item._isThreadChild && replyHref !== '' && ( - - Replying to - - - + + Trending with{' '} + {item.trendedBy.displayName || item.trendedBy.handle} + + + )} + {item.additionalParentPost ? ( + + + + - )} - - + + {item.additionalParentPost?.thread?.record.text} + + + + ) : undefined} + + + + + + + + {!item._isThreadChild ? ( + + ) : undefined} + {!item._isThreadChild && replyHref !== '' && ( + + Replying to + + + + + )} + + + + + - - - - - ) -}) + + ) + }), +) const styles = StyleSheet.create({ outer: { diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index 22b8bbdeaa..f0cfbb62b6 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -3,8 +3,9 @@ import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' import {Link} from '../util/Link' import {UserAvatar} from '../util/UserAvatar' import {s, colors} from '../../lib/styles' +import {register} from 'react-native-bundle-splitter' -export function ProfileCard({ +export const ProfileCard = register(function ProfileCard({ did, handle, displayName, @@ -48,7 +49,7 @@ export function ProfileCard({ ) -} +}) const styles = StyleSheet.create({ outer: { diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx index 6801ba161a..47dea283a4 100644 --- a/src/view/com/profile/ProfileFollowers.tsx +++ b/src/view/com/profile/ProfileFollowers.tsx @@ -10,74 +10,73 @@ import {ErrorMessage} from '../util/ErrorMessage' import {UserAvatar} from '../util/UserAvatar' import {useStores} from '../../../state' import {s, colors} from '../../lib/styles' +import {register} from 'react-native-bundle-splitter' -export const ProfileFollowers = observer(function ProfileFollowers({ - name, -}: { - name: string -}) { - const store = useStores() - const [view, setView] = useState() +export const ProfileFollowers = register( + observer(function ProfileFollowers({name}: {name: string}) { + const store = useStores() + const [view, setView] = useState() - useEffect(() => { - if (view?.params.user === name) { - console.log('User followers doing nothing') - return // no change needed? or trigger refresh? + useEffect(() => { + if (view?.params.user === name) { + console.log('User followers doing nothing') + return // no change needed? or trigger refresh? + } + console.log('Fetching user followers', name) + const newView = new UserFollowersViewModel(store, {user: name}) + setView(newView) + newView + .setup() + .catch(err => console.error('Failed to fetch user followers', err)) + }, [name, view?.params.user, store]) + + const onRefresh = () => { + view?.refresh() } - console.log('Fetching user followers', name) - const newView = new UserFollowersViewModel(store, {user: name}) - setView(newView) - newView - .setup() - .catch(err => console.error('Failed to fetch user followers', err)) - }, [name, view?.params.user, store]) - const onRefresh = () => { - view?.refresh() - } + // loading + // = + if ( + !view || + (view.isLoading && !view.isRefreshing) || + view.params.user !== name + ) { + return ( + + + + ) + } - // loading - // = - if ( - !view || - (view.isLoading && !view.isRefreshing) || - view.params.user !== name - ) { + // error + // = + if (view.hasError) { + return ( + + + + ) + } + + // loaded + // = + const renderItem = ({item}: {item: FollowerItem}) => return ( - - - ) - } - - // error - // = - if (view.hasError) { - return ( - - item._reactKey} + renderItem={renderItem} /> ) - } - - // loaded - // = - const renderItem = ({item}: {item: FollowerItem}) => - return ( - - item._reactKey} - renderItem={renderItem} - /> - - ) -}) + }), +) const User = ({item}: {item: FollowerItem}) => { return ( diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx index a705cf467e..65ba782fd2 100644 --- a/src/view/com/profile/ProfileFollows.tsx +++ b/src/view/com/profile/ProfileFollows.tsx @@ -10,74 +10,73 @@ import {Link} from '../util/Link' import {ErrorMessage} from '../util/ErrorMessage' import {UserAvatar} from '../util/UserAvatar' import {s, colors} from '../../lib/styles' +import {register} from 'react-native-bundle-splitter' -export const ProfileFollows = observer(function ProfileFollows({ - name, -}: { - name: string -}) { - const store = useStores() - const [view, setView] = useState() +export const ProfileFollows = register( + observer(function ProfileFollows({name}: {name: string}) { + const store = useStores() + const [view, setView] = useState() - useEffect(() => { - if (view?.params.user === name) { - console.log('User follows doing nothing') - return // no change needed? or trigger refresh? + useEffect(() => { + if (view?.params.user === name) { + console.log('User follows doing nothing') + return // no change needed? or trigger refresh? + } + console.log('Fetching user follows', name) + const newView = new UserFollowsViewModel(store, {user: name}) + setView(newView) + newView + .setup() + .catch(err => console.error('Failed to fetch user follows', err)) + }, [name, view?.params.user, store]) + + const onRefresh = () => { + view?.refresh() } - console.log('Fetching user follows', name) - const newView = new UserFollowsViewModel(store, {user: name}) - setView(newView) - newView - .setup() - .catch(err => console.error('Failed to fetch user follows', err)) - }, [name, view?.params.user, store]) - const onRefresh = () => { - view?.refresh() - } + // loading + // = + if ( + !view || + (view.isLoading && !view.isRefreshing) || + view.params.user !== name + ) { + return ( + + + + ) + } - // loading - // = - if ( - !view || - (view.isLoading && !view.isRefreshing) || - view.params.user !== name - ) { + // error + // = + if (view.hasError) { + return ( + + + + ) + } + + // loaded + // = + const renderItem = ({item}: {item: FollowItem}) => return ( - - - ) - } - - // error - // = - if (view.hasError) { - return ( - - item._reactKey} + renderItem={renderItem} /> ) - } - - // loaded - // = - const renderItem = ({item}: {item: FollowItem}) => - return ( - - item._reactKey} - renderItem={renderItem} - /> - - ) -}) + }), +) const User = ({item}: {item: FollowItem}) => { return ( diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index fb31c18477..fd07e94b4d 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -21,292 +21,298 @@ import {RichText} from '../util/RichText' import {UserAvatar} from '../util/UserAvatar' import {UserBanner} from '../util/UserBanner' import {UserInfoText} from '../util/UserInfoText' +import {register} from 'react-native-bundle-splitter' -export const ProfileHeader = observer(function ProfileHeader({ - view, - onRefreshAll, -}: { - view: ProfileViewModel - onRefreshAll: () => void -}) { - const store = useStores() - const isMember = useMemo( - () => view.isScene && view.myState.member, - [view.myState.member], - ) +export const ProfileHeader = register( + observer(function ProfileHeader({ + view, + onRefreshAll, + }: { + view: ProfileViewModel + onRefreshAll: () => void + }) { + const store = useStores() + const isMember = useMemo( + () => view.isScene && view.myState.member, + [view.myState.member], + ) - const onPressBack = () => { - store.nav.tab.goBack() - } - const onPressSearch = () => { - store.nav.navigate(`/search`) - } - const onPressToggleFollow = () => { - view?.toggleFollowing().then( - () => { - Toast.show( - `${view.myState.follow ? 'Following' : 'No longer following'} ${ - view.displayName || view.handle - }`, - ) - }, - err => console.error('Failed to toggle follow', err), - ) - } - const onPressEditProfile = () => { - store.shell.openModal(new EditProfileModel(view, onRefreshAll)) - } - const onPressFollowers = () => { - store.nav.navigate(`/profile/${view.handle}/followers`) - } - const onPressFollows = () => { - store.nav.navigate(`/profile/${view.handle}/follows`) - } - const onPressMembers = () => { - store.nav.navigate(`/profile/${view.handle}/members`) - } - const onPressInviteMembers = () => { - store.shell.openModal(new InviteToSceneModel(view)) - } - const onPressLeaveScene = () => { - store.shell.openModal( - new ConfirmModel( - 'Leave this scene?', - `You'll be able to come back unless your invite is revoked.`, - onPressConfirmLeaveScene, - ), - ) - } - const onPressConfirmLeaveScene = async () => { - if (view.myState.member) { - await store.api.app.bsky.graph.confirmation.delete({ - did: store.me.did || '', - rkey: new AtUri(view.myState.member).rkey, - }) - Toast.show(`Scene left`) + const onPressBack = () => { + store.nav.tab.goBack() + } + const onPressSearch = () => { + store.nav.navigate('/search') + } + const onPressToggleFollow = () => { + view?.toggleFollowing().then( + () => { + Toast.show( + `${view.myState.follow ? 'Following' : 'No longer following'} ${ + view.displayName || view.handle + }`, + ) + }, + err => console.error('Failed to toggle follow', err), + ) + } + const onPressEditProfile = () => { + store.shell.openModal(new EditProfileModel(view, onRefreshAll)) + } + const onPressFollowers = () => { + store.nav.navigate(`/profile/${view.handle}/followers`) + } + const onPressFollows = () => { + store.nav.navigate(`/profile/${view.handle}/follows`) + } + const onPressMembers = () => { + store.nav.navigate(`/profile/${view.handle}/members`) + } + const onPressInviteMembers = () => { + store.shell.openModal(new InviteToSceneModel(view)) + } + const onPressLeaveScene = () => { + store.shell.openModal( + new ConfirmModel( + 'Leave this scene?', + "You'll be able to come back unless your invite is revoked.", + onPressConfirmLeaveScene, + ), + ) + } + const onPressConfirmLeaveScene = async () => { + if (view.myState.member) { + await store.api.app.bsky.graph.confirmation.delete({ + did: store.me.did || '', + rkey: new AtUri(view.myState.member).rkey, + }) + Toast.show('Scene left') + } + onRefreshAll() } - onRefreshAll() - } - // loading - // = - if (!view || !view.hasLoaded) { + // loading + // = + if (!view || !view.hasLoaded) { + return ( + + + + + + + + + + + + {view.displayName || view.handle} + + + + + ) + } + + // error + // = + if (view.hasError) { + return ( + + {view.error} + + ) + } + + // loaded + // = + const gradient = getGradient(view.handle) + const isMe = store.me.did === view.did + const isCreator = view.isScene && view.creator === store.me.did + let dropdownItems: DropdownItem[] | undefined + if (isCreator || isMember) { + dropdownItems = [] + if (isCreator) { + dropdownItems.push({ + label: 'Edit Profile', + onPress: onPressEditProfile, + }) + } + if (isMember) { + dropdownItems.push({ + label: 'Leave Scene...', + onPress: onPressLeaveScene, + }) + } + } return ( - + - - + {isMe ? ( + + Edit Profile + + ) : ( + <> + {view.myState.follow ? ( + + + Following + + ) : ( + + + + Follow + + + )} + + )} + {view.isScene && + (view.myState.member || view.creator === store.me.did) ? ( + + + + ) : undefined} {view.displayName || view.handle} - - - ) - } - - // error - // = - if (view.hasError) { - return ( - - {view.error} - - ) - } - - // loaded - // = - const gradient = getGradient(view.handle) - const isMe = store.me.did === view.did - const isCreator = view.isScene && view.creator === store.me.did - let dropdownItems: DropdownItem[] | undefined - if (isCreator || isMember) { - dropdownItems = [] - if (isCreator) { - dropdownItems.push({ - label: 'Edit Profile', - onPress: onPressEditProfile, - }) - } - if (isMember) { - dropdownItems.push({ - label: 'Leave Scene...', - onPress: onPressLeaveScene, - }) - } - } - return ( - - - - - - - - {isMe ? ( - - Edit Profile - - ) : ( - <> - {view.myState.follow ? ( - - - Following - - ) : ( - - - - Follow - - - )} - - )} - {view.isScene && - (view.myState.member || view.creator === store.me.did) ? ( - - - - ) : undefined} - - - - {view.displayName || view.handle} - - - - {view.isScene ? ( - - Scene - - ) : undefined} - @{view.handle} - - - - - {view.followersCount} - - - {pluralize(view.followersCount, 'follower')} - - - {view.isUser ? ( + + {view.isScene ? ( + + Scene + + ) : undefined} + @{view.handle} + + + onPress={onPressFollowers}> - {view.followsCount} - - following - - ) : undefined} - {view.isScene ? ( - - - {view.membersCount} + {view.followersCount} - {pluralize(view.membersCount, 'member')} + {pluralize(view.followersCount, 'follower')} + {view.isUser ? ( + + + {view.followsCount} + + following + + ) : undefined} + {view.isScene ? ( + + + {view.membersCount} + + + {pluralize(view.membersCount, 'member')} + + + ) : undefined} + + + {view.postsCount} + + + {pluralize(view.postsCount, 'post')} + + + + {view.description ? ( + + ) : undefined} + {view.isScene && view.creator ? ( + + + Created by + + + ) : undefined} + {view.isScene && view.myState.member ? ( + + + You are a member + ) : undefined} - - - {view.postsCount} - - - {pluralize(view.postsCount, 'post')} - - - {view.description ? ( - - ) : undefined} - {view.isScene && view.creator ? ( - - - Created by - - - ) : undefined} - {view.isScene && view.myState.member ? ( - - - You are a member + {view.isScene && view.creator === store.me.did ? ( + + + + + Invite Members + + ) : undefined} - {view.isScene && view.creator === store.me.did ? ( - - - - - Invite Members - - - - ) : undefined} - - ) -}) + ) + }), +) const styles = StyleSheet.create({ outer: { diff --git a/src/view/com/profile/ProfileMembers.tsx b/src/view/com/profile/ProfileMembers.tsx index 251ece41ab..76e4cad06f 100644 --- a/src/view/com/profile/ProfileMembers.tsx +++ b/src/view/com/profile/ProfileMembers.tsx @@ -5,76 +5,77 @@ import {MembersViewModel, MemberItem} from '../../../state/models/members-view' import {ProfileCard} from './ProfileCard' import {ErrorMessage} from '../util/ErrorMessage' import {useStores} from '../../../state' +import {register} from 'react-native-bundle-splitter' -export const ProfileMembers = observer(function ProfileMembers({ - name, -}: { - name: string -}) { - const store = useStores() - const [view, setView] = useState() +export const ProfileMembers = register( + observer(function ProfileMembers({name}: {name: string}) { + const store = useStores() + const [view, setView] = useState() - useEffect(() => { - if (view?.params.actor === name) { - console.log('Members doing nothing') - return // no change needed? or trigger refresh? + useEffect(() => { + if (view?.params.actor === name) { + console.log('Members doing nothing') + return // no change needed? or trigger refresh? + } + console.log('Fetching members', name) + const newView = new MembersViewModel(store, {actor: name}) + setView(newView) + newView + .setup() + .catch(err => console.error('Failed to fetch members', err)) + }, [name, view?.params.actor, store]) + + const onRefresh = () => { + view?.refresh() } - console.log('Fetching members', name) - const newView = new MembersViewModel(store, {actor: name}) - setView(newView) - newView.setup().catch(err => console.error('Failed to fetch members', err)) - }, [name, view?.params.actor, store]) - const onRefresh = () => { - view?.refresh() - } + // loading + // = + if ( + !view || + (view.isLoading && !view.isRefreshing) || + view.params.actor !== name + ) { + return ( + + + + ) + } - // loading - // = - if ( - !view || - (view.isLoading && !view.isRefreshing) || - view.params.actor !== name - ) { - return ( - - - + // error + // = + if (view.hasError) { + return ( + + + + ) + } + + // loaded + // = + const renderItem = ({item}: {item: MemberItem}) => ( + ) - } - - // error - // = - if (view.hasError) { return ( - item._reactKey} + renderItem={renderItem} /> ) - } - - // loaded - // = - const renderItem = ({item}: {item: MemberItem}) => ( - - ) - return ( - - item._reactKey} - renderItem={renderItem} - /> - - ) -}) + }), +) diff --git a/src/view/com/util/DropdownBtn.tsx b/src/view/com/util/DropdownBtn.tsx index b38a6ed997..72da1b2168 100644 --- a/src/view/com/util/DropdownBtn.tsx +++ b/src/view/com/util/DropdownBtn.tsx @@ -17,6 +17,7 @@ import {toShareUrl} from '../../../lib/strings' import {useStores} from '../../../state' import {ConfirmModel} from '../../../state/models/shell-ui' import {TABS_ENABLED} from '../../../build-flags' +import {register} from 'react-native-bundle-splitter' const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} @@ -26,52 +27,54 @@ export interface DropdownItem { onPress: () => void } -export function DropdownBtn({ - style, - items, - menuWidth, - children, -}: { - style?: StyleProp - items: DropdownItem[] - menuWidth?: number - children?: React.ReactNode -}) { - const ref = useRef(null) +export const DropdownBtn = register( + ({ + style, + items, + menuWidth, + children, + }: { + style?: StyleProp + items: DropdownItem[] + menuWidth?: number + children?: React.ReactNode + }) => { + const ref = useRef(null) - const onPress = () => { - ref.current?.measure( - ( - _x: number, - _y: number, - width: number, - height: number, - pageX: number, - pageY: number, - ) => { - if (!menuWidth) { - menuWidth = 200 - } - createDropdownMenu( - pageX + width - menuWidth, - pageY + height, - menuWidth, - items, - ) - }, + const onPress = () => { + ref.current?.measure( + ( + _x: number, + _y: number, + width: number, + height: number, + pageX: number, + pageY: number, + ) => { + if (!menuWidth) { + menuWidth = 200 + } + createDropdownMenu( + pageX + width - menuWidth, + pageY + height, + menuWidth, + items, + ) + }, + ) + } + + return ( + + {children} + ) - } - - return ( - - {children} - - ) -} + }, +) export function PostDropdownBtn({ style, diff --git a/src/view/com/util/EmptyState.tsx b/src/view/com/util/EmptyState.tsx index adae9e3144..a3dd961181 100644 --- a/src/view/com/util/EmptyState.tsx +++ b/src/view/com/util/EmptyState.tsx @@ -5,28 +5,30 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {UserGroupIcon} from '../../lib/icons' import {colors} from '../../lib/styles' -export function EmptyState({ - icon, - message, - style, -}: { - icon: IconProp | 'user-group' - message: string - style?: StyleProp -}) { - return ( - - - {icon === 'user-group' ? ( - - ) : ( - - )} +export const EmptyState = register( + ({ + icon, + message, + style, + }: { + icon: IconProp | 'user-group' + message: string + style?: StyleProp + }) => { + return ( + + + {icon === 'user-group' ? ( + + ) : ( + + )} + + {message} - {message} - - ) -} + ) + }, +) const styles = StyleSheet.create({ container: { diff --git a/src/view/com/util/ErrorMessage.tsx b/src/view/com/util/ErrorMessage.tsx index 3f6522b861..be32496995 100644 --- a/src/view/com/util/ErrorMessage.tsx +++ b/src/view/com/util/ErrorMessage.tsx @@ -10,58 +10,62 @@ import { import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import LinearGradient from 'react-native-linear-gradient' import {colors, gradients} from '../../lib/styles' +import {register} from 'react-native-bundle-splitter' -export function ErrorMessage({ - message, - numberOfLines, - dark, - style, - onPressTryAgain, -}: { - message: string - numberOfLines?: number - dark?: boolean - style?: StyleProp - onPressTryAgain?: () => void -}) { - const inner = ( - <> - - - - - {message} - - {onPressTryAgain && ( - +export const ErrorMessage = register( + ({ + message, + numberOfLines, + dark, + style, + onPressTryAgain, + }: { + message: string + numberOfLines?: number + dark?: boolean + style?: StyleProp + onPressTryAgain?: () => void + }) => { + const inner = ( + <> + - - )} - - ) - if (dark) { - return ( - - {inner} - + + + {message} + + {onPressTryAgain && ( + + + + )} + ) - } - return {inner} -} + if (dark) { + return ( + + {inner} + + ) + } + return {inner} + }, +) const styles = StyleSheet.create({ outer: { diff --git a/src/view/com/util/ErrorScreen.tsx b/src/view/com/util/ErrorScreen.tsx index 4a3e41dc91..923ff19830 100644 --- a/src/view/com/util/ErrorScreen.tsx +++ b/src/view/com/util/ErrorScreen.tsx @@ -2,47 +2,50 @@ import React from 'react' import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {colors} from '../../lib/styles' +import {register} from 'react-native-bundle-splitter' -export function ErrorScreen({ - title, - message, - details, - onPressTryAgain, -}: { - title: string - message: string - details?: string - onPressTryAgain?: () => void -}) { - return ( - - - - - - - {title} - {message} - {details && {details}} - {onPressTryAgain && ( - - +export const ErrorScreen = register( + ({ + title, + message, + details, + onPressTryAgain, + }: { + title: string + message: string + details?: string + onPressTryAgain?: () => void + }) => { + return ( + + + - Try again - + - )} - - ) -} + {title} + {message} + {details && {details}} + {onPressTryAgain && ( + + + + Try again + + + )} + + ) + }, +) const styles = StyleSheet.create({ outer: { diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 70ba0df7af..139c755853 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -10,35 +10,38 @@ import { } from 'react-native' import {useStores, RootStoreModel} from '../../../state' import {convertBskyAppUrlIfNeeded} from '../../../lib/strings' +import {register} from 'react-native-bundle-splitter' -export const Link = observer(function Link({ - style, - href, - title, - children, -}: { - style?: StyleProp - href: string - title?: string - children?: React.ReactNode -}) { - const store = useStores() - const onPress = () => { - handleLink(store, href, false) - } - const onLongPress = () => { - handleLink(store, href, true) - } - return ( - - {children ? children : {title || 'link'}} - - ) -}) +export const Link = register( + observer(function Link({ + style, + href, + title, + children, + }: { + style?: StyleProp + href: string + title?: string + children?: React.ReactNode + }) { + const store = useStores() + const onPress = () => { + handleLink(store, href, false) + } + const onLongPress = () => { + handleLink(store, href, true) + } + return ( + + {children ? children : {title || 'link'}} + + ) + }), +) export const TextLink = observer(function Link({ style, diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx index 9c2d0398f3..5f4f6f999f 100644 --- a/src/view/com/util/LoadingPlaceholder.tsx +++ b/src/view/com/util/LoadingPlaceholder.tsx @@ -4,37 +4,39 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {UpIcon} from '../../lib/icons' import {s, colors} from '../../lib/styles' -export function LoadingPlaceholder({ - width, - height, - style, -}: { - width: string | number - height: string | number - style?: StyleProp -}) { - return ( - +export const LoadingPlaceholder = register( + ({ + width, + height, + style, + }: { + width: string | number + height: string | number + style?: StyleProp + }) => { + return ( - - ) -} + style={[ + { + width, + height, + backgroundColor: '#e7e9ea', + borderRadius: 6, + overflow: 'hidden', + }, + style, + ]}> + + + ) + }, +) export function PostLoadingPlaceholder({ style, @@ -63,7 +65,7 @@ export function PostLoadingPlaceholder({ - + diff --git a/src/view/com/util/Picker.tsx b/src/view/com/util/Picker.tsx index a02daef15f..97df54c4bc 100644 --- a/src/view/com/util/Picker.tsx +++ b/src/view/com/util/Picker.tsx @@ -33,45 +33,47 @@ interface PickerOpts { const MENU_WIDTH = 200 -export function Picker({ - style, - labelStyle, - iconStyle, - items, - value, - onChange, - enabled, -}: PickerOpts) { - const ref = useRef(null) - const valueLabel = items.find(item => item.value === value)?.label || value - const onPress = () => { - if (!enabled) { - return +export const Picker = register( + ({ + style, + labelStyle, + iconStyle, + items, + value, + onChange, + enabled, + }: PickerOpts) => { + const ref = useRef(null) + const valueLabel = items.find(item => item.value === value)?.label || value + const onPress = () => { + if (!enabled) { + return + } + ref.current?.measure( + ( + _x: number, + _y: number, + width: number, + height: number, + pageX: number, + pageY: number, + ) => { + createDropdownMenu(pageX, pageY + height, MENU_WIDTH, items, onChange) + }, + ) } - ref.current?.measure( - ( - _x: number, - _y: number, - width: number, - height: number, - pageX: number, - pageY: number, - ) => { - createDropdownMenu(pageX, pageY + height, MENU_WIDTH, items, onChange) - }, - ) - } - return ( - - - - {valueLabel} + return ( + + + + {valueLabel} + + - - - - ) -} + + ) + }, +) function createDropdownMenu( x: number, diff --git a/src/view/com/util/PostCtrls.tsx b/src/view/com/util/PostCtrls.tsx index b981cc1bf7..31e92aadbe 100644 --- a/src/view/com/util/PostCtrls.tsx +++ b/src/view/com/util/PostCtrls.tsx @@ -4,6 +4,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {UpIcon, UpIconSolid} from '../../lib/icons' import {s, colors} from '../../lib/styles' import {useAnimatedValue} from '../../lib/useAnimatedValue' +import {register} from 'react-native-bundle-splitter' interface PostCtrlsOpts { big?: boolean @@ -21,7 +22,7 @@ const redgray = '#7A6161' const sRedgray = {color: redgray} const HITSLOP = {top: 10, left: 10, bottom: 10, right: 10} -export function PostCtrls(opts: PostCtrlsOpts) { +export const PostCtrls = register((opts: PostCtrlsOpts) => { const interp1 = useAnimatedValue(0) const interp2 = useAnimatedValue(0) @@ -168,7 +169,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { ) -} +}) const styles = StyleSheet.create({ ctrls: { diff --git a/src/view/com/util/PostEmbeds.tsx b/src/view/com/util/PostEmbeds.tsx index 1591c658ad..5ec2b21465 100644 --- a/src/view/com/util/PostEmbeds.tsx +++ b/src/view/com/util/PostEmbeds.tsx @@ -12,60 +12,58 @@ import {Link} from '../util/Link' import {LinkMeta, getLikelyType, LikelyType} from '../../../lib/link-meta' import {colors} from '../../lib/styles' import {useStores} from '../../../state' +import {register} from 'react-native-bundle-splitter' -export function PostEmbeds({ - entities, - style, -}: { - entities?: Entity[] - style?: StyleProp -}) { - const store = useStores() - const [linkMeta, setLinkMeta] = useState(undefined) - const link = entities?.find( - ent => - ent.type === 'link' && getLikelyType(ent.value || '') === LikelyType.HTML, - ) +export const PostEmbeds = register( + ({entities, style}: {entities?: Entity[]; style?: StyleProp}) => { + const store = useStores() + const [linkMeta, setLinkMeta] = useState(undefined) + const link = entities?.find( + ent => + ent.type === 'link' && + getLikelyType(ent.value || '') === LikelyType.HTML, + ) - useEffect(() => { - let aborted = false - store.linkMetas.getLinkMeta(link?.value || '').then(linkMeta => { - if (!aborted) { - setLinkMeta(linkMeta) + useEffect(() => { + let aborted = false + store.linkMetas.getLinkMeta(link?.value || '').then(linkMeta => { + if (!aborted) { + setLinkMeta(linkMeta) + } + }) + + return () => { + aborted = true } - }) + }, [link]) - return () => { - aborted = true + if (!link) { + return } - }, [link]) - if (!link) { - return - } - - return ( - - {linkMeta ? ( - <> - - {linkMeta.title || linkMeta.url} - - - {linkMeta.url} - - {linkMeta.description ? ( - - {linkMeta.description} + return ( + + {linkMeta ? ( + <> + + {linkMeta.title || linkMeta.url} - ) : undefined} - - ) : ( - - )} - - ) -} + + {linkMeta.url} + + {linkMeta.description ? ( + + {linkMeta.description} + + ) : undefined} + + ) : ( + + )} + + ) + }, +) const styles = StyleSheet.create({ outer: { diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index 1994580c17..fdaff91275 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -5,6 +5,7 @@ import {Link} from '../util/Link' import {PostDropdownBtn} from '../util/DropdownBtn' import {s} from '../../lib/styles' import {ago} from '../../../lib/strings' +import {register} from 'react-native-bundle-splitter' interface PostMetaOpts { itemHref: string @@ -18,7 +19,7 @@ interface PostMetaOpts { onDeletePost: () => void } -export function PostMeta(opts: PostMetaOpts) { +export const PostMeta = register((opts: PostMetaOpts) => { return ( ) -} +}) const styles = StyleSheet.create({ meta: { diff --git a/src/view/com/util/RichText.tsx b/src/view/com/util/RichText.tsx index 66b0e2536f..f7cd836d77 100644 --- a/src/view/com/util/RichText.tsx +++ b/src/view/com/util/RichText.tsx @@ -3,6 +3,7 @@ import {Text, TextStyle, StyleProp} from 'react-native' import {TextLink} from './Link' import {s} from '../../lib/styles' import {toShortUrl} from '../../../lib/strings' +import {register} from 'react-native-bundle-splitter' type TextSlice = {start: number; end: number} type Entity = { @@ -11,65 +12,70 @@ type Entity = { value: string } -export function RichText({ - text, - entities, - style, - numberOfLines, -}: { - text: string - entities?: Entity[] - style?: StyleProp - numberOfLines?: number -}) { - if (!entities?.length) { - if (/^\p{Extended_Pictographic}+$/u.test(text) && text.length <= 5) { - style = { - fontSize: 26, - lineHeight: 30, +export const RichText = register( + ({ + text, + entities, + style, + numberOfLines, + }: { + text: string + entities?: Entity[] + style?: StyleProp + numberOfLines?: number + }) => { + if (!entities?.length) { + if (/^\p{Extended_Pictographic}+$/u.test(text) && text.length <= 5) { + style = { + fontSize: 26, + lineHeight: 30, + } + return {text} } return {text} } - return {text} - } - if (!style) style = [] - else if (!Array.isArray(style)) style = [style] - entities.sort(sortByIndex) - const segments = Array.from(toSegments(text, entities)) - const els = [] - let key = 0 - for (const segment of segments) { - if (typeof segment === 'string') { - els.push(segment) - } else { - if (segment.entity.type === 'mention') { - els.push( - , - ) - } else if (segment.entity.type === 'link') { - els.push( - , - ) - } + if (!style) { + style = [] + } else if (!Array.isArray(style)) { + style = [style] } - key++ - } - return ( - - {els} - - ) -} + entities.sort(sortByIndex) + const segments = Array.from(toSegments(text, entities)) + const els = [] + let key = 0 + for (const segment of segments) { + if (typeof segment === 'string') { + els.push(segment) + } else { + if (segment.entity.type === 'mention') { + els.push( + , + ) + } else if (segment.entity.type === 'link') { + els.push( + , + ) + } + } + key++ + } + return ( + + {els} + + ) + }, +) function sortByIndex(a: Entity, b: Entity) { return a.index.start - b.index.start diff --git a/src/view/com/util/Selector.tsx b/src/view/com/util/Selector.tsx index ed042d7c17..0117ea4099 100644 --- a/src/view/com/util/Selector.tsx +++ b/src/view/com/util/Selector.tsx @@ -7,100 +7,104 @@ import { View, } from 'react-native' import {colors} from '../../lib/styles' +import {register} from 'react-native-bundle-splitter' interface Layout { x: number width: number } -export function Selector({ - selectedIndex, - items, - panX, - onSelect, -}: { - selectedIndex: number - items: string[] - panX: Animated.Value - onSelect?: (index: number) => void -}) { - const [itemLayouts, setItemLayouts] = useState( - undefined, - ) - const itemRefs = useMemo( - () => Array.from({length: items.length}).map(() => createRef()), - [items.length], - ) +export const Selector = register( + ({ + selectedIndex, + items, + panX, + onSelect, + }: { + selectedIndex: number + items: string[] + panX: Animated.Value + onSelect?: (index: number) => void + }) => { + const [itemLayouts, setItemLayouts] = useState( + undefined, + ) + const itemRefs = useMemo( + () => Array.from({length: items.length}).map(() => createRef()), + [items.length], + ) - const currentLayouts = useMemo(() => { - const left = itemLayouts?.[selectedIndex - 1] || {x: 0, width: 0} - const middle = itemLayouts?.[selectedIndex] || {x: 0, width: 0} - const right = itemLayouts?.[selectedIndex + 1] || { - x: middle.x + 20, - width: middle.width, + const currentLayouts = useMemo(() => { + const left = itemLayouts?.[selectedIndex - 1] || {x: 0, width: 0} + const middle = itemLayouts?.[selectedIndex] || {x: 0, width: 0} + const right = itemLayouts?.[selectedIndex + 1] || { + x: middle.x + 20, + width: middle.width, + } + return [left, middle, right] + }, [selectedIndex, items, itemLayouts]) + + const underlineStyle = { + left: panX.interpolate({ + inputRange: [-1, 0, 1], + outputRange: [ + currentLayouts[0].x, + currentLayouts[1].x, + currentLayouts[2].x, + ], + }), + width: panX.interpolate({ + inputRange: [-1, 0, 1], + outputRange: [ + currentLayouts[0].width, + currentLayouts[1].width, + currentLayouts[2].width, + ], + }), } - return [left, middle, right] - }, [selectedIndex, items, itemLayouts]) - const underlineStyle = { - left: panX.interpolate({ - inputRange: [-1, 0, 1], - outputRange: [ - currentLayouts[0].x, - currentLayouts[1].x, - currentLayouts[2].x, - ], - }), - width: panX.interpolate({ - inputRange: [-1, 0, 1], - outputRange: [ - currentLayouts[0].width, - currentLayouts[1].width, - currentLayouts[2].width, - ], - }), - } - - const onLayout = () => { - const promises = [] - for (let i = 0; i < items.length; i++) { - promises.push( - new Promise(resolve => { - itemRefs[i].current?.measure( - (x: number, _y: number, width: number) => { - resolve({x, width}) - }, - ) - }), - ) - } - Promise.all(promises).then((layouts: Layout[]) => { - setItemLayouts(layouts) - }) - } - - const onPressItem = (index: number) => { - onSelect?.(index) - } - - return ( - - - {items.map((item, i) => { - const selected = i === selectedIndex - return ( - onPressItem(i)}> - - - {item} - - - + const onLayout = () => { + const promises = [] + for (let i = 0; i < items.length; i++) { + promises.push( + new Promise(resolve => { + itemRefs[i].current?.measure( + (x: number, _y: number, width: number) => { + resolve({x, width}) + }, + ) + }), ) - })} - - ) -} + } + Promise.all(promises).then((layouts: Layout[]) => { + setItemLayouts(layouts) + }) + } + + const onPressItem = (index: number) => { + onSelect?.(index) + } + + return ( + + + {items.map((item, i) => { + const selected = i === selectedIndex + return ( + onPressItem(i)}> + + + {item} + + + + ) + })} + + ) + }, +) const styles = StyleSheet.create({ outer: { diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index f4ac4a3220..6b9407963c 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -10,110 +10,113 @@ import { } from 'react-native-image-crop-picker' import {getGradient} from '../../lib/asset-gen' import {colors} from '../../lib/styles' +import {register} from 'react-native-bundle-splitter' -export function UserAvatar({ - size, - handle, - avatar, - displayName, - onSelectNewAvatar, -}: { - size: number - handle: string - displayName: string | undefined - avatar?: string | null - onSelectNewAvatar?: (img: PickedImage) => void -}) { - const initials = getInitials(displayName || handle) - const gradient = getGradient(handle) +export const UserAvatar = register( + ({ + size, + handle, + avatar, + displayName, + onSelectNewAvatar, + }: { + size: number + handle: string + displayName: string | undefined + avatar?: string | null + onSelectNewAvatar?: (img: PickedImage) => void + }) => { + const initials = getInitials(displayName || handle) + const gradient = getGradient(handle) - const handleEditAvatar = useCallback(() => { - Alert.alert('Select upload method', '', [ - { - text: 'Take a new photo', - onPress: () => { - openCamera({ - mediaType: 'photo', - cropping: true, - width: 400, - height: 400, - cropperCircleOverlay: true, - forceJpg: true, // ios only - compressImageQuality: 0.7, - }).then(onSelectNewAvatar) - }, - }, - { - text: 'Select from gallery', - onPress: () => { - openPicker({ - mediaType: 'photo', - }).then(async item => { - await openCropper({ + const handleEditAvatar = useCallback(() => { + Alert.alert('Select upload method', '', [ + { + text: 'Take a new photo', + onPress: () => { + openCamera({ mediaType: 'photo', - path: item.path, + cropping: true, width: 400, height: 400, cropperCircleOverlay: true, forceJpg: true, // ios only compressImageQuality: 0.7, }).then(onSelectNewAvatar) - }) + }, }, - }, - ]) - }, [onSelectNewAvatar]) + { + text: 'Select from gallery', + onPress: () => { + openPicker({ + mediaType: 'photo', + }).then(async item => { + await openCropper({ + mediaType: 'photo', + path: item.path, + width: 400, + height: 400, + cropperCircleOverlay: true, + forceJpg: true, // ios only + compressImageQuality: 0.7, + }).then(onSelectNewAvatar) + }) + }, + }, + ]) + }, [onSelectNewAvatar]) - const renderSvg = (size: number, initials: string) => ( - - - - - - - - - - {initials} - - - ) + const renderSvg = (size: number, initials: string) => ( + + + + + + + + + + {initials} + + + ) - // onSelectNewAvatar is only passed as prop on the EditProfile component - return onSelectNewAvatar ? ( - - {avatar ? ( - - ) : ( - renderSvg(size, initials) - )} - - - - - ) : avatar ? ( - - ) : ( - renderSvg(size, initials) - ) -} + // onSelectNewAvatar is only passed as prop on the EditProfile component + return onSelectNewAvatar ? ( + + {avatar ? ( + + ) : ( + renderSvg(size, initials) + )} + + + + + ) : avatar ? ( + + ) : ( + renderSvg(size, initials) + ) + }, +) function getInitials(str: string): string { const tokens = str diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index b0c6bb165c..876e9d5976 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -10,100 +10,103 @@ import { openPicker, } from 'react-native-image-crop-picker' import {IMAGES_ENABLED} from '../../../build-flags' +import {register} from 'react-native-bundle-splitter' -export function UserBanner({ - handle, - userBanner, - setUserBanner, -}: { - handle: string - userBanner?: string | null - setUserBanner?: React.Dispatch> -}) { - const gradient = getGradient(handle) +export const UserBanner = register( + ({ + handle, + userBanner, + setUserBanner, + }: { + handle: string + userBanner?: string | null + setUserBanner?: React.Dispatch> + }) => { + const gradient = getGradient(handle) - const handleEditBanner = useCallback(() => { - Alert.alert('Select upload method', '', [ - { - text: 'Take a new photo', - onPress: () => { - openCamera({ - mediaType: 'photo', - cropping: true, - width: 1500, - height: 500, - }).then(item => { - if (setUserBanner != null) { - setUserBanner(item.path) - } - }) - }, - }, - { - text: 'Select from gallery', - onPress: () => { - openPicker({ - mediaType: 'photo', - }).then(async item => { - await openCropper({ + const handleEditBanner = useCallback(() => { + Alert.alert('Select upload method', '', [ + { + text: 'Take a new photo', + onPress: () => { + openCamera({ mediaType: 'photo', - path: item.path, + cropping: true, width: 1500, height: 500, - }).then(croppedItem => { + }).then(item => { if (setUserBanner != null) { - setUserBanner(croppedItem.path) + setUserBanner(item.path) } }) - }) + }, }, - }, - ]) - }, [setUserBanner]) + { + text: 'Select from gallery', + onPress: () => { + openPicker({ + mediaType: 'photo', + }).then(async item => { + await openCropper({ + mediaType: 'photo', + path: item.path, + width: 1500, + height: 500, + }).then(croppedItem => { + if (setUserBanner != null) { + setUserBanner(croppedItem.path) + } + }) + }) + }, + }, + ]) + }, [setUserBanner]) - const renderSvg = () => ( - - - - - - - - - - - - - - - ) + const renderSvg = () => ( + + + + + + + + + + + + + + + ) - // setUserBanner is only passed as prop on the EditProfile component - return setUserBanner != null && IMAGES_ENABLED ? ( - - {userBanner ? ( - - ) : ( - renderSvg() - )} - - - - - ) : userBanner ? ( - - ) : ( - renderSvg() - ) -} + // setUserBanner is only passed as prop on the EditProfile component + return setUserBanner != null && IMAGES_ENABLED ? ( + + {userBanner ? ( + + ) : ( + renderSvg() + )} + + + + + ) : userBanner ? ( + + ) : ( + renderSvg() + ) + }, +) const styles = StyleSheet.create({ editButtonContainer: { diff --git a/src/view/com/util/UserInfoText.tsx b/src/view/com/util/UserInfoText.tsx index d1292cc703..065495c8d0 100644 --- a/src/view/com/util/UserInfoText.tsx +++ b/src/view/com/util/UserInfoText.tsx @@ -4,76 +4,83 @@ import {StyleProp, Text, TextStyle} from 'react-native' import {Link} from './Link' import {LoadingPlaceholder} from './LoadingPlaceholder' import {useStores} from '../../../state' +import {register} from 'react-native-bundle-splitter' -export function UserInfoText({ - did, - attr, - loading, - failed, - prefix, - style, - asLink, -}: { - did: string - attr?: keyof GetProfile.OutputSchema - loading?: string - failed?: string - prefix?: string - style?: StyleProp - asLink?: boolean -}) { - attr = attr || 'handle' - loading = loading || '...' - failed = failed || 'user' +export const UserInfoText = register( + ({ + did, + attr, + loading, + failed, + prefix, + style, + asLink, + }: { + did: string + attr?: keyof GetProfile.OutputSchema + loading?: string + failed?: string + prefix?: string + style?: StyleProp + asLink?: boolean + }) => { + attr = attr || 'handle' + loading = loading || '...' + failed = failed || 'user' - const store = useStores() - const [profile, setProfile] = useState( - undefined, - ) - const [didFail, setFailed] = useState(false) - - useEffect(() => { - let aborted = false - store.profiles.getProfile(did).then( - v => { - if (aborted) return - setProfile(v.data) - }, - _err => { - if (aborted) return - setFailed(true) - }, + const store = useStores() + const [profile, setProfile] = useState( + undefined, ) - return () => { - aborted = true + const [didFail, setFailed] = useState(false) + + useEffect(() => { + let aborted = false + store.profiles.getProfile(did).then( + v => { + if (aborted) { + return + } + setProfile(v.data) + }, + _err => { + if (aborted) { + return + } + setFailed(true) + }, + ) + return () => { + aborted = true + } + }, [did, store.api.app.bsky]) + + let inner + if (didFail) { + inner = {failed} + } else if (profile) { + inner = {`${prefix || ''}${profile[attr]}`} + } else { + inner = ( + + ) } - }, [did, store.api.app.bsky]) - let inner - if (didFail) { - inner = {failed} - } else if (profile) { - inner = {`${prefix || ''}${profile[attr]}`} - } else { - inner = ( - - ) - } + if (asLink) { + const title = profile?.displayName || profile?.handle || 'User' + return ( + + {inner} + + ) + } - if (asLink) { - const title = profile?.displayName || profile?.handle || 'User' - return ( - - {inner} - - ) - } - - return inner -} + return inner + }, +) diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index 2f0e504318..441111e487 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -1,5 +1,4 @@ import React from 'react' -import {observer} from 'mobx-react-lite' import { ActivityIndicator, StyleSheet, @@ -11,116 +10,121 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {s, colors} from '../../lib/styles' import {MagnifyingGlassIcon} from '../../lib/icons' import {useStores} from '../../../state' +import {register} from 'react-native-bundle-splitter' const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} const BACK_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10} -export const ViewHeader = observer(function ViewHeader({ - title, - subtitle, - onPost, -}: { - title: string - subtitle?: string - onPost?: () => void -}) { - const store = useStores() - const onPressBack = () => { - store.nav.tab.goBack() - } - const onPressMenu = () => { - store.shell.setMainMenuOpen(true) - } - const onPressCompose = () => { - store.shell.openComposer({onPost}) - } - const onPressSearch = () => { - store.nav.navigate(`/search`) - } - const onPressReconnect = () => { - store.session.connect().catch(e => { - // log for debugging but ignore otherwise - console.log(e) - }) - } - const canGoBack = store.nav.tab.canGoBack - return ( - <> - - - - - - {title} - {subtitle ? ( - - {subtitle} - - ) : undefined} - - - - - - - - - {!store.session.online ? ( - - {store.session.attemptingConnect ? ( - <> - - - Connecting... - - - ) : ( - <> - +export const ViewHeader = register( + observer( + ({ + title, + subtitle, + onPost, + }: { + title: string + subtitle?: string + onPost?: () => void + }) => { + const store = useStores() + const onPressBack = () => { + store.nav.tab.goBack() + } + const onPressMenu = () => { + store.shell.setMainMenuOpen(true) + } + const onPressCompose = () => { + store.shell.openComposer({onPost}) + } + const onPressSearch = () => { + store.nav.navigate('/search') + } + const onPressReconnect = () => { + store.session.connect().catch(e => { + // log for debugging but ignore otherwise + console.log(e) + }) + } + const canGoBack = store.nav.tab.canGoBack + return ( + <> + + - - Unable to connect - - - Try again - - - )} - - ) : undefined} - - ) -}) + + + {title} + {subtitle ? ( + + {subtitle} + + ) : undefined} + + + + + + + + + {!store.session.online ? ( + + {store.session.attemptingConnect ? ( + <> + + + Connecting... + + + ) : ( + <> + + + + Unable to connect + + + Try again + + + )} + + ) : undefined} + + ) + }, + ), +) const styles = StyleSheet.create({ header: { diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index b2d69571fd..b9e91c6c3f 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -4,92 +4,95 @@ import {Selector} from './Selector' import {HorzSwipe} from './gestures/HorzSwipe' import {useAnimatedValue} from '../../lib/useAnimatedValue' import {useStores} from '../../../state' +import {register} from 'react-native-bundle-splitter' const HEADER_ITEM = {_reactKey: '__header__'} const SELECTOR_ITEM = {_reactKey: '__selector__'} const STICKY_HEADER_INDICES = [1] -export function ViewSelector({ - sections, - items, - refreshing, - swipeEnabled, - renderHeader, - renderItem, - onSelectView, - onRefresh, - onEndReached, -}: { - sections: string[] - items: any[] - refreshing?: boolean - swipeEnabled?: boolean - renderHeader?: () => JSX.Element - renderItem: (item: any) => JSX.Element - onSelectView?: (viewIndex: number) => void - onRefresh?: () => void - onEndReached?: (info: {distanceFromEnd: number}) => void -}) { - const store = useStores() - const [selectedIndex, setSelectedIndex] = useState(0) - const panX = useAnimatedValue(0) +export const ViewSelector = register( + ({ + sections, + items, + refreshing, + swipeEnabled, + renderHeader, + renderItem, + onSelectView, + onRefresh, + onEndReached, + }: { + sections: string[] + items: any[] + refreshing?: boolean + swipeEnabled?: boolean + renderHeader?: () => JSX.Element + renderItem: (item: any) => JSX.Element + onSelectView?: (viewIndex: number) => void + onRefresh?: () => void + onEndReached?: (info: {distanceFromEnd: number}) => void + }) => { + const store = useStores() + const [selectedIndex, setSelectedIndex] = useState(0) + const panX = useAnimatedValue(0) - // events - // = + // events + // = - const onSwipeEnd = (dx: number) => { - if (dx !== 0) { - setSelectedIndex(selectedIndex + dx) - } - } - const onPressSelection = (index: number) => setSelectedIndex(index) - useEffect(() => { - onSelectView?.(selectedIndex) - }, [selectedIndex]) - - // rendering - // = - - const renderItemInternal = ({item}: {item: any}) => { - if (item === HEADER_ITEM) { - if (renderHeader) { - return renderHeader() + const onSwipeEnd = (dx: number) => { + if (dx !== 0) { + setSelectedIndex(selectedIndex + dx) } - return - } else if (item === SELECTOR_ITEM) { - return ( - - ) - } else { - return renderItem(item) } - } + const onPressSelection = (index: number) => setSelectedIndex(index) + useEffect(() => { + onSelectView?.(selectedIndex) + }, [selectedIndex]) - const data = [HEADER_ITEM, SELECTOR_ITEM, ...items] - return ( - 0} - canSwipeRight={selectedIndex < sections.length - 1} - onSwipeEnd={onSwipeEnd}> - item._reactKey} - renderItem={renderItemInternal} - stickyHeaderIndices={STICKY_HEADER_INDICES} - refreshing={refreshing} - onRefresh={onRefresh} - onEndReached={onEndReached} - /> - - ) -} + // rendering + // = + + const renderItemInternal = ({item}: {item: any}) => { + if (item === HEADER_ITEM) { + if (renderHeader) { + return renderHeader() + } + return + } else if (item === SELECTOR_ITEM) { + return ( + + ) + } else { + return renderItem(item) + } + } + + const data = [HEADER_ITEM, SELECTOR_ITEM, ...items] + return ( + 0} + canSwipeRight={selectedIndex < sections.length - 1} + onSwipeEnd={onSwipeEnd}> + item._reactKey} + renderItem={renderItemInternal} + stickyHeaderIndices={STICKY_HEADER_INDICES} + refreshing={refreshing} + onRefresh={onRefresh} + onEndReached={onEndReached} + /> + + ) + }, +) const styles = StyleSheet.create({}) diff --git a/src/view/screens/Contacts.tsx b/src/view/screens/Contacts.tsx index be92f74c92..98de9d8c81 100644 --- a/src/view/screens/Contacts.tsx +++ b/src/view/screens/Contacts.tsx @@ -7,8 +7,9 @@ import {colors} from '../lib/styles' import {ScreenParams} from '../routes' import {useStores} from '../../state' import {useAnimatedValue} from '../lib/useAnimatedValue' +import {register} from 'react-native-bundle-splitter' -export const Contacts = ({navIdx, visible, params}: ScreenParams) => { +export const Contacts = register(({navIdx, visible, params}: ScreenParams) => { const store = useStores() const selectorInterp = useAnimatedValue(0) @@ -50,7 +51,7 @@ export const Contacts = ({navIdx, visible, params}: ScreenParams) => { {!!store.me.handle && } ) -} +}) const styles = StyleSheet.create({ section: { diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index a1d2027385..05bb45c234 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -9,105 +9,106 @@ import {useStores} from '../../state' import {FeedModel} from '../../state/models/feed-view' import {ScreenParams} from '../routes' import {s, colors} from '../lib/styles' +import {register} from 'react-native-bundle-splitter' const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20} -export const Home = observer(function Home({ - navIdx, - visible, - scrollElRef, -}: ScreenParams) { - const store = useStores() - const [hasSetup, setHasSetup] = useState(false) - const {appState} = useAppState({ - onForeground: () => doPoll(true), - }) - const defaultFeedView = useMemo( - () => - new FeedModel(store, 'home', { - algorithm: 'reverse-chronological', - }), - [store], - ) - - const doPoll = (knownActive = false) => { - if ((!knownActive && appState !== 'active') || !visible) { - return - } - if (defaultFeedView.isLoading) { - return - } - console.log('Polling home feed') - defaultFeedView.checkForLatest().catch(e => { - console.error('Failed to poll feed', e) +export const Home = register( + observer(({navIdx, visible, scrollElRef}: ScreenParams) => { + const store = useStores() + const [hasSetup, setHasSetup] = useState(false) + const {appState} = useAppState({ + onForeground: () => doPoll(true), }) - } + const defaultFeedView = useMemo( + () => + new FeedModel(store, 'home', { + algorithm: 'reverse-chronological', + }), + [store], + ) - useEffect(() => { - let aborted = false - const pollInterval = setInterval(() => doPoll(), 15e3) - if (!visible) { - return - } - - if (hasSetup) { - console.log('Updating home feed') - defaultFeedView.update() - } else { - store.nav.setTitle(navIdx, 'Home') - console.log('Fetching home feed') - defaultFeedView.setup().then(() => { - if (aborted) return - setHasSetup(true) + const doPoll = (knownActive = false) => { + if ((!knownActive && appState !== 'active') || !visible) { + return + } + if (defaultFeedView.isLoading) { + return + } + console.log('Polling home feed') + defaultFeedView.checkForLatest().catch(e => { + console.error('Failed to poll feed', e) }) } - return () => { - clearInterval(pollInterval) - aborted = true + + useEffect(() => { + let aborted = false + const pollInterval = setInterval(() => doPoll(), 15e3) + if (!visible) { + return + } + + if (hasSetup) { + console.log('Updating home feed') + defaultFeedView.update() + } else { + store.nav.setTitle(navIdx, 'Home') + console.log('Fetching home feed') + defaultFeedView.setup().then(() => { + if (aborted) { + return + } + setHasSetup(true) + }) + } + return () => { + clearInterval(pollInterval) + aborted = true + } + }, [visible, store]) + + const onPressCompose = () => { + store.shell.openComposer({onPost: onCreatePost}) + } + const onCreatePost = () => { + defaultFeedView.loadLatest() + } + const onPressTryAgain = () => { + defaultFeedView.refresh() + } + const onPressLoadLatest = () => { + defaultFeedView.refresh() + scrollElRef?.current?.scrollToOffset({offset: 0}) } - }, [visible, store]) - const onPressCompose = () => { - store.shell.openComposer({onPost: onCreatePost}) - } - const onCreatePost = () => { - defaultFeedView.loadLatest() - } - const onPressTryAgain = () => { - defaultFeedView.refresh() - } - const onPressLoadLatest = () => { - defaultFeedView.refresh() - scrollElRef?.current?.scrollToOffset({offset: 0}) - } - - return ( - - - - {defaultFeedView.hasNewLatest ? ( - - - Load new posts - - ) : undefined} - - ) -}) + return ( + + + + {defaultFeedView.hasNewLatest ? ( + + + Load new posts + + ) : undefined} + + ) + }), +) const styles = StyleSheet.create({ loadLatest: { diff --git a/src/view/screens/Login.tsx b/src/view/screens/Login.tsx index 38c2c1c79d..bfa641a49b 100644 --- a/src/view/screens/Login.tsx +++ b/src/view/screens/Login.tsx @@ -27,6 +27,7 @@ import {ServiceDescription} from '../../state/models/session' import {ServerInputModel} from '../../state/models/shell-ui' import {ComAtprotoAccountCreate} from '../../third-party/api/index' import {isNetworkError} from '../../lib/errors' +import {investigate} from 'react-native-bundle-splitter/dist/utils' enum ScreenState { SigninOrCreateAccount, @@ -589,6 +590,12 @@ export const Login = observer( ScreenState.SigninOrCreateAccount, ) + console.log( + `loaded: ${investigate().loaded.length} \n waiting: ${ + investigate().waiting.length + }`, + ) + return ( {screenState === ScreenState.SigninOrCreateAccount ? ( diff --git a/src/view/screens/NotFound.tsx b/src/view/screens/NotFound.tsx index 6b196500d5..143f82b047 100644 --- a/src/view/screens/NotFound.tsx +++ b/src/view/screens/NotFound.tsx @@ -2,8 +2,9 @@ import React from 'react' import {Text, Button, View} from 'react-native' import {ViewHeader} from '../com/util/ViewHeader' import {useStores} from '../../state' +import {register} from 'react-native-bundle-splitter' -export const NotFound = () => { +export const NotFound = register(() => { const stores = useStores() return ( @@ -19,4 +20,4 @@ export const NotFound = () => { ) -} +}) diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index b168ffaff4..75d42cc192 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -5,8 +5,9 @@ import {Feed} from '../com/notifications/Feed' import {useStores} from '../../state' import {NotificationsViewModel} from '../../state/models/notifications-view' import {ScreenParams} from '../routes' +import {register} from 'react-native-bundle-splitter' -export const Notifications = ({navIdx, visible}: ScreenParams) => { +export const Notifications = register(({navIdx, visible}: ScreenParams) => { const store = useStores() useEffect(() => { @@ -36,4 +37,4 @@ export const Notifications = ({navIdx, visible}: ScreenParams) => { ) -} +}) diff --git a/src/view/screens/Onboard.tsx b/src/view/screens/Onboard.tsx index 4aa0e6cac1..c12406d35a 100644 --- a/src/view/screens/Onboard.tsx +++ b/src/view/screens/Onboard.tsx @@ -5,29 +5,32 @@ import {FeatureExplainer} from '../com/onboard/FeatureExplainer' import {Follows} from '../com/onboard/Follows' import {OnboardStage, OnboardStageOrder} from '../../state/models/onboard' import {useStores} from '../../state' +import {register} from 'react-native-bundle-splitter' -export const Onboard = observer(() => { - const store = useStores() +export const Onboard = register( + 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() + 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 } - }, [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 ( - - - - ) -}) + return ( + + + + ) + }), +) diff --git a/src/view/screens/PostDownvotedBy.tsx b/src/view/screens/PostDownvotedBy.tsx index b16ec5c0a8..718ab60ac7 100644 --- a/src/view/screens/PostDownvotedBy.tsx +++ b/src/view/screens/PostDownvotedBy.tsx @@ -5,22 +5,25 @@ import {PostVotedBy as PostLikedByComponent} from '../com/post-thread/PostVotedB import {ScreenParams} from '../routes' import {useStores} from '../../state' import {makeRecordUri} from '../../lib/strings' +import {register} from 'react-native-bundle-splitter' -export const PostDownvotedBy = ({navIdx, visible, params}: ScreenParams) => { - const store = useStores() - const {name, rkey} = params - const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) +export const PostDownvotedBy = register( + ({navIdx, visible, params}: ScreenParams) => { + const store = useStores() + const {name, rkey} = params + const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) - useEffect(() => { - if (visible) { - store.nav.setTitle(navIdx, 'Downvoted by') - } - }, [store, visible]) + useEffect(() => { + if (visible) { + store.nav.setTitle(navIdx, 'Downvoted by') + } + }, [store, visible]) - return ( - - - - - ) -} + return ( + + + + + ) + }, +) diff --git a/src/view/screens/PostRepostedBy.tsx b/src/view/screens/PostRepostedBy.tsx index d8e4b910ec..90dc1fa034 100644 --- a/src/view/screens/PostRepostedBy.tsx +++ b/src/view/screens/PostRepostedBy.tsx @@ -5,22 +5,25 @@ import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/Post import {ScreenParams} from '../routes' import {useStores} from '../../state' import {makeRecordUri} from '../../lib/strings' +import {register} from 'react-native-bundle-splitter' -export const PostRepostedBy = ({navIdx, visible, params}: ScreenParams) => { - const store = useStores() - const {name, rkey} = params - const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) +export const PostRepostedBy = register( + ({navIdx, visible, params}: ScreenParams) => { + const store = useStores() + const {name, rkey} = params + const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) - useEffect(() => { - if (visible) { - store.nav.setTitle(navIdx, 'Reposted by') - } - }, [store, visible]) + useEffect(() => { + if (visible) { + store.nav.setTitle(navIdx, 'Reposted by') + } + }, [store, visible]) - return ( - - - - - ) -} + return ( + + + + + ) + }, +) diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx index 1e63ac390d..b3870e4b15 100644 --- a/src/view/screens/PostThread.tsx +++ b/src/view/screens/PostThread.tsx @@ -6,53 +6,56 @@ import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread' import {PostThreadViewModel} from '../../state/models/post-thread-view' import {ScreenParams} from '../routes' import {useStores} from '../../state' +import {register} from 'react-native-bundle-splitter' -export const PostThread = ({navIdx, visible, params}: ScreenParams) => { - const store = useStores() - const {name, rkey} = params - const [viewSubtitle, setViewSubtitle] = useState(`by ${name}`) - const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) - const view = useMemo( - () => new PostThreadViewModel(store, {uri}), - [uri], - ) +export const PostThread = register( + ({navIdx, visible, params}: ScreenParams) => { + const store = useStores() + const {name, rkey} = params + const [viewSubtitle, setViewSubtitle] = useState(`by ${name}`) + const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) + const view = useMemo( + () => new PostThreadViewModel(store, {uri}), + [uri], + ) - const setTitle = () => { - const author = view.thread?.author - const niceName = author?.handle || name - setViewSubtitle(`by ${niceName}`) - store.nav.setTitle(navIdx, `Post by ${niceName}`) - } - useEffect(() => { - let aborted = false - if (!visible) { - return + const setTitle = () => { + const author = view.thread?.author + const niceName = author?.handle || name + setViewSubtitle(`by ${niceName}`) + store.nav.setTitle(navIdx, `Post by ${niceName}`) } - setTitle() - if (!view.hasLoaded && !view.isLoading) { - console.log('Fetching post thread', uri) - view.setup().then( - () => { - if (!aborted) { - setTitle() - } - }, - err => { - console.error('Failed to fetch thread', err) - }, - ) - } - return () => { - aborted = true - } - }, [visible, store.nav, name]) + useEffect(() => { + let aborted = false + if (!visible) { + return + } + setTitle() + if (!view.hasLoaded && !view.isLoading) { + console.log('Fetching post thread', uri) + view.setup().then( + () => { + if (!aborted) { + setTitle() + } + }, + err => { + console.error('Failed to fetch thread', err) + }, + ) + } + return () => { + aborted = true + } + }, [visible, store.nav, name]) - return ( - - + return ( - + + + + - - ) -} + ) + }, +) diff --git a/src/view/screens/PostUpvotedBy.tsx b/src/view/screens/PostUpvotedBy.tsx index 2794d529a3..b31e58b75a 100644 --- a/src/view/screens/PostUpvotedBy.tsx +++ b/src/view/screens/PostUpvotedBy.tsx @@ -5,22 +5,25 @@ import {PostVotedBy as PostLikedByComponent} from '../com/post-thread/PostVotedB import {ScreenParams} from '../routes' import {useStores} from '../../state' import {makeRecordUri} from '../../lib/strings' +import {register} from 'react-native-bundle-splitter' -export const PostUpvotedBy = ({navIdx, visible, params}: ScreenParams) => { - const store = useStores() - const {name, rkey} = params - const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) +export const PostUpvotedBy = register( + ({navIdx, visible, params}: ScreenParams) => { + const store = useStores() + const {name, rkey} = params + const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) - useEffect(() => { - if (visible) { - store.nav.setTitle(navIdx, 'Upvoted by') - } - }, [store, visible]) + useEffect(() => { + if (visible) { + store.nav.setTitle(navIdx, 'Upvoted by') + } + }, [store, visible]) - return ( - - - - - ) -} + return ( + + + + + ) + }, +) diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 1a52ded844..802300a48a 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -18,243 +18,248 @@ import {EmptyState} from '../com/util/EmptyState' import {ViewHeader} from '../com/util/ViewHeader' import * as Toast from '../com/util/Toast' import {s, colors} from '../lib/styles' +import {register} from 'react-native-bundle-splitter' const LOADING_ITEM = {_reactKey: '__loading__'} const END_ITEM = {_reactKey: '__end__'} const EMPTY_ITEM = {_reactKey: '__empty__'} -export const Profile = observer(({navIdx, visible, params}: ScreenParams) => { - const store = useStores() - const [hasSetup, setHasSetup] = useState(false) - const uiState = useMemo( - () => new ProfileUiModel(store, {user: params.name}), - [params.user], - ) - - useEffect(() => { - let aborted = false - if (!visible) { - return - } - if (hasSetup) { - console.log('Updating profile for', params.name) - uiState.update() - } else { - console.log('Fetching profile for', params.name) - store.nav.setTitle(navIdx, params.name) - uiState.setup().then(() => { - if (aborted) return - setHasSetup(true) - }) - } - return () => { - aborted = true - } - }, [visible, params.name, store]) - - // events - // = - - const onSelectView = (index: number) => { - uiState.setSelectedViewIndex(index) - } - const onRefresh = () => { - uiState - .refresh() - .catch((err: any) => console.error('Failed to refresh', err)) - } - const onEndReached = () => { - uiState - .loadMore() - .catch((err: any) => console.error('Failed to load more', err)) - } - const onPressTryAgain = () => { - uiState.setup() - } - const onPressRemoveMember = (membership: MembershipItem) => { - store.shell.openModal( - new ConfirmModel( - `Remove ${membership.displayName || membership.handle}?`, - `You'll be able to invite them again if you change your mind.`, - async () => { - await uiState.members.removeMember(membership.did) - Toast.show(`User removed`) - }, - ), +export const Profile = register( + observer(({navIdx, visible, params}: ScreenParams) => { + const store = useStores() + const [hasSetup, setHasSetup] = useState(false) + const uiState = useMemo( + () => new ProfileUiModel(store, {user: params.name}), + [params.user], ) - } - // rendering - // = + useEffect(() => { + let aborted = false + if (!visible) { + return + } + if (hasSetup) { + console.log('Updating profile for', params.name) + uiState.update() + } else { + console.log('Fetching profile for', params.name) + store.nav.setTitle(navIdx, params.name) + uiState.setup().then(() => { + if (aborted) { + return + } + setHasSetup(true) + }) + } + return () => { + aborted = true + } + }, [visible, params.name, store]) - const isSceneCreator = - uiState.isScene && store.me.did === uiState.profile.creator + // events + // = - const renderHeader = () => { - if (!uiState) { - return + const onSelectView = (index: number) => { + uiState.setSelectedViewIndex(index) } - return - } - let renderItem - let items: any[] = [] - if (uiState) { - if (uiState.isInitialLoading) { - items.push(LOADING_ITEM) - renderItem = () => - } else if (uiState.currentView.hasError) { - items.push({ - _reactKey: '__error__', - error: uiState.currentView.error, - }) - renderItem = (item: any) => ( - - - + const onRefresh = () => { + uiState + .refresh() + .catch((err: any) => console.error('Failed to refresh', err)) + } + const onEndReached = () => { + uiState + .loadMore() + .catch((err: any) => console.error('Failed to load more', err)) + } + const onPressTryAgain = () => { + uiState.setup() + } + const onPressRemoveMember = (membership: MembershipItem) => { + store.shell.openModal( + new ConfirmModel( + `Remove ${membership.displayName || membership.handle}?`, + "You'll be able to invite them again if you change your mind.", + async () => { + await uiState.members.removeMember(membership.did) + Toast.show('User removed') + }, + ), ) - } else { - if ( - uiState.selectedView === Sections.Posts || - uiState.selectedView === Sections.PostsWithReplies || - uiState.selectedView === Sections.Trending - ) { - if (uiState.feed.hasContent) { - if (uiState.selectedView === Sections.Posts) { - items = uiState.feed.nonReplyFeed - } else { - items = uiState.feed.feed.slice() - } - if (!uiState.feed.hasMore) { - items.push(END_ITEM) - } - renderItem = (item: any) => { - if (item === END_ITEM) { - return - end of feed - + } + + // rendering + // = + + const isSceneCreator = + uiState.isScene && store.me.did === uiState.profile.creator + + const renderHeader = () => { + if (!uiState) { + return + } + return + } + let renderItem + let items: any[] = [] + if (uiState) { + if (uiState.isInitialLoading) { + items.push(LOADING_ITEM) + renderItem = () => + } else if (uiState.currentView.hasError) { + items.push({ + _reactKey: '__error__', + error: uiState.currentView.error, + }) + renderItem = (item: any) => ( + + + + ) + } else { + if ( + uiState.selectedView === Sections.Posts || + uiState.selectedView === Sections.PostsWithReplies || + uiState.selectedView === Sections.Trending + ) { + if (uiState.feed.hasContent) { + if (uiState.selectedView === Sections.Posts) { + items = uiState.feed.nonReplyFeed + } else { + items = uiState.feed.feed.slice() + } + if (!uiState.feed.hasMore) { + items.push(END_ITEM) + } + renderItem = (item: any) => { + if (item === END_ITEM) { + return - end of feed - + } + return + } + } else if (uiState.feed.isEmpty) { + items.push(EMPTY_ITEM) + if (uiState.profile.isScene) { + renderItem = () => ( + + ) + } else { + renderItem = () => ( + + ) } - return } - } else if (uiState.feed.isEmpty) { - items.push(EMPTY_ITEM) - if (uiState.profile.isScene) { + } else if (uiState.selectedView === Sections.Scenes) { + if (uiState.memberships.hasContent) { + items = uiState.memberships.memberships.slice() + renderItem = (item: any) => { + return ( + + ) + } + } else if (uiState.memberships.isEmpty) { + items.push(EMPTY_ITEM) renderItem = () => ( ) - } else { + } + } else if (uiState.selectedView === Sections.Members) { + if (uiState.members.hasContent) { + items = uiState.members.members.slice() + renderItem = (item: any) => { + const shouldAdmin = isSceneCreator && item.did !== store.me.did + const renderButton = shouldAdmin + ? () => ( + <> + + Remove + + ) + : undefined + return ( + onPressRemoveMember(item)} + /> + ) + } + } else if (uiState.members.isEmpty) { + items.push(EMPTY_ITEM) renderItem = () => ( ) } - } - } else if (uiState.selectedView === Sections.Scenes) { - if (uiState.memberships.hasContent) { - items = uiState.memberships.memberships.slice() - renderItem = (item: any) => { - return ( - - ) - } - } else if (uiState.memberships.isEmpty) { + } else { items.push(EMPTY_ITEM) - renderItem = () => ( - - ) + renderItem = () => TODO } - } else if (uiState.selectedView === Sections.Members) { - if (uiState.members.hasContent) { - items = uiState.members.members.slice() - renderItem = (item: any) => { - const shouldAdmin = isSceneCreator && item.did !== store.me.did - const renderButton = shouldAdmin - ? () => ( - <> - - Remove - - ) - : undefined - return ( - onPressRemoveMember(item)} - /> - ) - } - } else if (uiState.members.isEmpty) { - items.push(EMPTY_ITEM) - renderItem = () => ( - - ) - } - } else { - items.push(EMPTY_ITEM) - renderItem = () => TODO } } - } - if (!renderItem) { - renderItem = () => - } + if (!renderItem) { + renderItem = () => + } - const title = - uiState.profile.displayName || uiState.profile.handle || params.name - return ( - - - {uiState.profile.hasError ? ( - - ) : uiState.profile.hasLoaded ? ( - - ) : ( - renderHeader() - )} - - ) -}) + const title = + uiState.profile.displayName || uiState.profile.handle || params.name + return ( + + + {uiState.profile.hasError ? ( + + ) : uiState.profile.hasLoaded ? ( + + ) : ( + renderHeader() + )} + + ) + }), +) const styles = StyleSheet.create({ container: { diff --git a/src/view/screens/ProfileFollowers.tsx b/src/view/screens/ProfileFollowers.tsx index b19a5bc37c..8336baa9b5 100644 --- a/src/view/screens/ProfileFollowers.tsx +++ b/src/view/screens/ProfileFollowers.tsx @@ -4,21 +4,24 @@ import {ViewHeader} from '../com/util/ViewHeader' import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/ProfileFollowers' import {ScreenParams} from '../routes' import {useStores} from '../../state' +import {register} from 'react-native-bundle-splitter' -export const ProfileFollowers = ({navIdx, visible, params}: ScreenParams) => { - const store = useStores() - const {name} = params +export const ProfileFollowers = register( + ({navIdx, visible, params}: ScreenParams) => { + const store = useStores() + const {name} = params - useEffect(() => { - if (visible) { - store.nav.setTitle(navIdx, `Followers of ${name}`) - } - }, [store, visible, name]) + useEffect(() => { + if (visible) { + store.nav.setTitle(navIdx, `Followers of ${name}`) + } + }, [store, visible, name]) - return ( - - - - - ) -} + return ( + + + + + ) + }, +) diff --git a/src/view/screens/ProfileFollows.tsx b/src/view/screens/ProfileFollows.tsx index e54b562e7b..faa11bdb64 100644 --- a/src/view/screens/ProfileFollows.tsx +++ b/src/view/screens/ProfileFollows.tsx @@ -4,21 +4,24 @@ import {ViewHeader} from '../com/util/ViewHeader' import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows' import {ScreenParams} from '../routes' import {useStores} from '../../state' +import {register} from 'react-native-bundle-splitter' -export const ProfileFollows = ({navIdx, visible, params}: ScreenParams) => { - const store = useStores() - const {name} = params +export const ProfileFollows = register( + ({navIdx, visible, params}: ScreenParams) => { + const store = useStores() + const {name} = params - useEffect(() => { - if (visible) { - store.nav.setTitle(navIdx, `Followed by ${name}`) - } - }, [store, visible, name]) + useEffect(() => { + if (visible) { + store.nav.setTitle(navIdx, `Followed by ${name}`) + } + }, [store, visible, name]) - return ( - - - - - ) -} + return ( + + + + + ) + }, +) diff --git a/src/view/screens/ProfileMembers.tsx b/src/view/screens/ProfileMembers.tsx index b4b6c7e508..4122a6ec10 100644 --- a/src/view/screens/ProfileMembers.tsx +++ b/src/view/screens/ProfileMembers.tsx @@ -4,21 +4,24 @@ import {ViewHeader} from '../com/util/ViewHeader' import {ProfileMembers as ProfileMembersComponent} from '../com/profile/ProfileMembers' import {ScreenParams} from '../routes' import {useStores} from '../../state' +import {register} from 'react-native-bundle-splitter' -export const ProfileMembers = ({navIdx, visible, params}: ScreenParams) => { - const store = useStores() - const {name} = params +export const ProfileMembers = register( + ({navIdx, visible, params}: ScreenParams) => { + const store = useStores() + const {name} = params - useEffect(() => { - if (visible) { - store.nav.setTitle(navIdx, `Members of ${name}`) - } - }, [store, visible, name]) + useEffect(() => { + if (visible) { + store.nav.setTitle(navIdx, `Members of ${name}`) + } + }, [store, visible, name]) - return ( - - - - - ) -} + return ( + + + + + ) + }, +) diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx index 1cc0a5fe9c..20eb0bae57 100644 --- a/src/view/screens/Search.tsx +++ b/src/view/screens/Search.tsx @@ -16,8 +16,9 @@ import {useStores} from '../../state' import {UserAutocompleteViewModel} from '../../state/models/user-autocomplete-view' import {s, colors} from '../lib/styles' import {MagnifyingGlassIcon} from '../lib/icons' +import {register} from 'react-native-bundle-splitter' -export const Search = ({navIdx, visible, params}: ScreenParams) => { +export const Search = register(({navIdx, visible, params}: ScreenParams) => { const store = useStores() const textInput = useRef(null) const [query, setQuery] = useState('') @@ -92,7 +93,7 @@ export const Search = ({navIdx, visible, params}: ScreenParams) => { ) -} +}) const styles = StyleSheet.create({ container: { diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 599bb57931..7d2287ea54 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -7,55 +7,55 @@ import {s, colors} from '../lib/styles' import {ViewHeader} from '../com/util/ViewHeader' import {Link} from '../com/util/Link' import {UserAvatar} from '../com/util/UserAvatar' +import {register} from 'react-native-bundle-splitter' -export const Settings = observer(function Settings({ - navIdx, - visible, -}: ScreenParams) { - const store = useStores() +export const Settings = register( + observer(function Settings({navIdx, visible}: ScreenParams) { + const store = useStores() - useEffect(() => { - if (!visible) { - return + useEffect(() => { + if (!visible) { + return + } + store.nav.setTitle(navIdx, 'Settings') + }, [visible, store]) + + const onPressSignout = () => { + store.session.logout() } - store.nav.setTitle(navIdx, 'Settings') - }, [visible, store]) - const onPressSignout = () => { - store.session.logout() - } - - return ( - - - - - Signed in as - - - Sign out - - - - - - - - {store.me.displayName || store.me.handle} - - @{store.me.handle} - + return ( + + + + + Signed in as + + + Sign out + - + + + + + + {store.me.displayName || store.me.handle} + + @{store.me.handle} + + + + - - ) -}) + ) + }), +) const styles = StyleSheet.create({ title: { diff --git a/src/view/shell/mobile/Composer.tsx b/src/view/shell/mobile/Composer.tsx index d72311ea97..c54504f61d 100644 --- a/src/view/shell/mobile/Composer.tsx +++ b/src/view/shell/mobile/Composer.tsx @@ -4,62 +4,65 @@ import {Animated, Easing, StyleSheet, View} from 'react-native' import {ComposePost} from '../../com/composer/ComposePost' import {ComposerOpts} from '../../../state/models/shell-ui' import {useAnimatedValue} from '../../lib/useAnimatedValue' +import {register} from 'react-native-bundle-splitter' -export const Composer = observer( - ({ - active, - winHeight, - replyTo, - onPost, - onClose, - }: { - active: boolean - winHeight: number - replyTo?: ComposerOpts['replyTo'] - onPost?: ComposerOpts['onPost'] - onClose: () => void - }) => { - const initInterp = useAnimatedValue(0) +export const Composer = register( + observer( + ({ + active, + winHeight, + replyTo, + onPost, + onClose, + }: { + active: boolean + winHeight: number + replyTo?: ComposerOpts['replyTo'] + onPost?: ComposerOpts['onPost'] + onClose: () => void + }) => { + const initInterp = useAnimatedValue(0) - useEffect(() => { - if (active) { - Animated.timing(initInterp, { - toValue: 1, - duration: 300, - easing: Easing.out(Easing.exp), - useNativeDriver: true, - }).start() - } else { - initInterp.setValue(0) + useEffect(() => { + if (active) { + Animated.timing(initInterp, { + toValue: 1, + duration: 300, + easing: Easing.out(Easing.exp), + useNativeDriver: true, + }).start() + } else { + initInterp.setValue(0) + } + }, [initInterp, active]) + const wrapperAnimStyle = { + transform: [ + { + translateY: initInterp.interpolate({ + inputRange: [0, 1], + outputRange: [winHeight, 0], + }), + }, + ], } - }, [initInterp, active]) - const wrapperAnimStyle = { - transform: [ - { - translateY: initInterp.interpolate({ - inputRange: [0, 1], - outputRange: [winHeight, 0], - }), - }, - ], - } - // events - // = + // events + // = - // rendering - // = + // rendering + // = - if (!active) { - return - } + if (!active) { + return + } - return ( - - - - ) - }, + return ( + + + + ) + }, + ), ) const styles = StyleSheet.create({ diff --git a/yarn.lock b/yarn.lock index 8fd48e9f94..3dc28acea0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10163,6 +10163,11 @@ react-native-appstate-hook@^1.0.6: resolved "https://registry.yarnpkg.com/react-native-appstate-hook/-/react-native-appstate-hook-1.0.6.tgz#cbc16e7b89cfaea034cabd999f00e99053cabd06" integrity sha512-0hPVyf5yLxCSVrrNEuGqN1ZnSSj3Ye2gZex0NtcK/AHYwMc0rXWFNZjBKOoZSouspqu3hXBbQ6NOUSTzrME1AQ== +react-native-bundle-splitter@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/react-native-bundle-splitter/-/react-native-bundle-splitter-2.2.3.tgz#aff144fb1b7c9dbd2da334b318b86032869a7755" + integrity sha512-cxKb8/NUKTqAZ+nHp9PoTdRCI6+tWlUUP0Mq0UKgpVbD0KaxRIXnvdSM8Fp9ctiMyB1rk6izZzQkTJ+GcDXQLA== + react-native-codegen@^0.0.17: version "0.0.17" resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.0.17.tgz#83fb814d94061cbd46667f510d2ddba35ffb50ac"