using register
This commit is contained in:
@@ -34,6 +34,7 @@
|
|||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-native": "0.68.2",
|
"react-native": "0.68.2",
|
||||||
"react-native-appstate-hook": "^1.0.6",
|
"react-native-appstate-hook": "^1.0.6",
|
||||||
|
"react-native-bundle-splitter": "^2.2.3",
|
||||||
"react-native-gesture-handler": "^2.5.0",
|
"react-native-gesture-handler": "^2.5.0",
|
||||||
"react-native-image-crop-picker": "^0.38.1",
|
"react-native-image-crop-picker": "^0.38.1",
|
||||||
"react-native-inappbrowser-reborn": "^3.6.3",
|
"react-native-inappbrowser-reborn": "^3.6.3",
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ import {
|
|||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {useAnimatedValue} from '../../lib/useAnimatedValue'
|
import {useAnimatedValue} from '../../lib/useAnimatedValue'
|
||||||
import {colors} from '../../lib/styles'
|
import {colors} from '../../lib/styles'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
interface AutocompleteItem {
|
interface AutocompleteItem {
|
||||||
handle: string
|
handle: string
|
||||||
displayName?: string
|
displayName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Autocomplete({
|
export const Autocomplete = register(function Autocomplete({
|
||||||
active,
|
active,
|
||||||
items,
|
items,
|
||||||
onSelect,
|
onSelect,
|
||||||
@@ -52,7 +53,7 @@ export function Autocomplete({
|
|||||||
))}
|
))}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
outer: {
|
outer: {
|
||||||
|
|||||||
@@ -30,264 +30,268 @@ import {UserLocalPhotosModel} from '../../../state/models/user-local-photos'
|
|||||||
import {PhotoCarouselPicker} from './PhotoCarouselPicker'
|
import {PhotoCarouselPicker} from './PhotoCarouselPicker'
|
||||||
import {SelectedPhoto} from './SelectedPhoto'
|
import {SelectedPhoto} from './SelectedPhoto'
|
||||||
import {IMAGES_ENABLED} from '../../../build-flags'
|
import {IMAGES_ENABLED} from '../../../build-flags'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
const MAX_TEXT_LENGTH = 256
|
const MAX_TEXT_LENGTH = 256
|
||||||
const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH
|
const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH
|
||||||
|
|
||||||
export const ComposePost = observer(function ComposePost({
|
export const ComposePost = register(
|
||||||
replyTo,
|
observer(function ComposePost({
|
||||||
onPost,
|
replyTo,
|
||||||
onClose,
|
onPost,
|
||||||
}: {
|
onClose,
|
||||||
replyTo?: ComposerOpts['replyTo']
|
}: {
|
||||||
onPost?: ComposerOpts['onPost']
|
replyTo?: ComposerOpts['replyTo']
|
||||||
onClose: () => void
|
onPost?: ComposerOpts['onPost']
|
||||||
}) {
|
onClose: () => void
|
||||||
const store = useStores()
|
}) {
|
||||||
const textInput = useRef<TextInput>(null)
|
const store = useStores()
|
||||||
const [isProcessing, setIsProcessing] = useState(false)
|
const textInput = useRef<TextInput>(null)
|
||||||
const [error, setError] = useState('')
|
const [isProcessing, setIsProcessing] = useState(false)
|
||||||
const [text, setText] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [selectedPhotos, setSelectedPhotos] = useState<string[]>([])
|
const [text, setText] = useState('')
|
||||||
|
const [selectedPhotos, setSelectedPhotos] = useState<string[]>([])
|
||||||
|
|
||||||
const autocompleteView = useMemo<UserAutocompleteViewModel>(
|
const autocompleteView = useMemo<UserAutocompleteViewModel>(
|
||||||
() => new UserAutocompleteViewModel(store),
|
() => new UserAutocompleteViewModel(store),
|
||||||
[store],
|
[store],
|
||||||
)
|
)
|
||||||
const localPhotos = useMemo<UserLocalPhotosModel>(
|
const localPhotos = useMemo<UserLocalPhotosModel>(
|
||||||
() => new UserLocalPhotosModel(store),
|
() => new UserLocalPhotosModel(store),
|
||||||
[store],
|
[store],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
autocompleteView.setup()
|
autocompleteView.setup()
|
||||||
localPhotos.setup()
|
localPhotos.setup()
|
||||||
}, [autocompleteView, localPhotos])
|
}, [autocompleteView, localPhotos])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// HACK
|
// HACK
|
||||||
// wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view
|
// wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view
|
||||||
// -prf
|
// -prf
|
||||||
let to: NodeJS.Timeout | undefined
|
let to: NodeJS.Timeout | undefined
|
||||||
if (textInput.current) {
|
if (textInput.current) {
|
||||||
to = setTimeout(() => {
|
to = setTimeout(() => {
|
||||||
textInput.current?.focus()
|
textInput.current?.focus()
|
||||||
}, 250)
|
}, 250)
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (to) {
|
if (to) {
|
||||||
clearTimeout(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 onPressCancel = () => {
|
||||||
|
onClose()
|
||||||
const onChangeText = (newText: string) => {
|
}
|
||||||
setText(newText)
|
const onPressPublish = async () => {
|
||||||
|
if (isProcessing) {
|
||||||
const prefix = extractTextAutocompletePrefix(newText)
|
return
|
||||||
if (typeof prefix === 'string') {
|
}
|
||||||
autocompleteView.setActive(true)
|
if (text.length > MAX_TEXT_LENGTH) {
|
||||||
autocompleteView.setPrefix(prefix)
|
return
|
||||||
} else {
|
}
|
||||||
|
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)
|
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 canPost = text.length <= MAX_TEXT_LENGTH
|
||||||
const progressColor = text.length > DANGER_TEXT_LENGTH ? '#e60000' : undefined
|
const progressColor =
|
||||||
|
text.length > DANGER_TEXT_LENGTH ? '#e60000' : undefined
|
||||||
|
|
||||||
const selectTextInputLayout =
|
const selectTextInputLayout =
|
||||||
selectedPhotos.length !== 0
|
selectedPhotos.length !== 0
|
||||||
? styles.textInputLayoutWithPhoto
|
? styles.textInputLayoutWithPhoto
|
||||||
: styles.textInputLayoutWithoutPhoto
|
: styles.textInputLayoutWithoutPhoto
|
||||||
const selectTextInputPlaceholder = replyTo
|
const selectTextInputPlaceholder = replyTo
|
||||||
? 'Write your reply'
|
? 'Write your reply'
|
||||||
: selectedPhotos.length !== 0
|
: selectedPhotos.length !== 0
|
||||||
? 'Write a comment'
|
? 'Write a comment'
|
||||||
: "What's up?"
|
: "What's up?"
|
||||||
|
|
||||||
const textDecorated = useMemo(() => {
|
const textDecorated = useMemo(() => {
|
||||||
let i = 0
|
let i = 0
|
||||||
return detectLinkables(text).map(v => {
|
return detectLinkables(text).map(v => {
|
||||||
if (typeof v === 'string') {
|
if (typeof v === 'string') {
|
||||||
return v
|
return v
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Text key={i++} style={{color: colors.blue3}}>
|
<Text key={i++} style={{color: colors.blue3}}>
|
||||||
{v.link}
|
{v.link}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [text])
|
}, [text])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView behavior="padding" style={styles.outer}>
|
<KeyboardAvoidingView behavior="padding" style={styles.outer}>
|
||||||
<SafeAreaView style={s.flex1}>
|
<SafeAreaView style={s.flex1}>
|
||||||
<View style={styles.topbar}>
|
<View style={styles.topbar}>
|
||||||
<TouchableOpacity onPress={onPressCancel}>
|
<TouchableOpacity onPress={onPressCancel}>
|
||||||
<Text style={[s.blue3, s.f18]}>Cancel</Text>
|
<Text style={[s.blue3, s.f18]}>Cancel</Text>
|
||||||
</TouchableOpacity>
|
|
||||||
<View style={s.flex1} />
|
|
||||||
{isProcessing ? (
|
|
||||||
<View style={styles.postBtn}>
|
|
||||||
<ActivityIndicator />
|
|
||||||
</View>
|
|
||||||
) : canPost ? (
|
|
||||||
<TouchableOpacity onPress={onPressPublish}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[gradients.primary.start, gradients.primary.end]}
|
|
||||||
start={{x: 0, y: 0}}
|
|
||||||
end={{x: 1, y: 1}}
|
|
||||||
style={styles.postBtn}>
|
|
||||||
<Text style={[s.white, s.f16, s.bold]}>
|
|
||||||
{replyTo ? 'Reply' : 'Post'}
|
|
||||||
</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : (
|
<View style={s.flex1} />
|
||||||
<View style={[styles.postBtn, {backgroundColor: colors.gray1}]}>
|
{isProcessing ? (
|
||||||
<Text style={[s.gray5, s.f16, s.bold]}>Post</Text>
|
<View style={styles.postBtn}>
|
||||||
</View>
|
<ActivityIndicator />
|
||||||
)}
|
</View>
|
||||||
</View>
|
) : canPost ? (
|
||||||
{error !== '' && (
|
<TouchableOpacity onPress={onPressPublish}>
|
||||||
<View style={styles.errorLine}>
|
<LinearGradient
|
||||||
<View style={styles.errorIcon}>
|
colors={[gradients.primary.start, gradients.primary.end]}
|
||||||
<FontAwesomeIcon
|
start={{x: 0, y: 0}}
|
||||||
icon="exclamation"
|
end={{x: 1, y: 1}}
|
||||||
style={{color: colors.red4}}
|
style={styles.postBtn}>
|
||||||
size={10}
|
<Text style={[s.white, s.f16, s.bold]}>
|
||||||
/>
|
{replyTo ? 'Reply' : 'Post'}
|
||||||
</View>
|
</Text>
|
||||||
<Text style={s.red4}>{error}</Text>
|
</LinearGradient>
|
||||||
</View>
|
</TouchableOpacity>
|
||||||
)}
|
|
||||||
{replyTo ? (
|
|
||||||
<View style={styles.replyToLayout}>
|
|
||||||
<UserAvatar
|
|
||||||
handle={replyTo.author.handle}
|
|
||||||
displayName={replyTo.author.displayName}
|
|
||||||
avatar={replyTo.author.avatar}
|
|
||||||
size={50}
|
|
||||||
/>
|
|
||||||
<View style={styles.replyToPost}>
|
|
||||||
<TextLink
|
|
||||||
href={`/profile/${replyTo.author.handle}`}
|
|
||||||
text={replyTo.author.displayName || replyTo.author.handle}
|
|
||||||
style={[s.f16, s.bold]}
|
|
||||||
/>
|
|
||||||
<Text style={[s.f16, s['lh16-1.3']]} numberOfLines={6}>
|
|
||||||
{replyTo.text}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
<View style={[styles.textInputLayout, selectTextInputLayout]}>
|
|
||||||
<UserAvatar
|
|
||||||
handle={store.me.handle || ''}
|
|
||||||
displayName={store.me.displayName}
|
|
||||||
avatar={store.me.avatar}
|
|
||||||
size={50}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
ref={textInput}
|
|
||||||
multiline
|
|
||||||
scrollEnabled
|
|
||||||
onChangeText={(text: string) => onChangeText(text)}
|
|
||||||
placeholder={selectTextInputPlaceholder}
|
|
||||||
style={styles.textInput}>
|
|
||||||
{textDecorated}
|
|
||||||
</TextInput>
|
|
||||||
</View>
|
|
||||||
<SelectedPhoto
|
|
||||||
selectedPhotos={selectedPhotos}
|
|
||||||
setSelectedPhotos={setSelectedPhotos}
|
|
||||||
/>
|
|
||||||
{IMAGES_ENABLED &&
|
|
||||||
localPhotos.photos != null &&
|
|
||||||
text === '' &&
|
|
||||||
selectedPhotos.length === 0 && (
|
|
||||||
<PhotoCarouselPicker
|
|
||||||
selectedPhotos={selectedPhotos}
|
|
||||||
setSelectedPhotos={setSelectedPhotos}
|
|
||||||
localPhotos={localPhotos}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<View style={styles.bottomBar}>
|
|
||||||
<View style={s.flex1} />
|
|
||||||
<Text style={[s.mr10, {color: progressColor}]}>
|
|
||||||
{MAX_TEXT_LENGTH - text.length}
|
|
||||||
</Text>
|
|
||||||
<View>
|
|
||||||
{text.length > DANGER_TEXT_LENGTH ? (
|
|
||||||
<ProgressPie
|
|
||||||
size={30}
|
|
||||||
borderWidth={4}
|
|
||||||
borderColor={progressColor}
|
|
||||||
color={progressColor}
|
|
||||||
progress={Math.min(
|
|
||||||
(text.length - MAX_TEXT_LENGTH) / MAX_TEXT_LENGTH,
|
|
||||||
1,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<ProgressCircle
|
<View style={[styles.postBtn, {backgroundColor: colors.gray1}]}>
|
||||||
size={30}
|
<Text style={[s.gray5, s.f16, s.bold]}>Post</Text>
|
||||||
borderWidth={1}
|
</View>
|
||||||
borderColor={colors.gray2}
|
|
||||||
color={progressColor}
|
|
||||||
progress={text.length / MAX_TEXT_LENGTH}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
{error !== '' && (
|
||||||
<Autocomplete
|
<View style={styles.errorLine}>
|
||||||
active={autocompleteView.isActive}
|
<View style={styles.errorIcon}>
|
||||||
items={autocompleteView.suggestions}
|
<FontAwesomeIcon
|
||||||
onSelect={onSelectAutocompleteItem}
|
icon="exclamation"
|
||||||
/>
|
style={{color: colors.red4}}
|
||||||
</SafeAreaView>
|
size={10}
|
||||||
</KeyboardAvoidingView>
|
/>
|
||||||
)
|
</View>
|
||||||
})
|
<Text style={s.red4}>{error}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{replyTo ? (
|
||||||
|
<View style={styles.replyToLayout}>
|
||||||
|
<UserAvatar
|
||||||
|
handle={replyTo.author.handle}
|
||||||
|
displayName={replyTo.author.displayName}
|
||||||
|
avatar={replyTo.author.avatar}
|
||||||
|
size={50}
|
||||||
|
/>
|
||||||
|
<View style={styles.replyToPost}>
|
||||||
|
<TextLink
|
||||||
|
href={`/profile/${replyTo.author.handle}`}
|
||||||
|
text={replyTo.author.displayName || replyTo.author.handle}
|
||||||
|
style={[s.f16, s.bold]}
|
||||||
|
/>
|
||||||
|
<Text style={[s.f16, s['lh16-1.3']]} numberOfLines={6}>
|
||||||
|
{replyTo.text}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : undefined}
|
||||||
|
<View style={[styles.textInputLayout, selectTextInputLayout]}>
|
||||||
|
<UserAvatar
|
||||||
|
handle={store.me.handle || ''}
|
||||||
|
displayName={store.me.displayName}
|
||||||
|
avatar={store.me.avatar}
|
||||||
|
size={50}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
ref={textInput}
|
||||||
|
multiline
|
||||||
|
scrollEnabled
|
||||||
|
onChangeText={(text: string) => onChangeText(text)}
|
||||||
|
placeholder={selectTextInputPlaceholder}
|
||||||
|
style={styles.textInput}>
|
||||||
|
{textDecorated}
|
||||||
|
</TextInput>
|
||||||
|
</View>
|
||||||
|
<SelectedPhoto
|
||||||
|
selectedPhotos={selectedPhotos}
|
||||||
|
setSelectedPhotos={setSelectedPhotos}
|
||||||
|
/>
|
||||||
|
{IMAGES_ENABLED &&
|
||||||
|
localPhotos.photos != null &&
|
||||||
|
text === '' &&
|
||||||
|
selectedPhotos.length === 0 && (
|
||||||
|
<PhotoCarouselPicker
|
||||||
|
selectedPhotos={selectedPhotos}
|
||||||
|
setSelectedPhotos={setSelectedPhotos}
|
||||||
|
localPhotos={localPhotos}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<View style={styles.bottomBar}>
|
||||||
|
<View style={s.flex1} />
|
||||||
|
<Text style={[s.mr10, {color: progressColor}]}>
|
||||||
|
{MAX_TEXT_LENGTH - text.length}
|
||||||
|
</Text>
|
||||||
|
<View>
|
||||||
|
{text.length > DANGER_TEXT_LENGTH ? (
|
||||||
|
<ProgressPie
|
||||||
|
size={30}
|
||||||
|
borderWidth={4}
|
||||||
|
borderColor={progressColor}
|
||||||
|
color={progressColor}
|
||||||
|
progress={Math.min(
|
||||||
|
(text.length - MAX_TEXT_LENGTH) / MAX_TEXT_LENGTH,
|
||||||
|
1,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ProgressCircle
|
||||||
|
size={30}
|
||||||
|
borderWidth={1}
|
||||||
|
borderColor={colors.gray2}
|
||||||
|
color={progressColor}
|
||||||
|
progress={text.length / MAX_TEXT_LENGTH}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Autocomplete
|
||||||
|
active={autocompleteView.isActive}
|
||||||
|
items={autocompleteView.suggestions}
|
||||||
|
onSelect={onSelectAutocompleteItem}
|
||||||
|
/>
|
||||||
|
</SafeAreaView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const atPrefixRegex = /@([a-z0-9\.]*)$/i
|
const atPrefixRegex = /@([a-z0-9\.]*)$/i
|
||||||
function extractTextAutocompletePrefix(text: string) {
|
function extractTextAutocompletePrefix(text: string) {
|
||||||
|
|||||||
@@ -7,96 +7,103 @@ import {
|
|||||||
openCamera,
|
openCamera,
|
||||||
openCropper,
|
openCropper,
|
||||||
} from 'react-native-image-crop-picker'
|
} from 'react-native-image-crop-picker'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const PhotoCarouselPicker = ({
|
export const PhotoCarouselPicker = register(
|
||||||
selectedPhotos,
|
({
|
||||||
setSelectedPhotos,
|
selectedPhotos,
|
||||||
localPhotos,
|
setSelectedPhotos,
|
||||||
}: {
|
localPhotos,
|
||||||
selectedPhotos: string[]
|
}: {
|
||||||
setSelectedPhotos: React.Dispatch<React.SetStateAction<string[]>>
|
selectedPhotos: string[]
|
||||||
localPhotos: any
|
setSelectedPhotos: React.Dispatch<React.SetStateAction<string[]>>
|
||||||
}) => {
|
localPhotos: any
|
||||||
const handleOpenCamera = useCallback(() => {
|
}) => {
|
||||||
openCamera({
|
const handleOpenCamera = useCallback(() => {
|
||||||
mediaType: 'photo',
|
openCamera({
|
||||||
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({
|
|
||||||
mediaType: 'photo',
|
mediaType: 'photo',
|
||||||
path: uri,
|
cropping: true,
|
||||||
width: 1000,
|
width: 1000,
|
||||||
height: 1000,
|
height: 1000,
|
||||||
})
|
}).then(
|
||||||
setSelectedPhotos([img.path, ...selectedPhotos])
|
item => {
|
||||||
},
|
setSelectedPhotos([item.path, ...selectedPhotos])
|
||||||
[selectedPhotos, setSelectedPhotos],
|
},
|
||||||
)
|
_err => {
|
||||||
|
// ignore
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}, [selectedPhotos, setSelectedPhotos])
|
||||||
|
|
||||||
const handleOpenGallery = useCallback(() => {
|
const handleSelectPhoto = useCallback(
|
||||||
openPicker({
|
async (uri: string) => {
|
||||||
multiple: true,
|
|
||||||
maxFiles: 4,
|
|
||||||
mediaType: 'photo',
|
|
||||||
}).then(async items => {
|
|
||||||
const result = []
|
|
||||||
|
|
||||||
for await (const image of items) {
|
|
||||||
const img = await openCropper({
|
const img = await openCropper({
|
||||||
mediaType: 'photo',
|
mediaType: 'photo',
|
||||||
path: image.path,
|
path: uri,
|
||||||
width: 1000,
|
width: 1000,
|
||||||
height: 1000,
|
height: 1000,
|
||||||
})
|
})
|
||||||
result.push(img.path)
|
setSelectedPhotos([img.path, ...selectedPhotos])
|
||||||
}
|
},
|
||||||
setSelectedPhotos([...result, ...selectedPhotos])
|
[selectedPhotos, setSelectedPhotos],
|
||||||
})
|
)
|
||||||
}, [selectedPhotos, setSelectedPhotos])
|
|
||||||
|
|
||||||
return (
|
const handleOpenGallery = useCallback(() => {
|
||||||
<ScrollView
|
openPicker({
|
||||||
horizontal
|
multiple: true,
|
||||||
style={styles.photosContainer}
|
maxFiles: 4,
|
||||||
showsHorizontalScrollIndicator={false}>
|
mediaType: 'photo',
|
||||||
<TouchableOpacity
|
}).then(async items => {
|
||||||
style={[styles.galleryButton, styles.photo]}
|
const result = []
|
||||||
onPress={handleOpenCamera}>
|
|
||||||
<FontAwesomeIcon
|
for await (const image of items) {
|
||||||
icon="camera"
|
const img = await openCropper({
|
||||||
size={24}
|
mediaType: 'photo',
|
||||||
style={{color: colors.blue3}}
|
path: image.path,
|
||||||
/>
|
width: 1000,
|
||||||
</TouchableOpacity>
|
height: 1000,
|
||||||
{localPhotos.photos.map((item: any, index: number) => (
|
})
|
||||||
|
result.push(img.path)
|
||||||
|
}
|
||||||
|
setSelectedPhotos([...result, ...selectedPhotos])
|
||||||
|
})
|
||||||
|
}, [selectedPhotos, setSelectedPhotos])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
style={styles.photosContainer}
|
||||||
|
showsHorizontalScrollIndicator={false}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={`local-image-${index}`}
|
style={[styles.galleryButton, styles.photo]}
|
||||||
style={styles.photoButton}
|
onPress={handleOpenCamera}>
|
||||||
onPress={() => handleSelectPhoto(item.node.image.uri)}>
|
<FontAwesomeIcon
|
||||||
<Image style={styles.photo} source={{uri: item.node.image.uri}} />
|
icon="camera"
|
||||||
|
size={24}
|
||||||
|
style={{color: colors.blue3}}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
{localPhotos.photos.map((item: any, index: number) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.galleryButton, styles.photo]}
|
key={`local-image-${index}`}
|
||||||
onPress={handleOpenGallery}>
|
style={styles.photoButton}
|
||||||
<FontAwesomeIcon icon="image" style={{color: colors.blue3}} size={24} />
|
onPress={() => handleSelectPhoto(item.node.image.uri)}>
|
||||||
</TouchableOpacity>
|
<Image style={styles.photo} source={{uri: item.node.image.uri}} />
|
||||||
</ScrollView>
|
</TouchableOpacity>
|
||||||
)
|
))}
|
||||||
}
|
<TouchableOpacity
|
||||||
|
style={[styles.galleryButton, styles.photo]}
|
||||||
|
onPress={handleOpenGallery}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="image"
|
||||||
|
style={{color: colors.blue3}}
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</ScrollView>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
photosContainer: {
|
photosContainer: {
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
|
|||||||
import {colors} from '../../lib/styles'
|
import {colors} from '../../lib/styles'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export function ComposePrompt({
|
export const ComposePrompt = register(function ComposePrompt({
|
||||||
noAvi = false,
|
noAvi = false,
|
||||||
text = "What's up?",
|
text = "What's up?",
|
||||||
btn = 'Post',
|
btn = 'Post',
|
||||||
@@ -41,7 +42,7 @@ export function ComposePrompt({
|
|||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
|||||||
@@ -2,53 +2,56 @@ import React, {useCallback} from 'react'
|
|||||||
import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
|
import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {colors} from '../../lib/styles'
|
import {colors} from '../../lib/styles'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const SelectedPhoto = ({
|
export const SelectedPhoto = register(
|
||||||
selectedPhotos,
|
({
|
||||||
setSelectedPhotos,
|
selectedPhotos,
|
||||||
}: {
|
setSelectedPhotos,
|
||||||
selectedPhotos: string[]
|
}: {
|
||||||
setSelectedPhotos: React.Dispatch<React.SetStateAction<string[]>>
|
selectedPhotos: string[]
|
||||||
}) => {
|
setSelectedPhotos: React.Dispatch<React.SetStateAction<string[]>>
|
||||||
const imageStyle =
|
}) => {
|
||||||
selectedPhotos.length === 1
|
const imageStyle =
|
||||||
? styles.image250
|
selectedPhotos.length === 1
|
||||||
: selectedPhotos.length === 2
|
? styles.image250
|
||||||
? styles.image175
|
: selectedPhotos.length === 2
|
||||||
: styles.image85
|
? styles.image175
|
||||||
|
: styles.image85
|
||||||
|
|
||||||
const handleRemovePhoto = useCallback(
|
const handleRemovePhoto = useCallback(
|
||||||
item => {
|
item => {
|
||||||
setSelectedPhotos(
|
setSelectedPhotos(
|
||||||
selectedPhotos.filter(filterItem => filterItem !== item),
|
selectedPhotos.filter(filterItem => filterItem !== item),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[selectedPhotos, setSelectedPhotos],
|
[selectedPhotos, setSelectedPhotos],
|
||||||
)
|
)
|
||||||
|
|
||||||
return selectedPhotos.length !== 0 ? (
|
return selectedPhotos.length !== 0 ? (
|
||||||
<View style={styles.imageContainer}>
|
<View style={styles.imageContainer}>
|
||||||
{selectedPhotos.length !== 0 &&
|
{selectedPhotos.length !== 0 &&
|
||||||
selectedPhotos.map((item, index) => (
|
selectedPhotos.map((item, index) => (
|
||||||
<View
|
<View
|
||||||
key={`selected-image-${index}`}
|
key={`selected-image-${index}`}
|
||||||
style={[styles.image, imageStyle]}>
|
style={[styles.image, imageStyle]}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => handleRemovePhoto(item)}
|
onPress={() => handleRemovePhoto(item)}
|
||||||
style={styles.removePhotoButton}>
|
style={styles.removePhotoButton}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="xmark"
|
icon="xmark"
|
||||||
size={16}
|
size={16}
|
||||||
style={{color: colors.white}}
|
style={{color: colors.white}}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<Image style={[styles.image, imageStyle]} source={{uri: item}} />
|
<Image style={[styles.image, imageStyle]} source={{uri: item}} />
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
) : null
|
) : null
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
imageContainer: {
|
imageContainer: {
|
||||||
|
|||||||
@@ -22,112 +22,119 @@ import {
|
|||||||
SuggestedActor,
|
SuggestedActor,
|
||||||
} from '../../../state/models/suggested-actors-view'
|
} from '../../../state/models/suggested-actors-view'
|
||||||
import {s, colors, gradients} from '../../lib/styles'
|
import {s, colors, gradients} from '../../lib/styles'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const SuggestedFollows = observer(
|
export const SuggestedFollows = register(
|
||||||
({
|
observer(
|
||||||
onNoSuggestions,
|
({
|
||||||
asLinks,
|
onNoSuggestions,
|
||||||
}: {
|
asLinks,
|
||||||
onNoSuggestions?: () => void
|
}: {
|
||||||
asLinks?: boolean
|
onNoSuggestions?: () => void
|
||||||
}) => {
|
asLinks?: boolean
|
||||||
const store = useStores()
|
}) => {
|
||||||
const [follows, setFollows] = useState<Record<string, string>>({})
|
const store = useStores()
|
||||||
|
const [follows, setFollows] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
const view = useMemo<SuggestedActorsViewModel>(
|
const view = useMemo<SuggestedActorsViewModel>(
|
||||||
() => new SuggestedActorsViewModel(store),
|
() => new SuggestedActorsViewModel(store),
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('Fetching suggested actors')
|
console.log('Fetching suggested actors')
|
||||||
view
|
view
|
||||||
.setup()
|
.setup()
|
||||||
.catch((err: any) => console.error('Failed to fetch suggestions', err))
|
.catch((err: any) =>
|
||||||
}, [view])
|
console.error('Failed to fetch suggestions', err),
|
||||||
|
)
|
||||||
|
}, [view])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!view.isLoading && !view.hasError && !view.hasContent) {
|
if (!view.isLoading && !view.hasError && !view.hasContent) {
|
||||||
onNoSuggestions?.()
|
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 onPressUnfollow = async (item: SuggestedActor) => {
|
||||||
|
try {
|
||||||
const onPressTryAgain = () =>
|
await apilib.unfollow(store, follows[item.did])
|
||||||
view
|
setFollows(_omit(follows, [item.did]))
|
||||||
.setup()
|
} catch (e) {
|
||||||
.catch((err: any) => console.error('Failed to fetch suggestions', err))
|
console.log(e)
|
||||||
|
Toast.show('An issue occurred, please try again.')
|
||||||
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 renderItem = ({item}: {item: SuggestedActor}) => {
|
const renderItem = ({item}: {item: SuggestedActor}) => {
|
||||||
if (asLinks) {
|
if (asLinks) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`/profile/${item.handle}`}
|
||||||
|
title={item.displayName || item.handle}>
|
||||||
|
<User
|
||||||
|
item={item}
|
||||||
|
follow={follows[item.did]}
|
||||||
|
onPressFollow={onPressFollow}
|
||||||
|
onPressUnfollow={onPressUnfollow}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Link
|
<User
|
||||||
href={`/profile/${item.handle}`}
|
item={item}
|
||||||
title={item.displayName || item.handle}>
|
follow={follows[item.did]}
|
||||||
<User
|
onPressFollow={onPressFollow}
|
||||||
item={item}
|
onPressUnfollow={onPressUnfollow}
|
||||||
follow={follows[item.did]}
|
/>
|
||||||
onPressFollow={onPressFollow}
|
|
||||||
onPressUnfollow={onPressUnfollow}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<User
|
<View style={styles.container}>
|
||||||
item={item}
|
{view.isLoading ? (
|
||||||
follow={follows[item.did]}
|
<View>
|
||||||
onPressFollow={onPressFollow}
|
<ActivityIndicator />
|
||||||
onPressUnfollow={onPressUnfollow}
|
</View>
|
||||||
/>
|
) : view.hasError ? (
|
||||||
)
|
<ErrorScreen
|
||||||
}
|
title="Failed to load suggestions"
|
||||||
return (
|
message="There was an error while trying to load suggested follows."
|
||||||
<View style={styles.container}>
|
details={view.error}
|
||||||
{view.isLoading ? (
|
onPressTryAgain={onPressTryAgain}
|
||||||
<View>
|
|
||||||
<ActivityIndicator />
|
|
||||||
</View>
|
|
||||||
) : view.hasError ? (
|
|
||||||
<ErrorScreen
|
|
||||||
title="Failed to load suggestions"
|
|
||||||
message="There was an error while trying to load suggested follows."
|
|
||||||
details={view.error}
|
|
||||||
onPressTryAgain={onPressTryAgain}
|
|
||||||
/>
|
|
||||||
) : view.isEmpty ? (
|
|
||||||
<View />
|
|
||||||
) : (
|
|
||||||
<View style={styles.suggestionsContainer}>
|
|
||||||
<FlatList
|
|
||||||
data={view.suggestions}
|
|
||||||
keyExtractor={item => item._reactKey}
|
|
||||||
renderItem={renderItem}
|
|
||||||
style={s.flex1}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
) : view.isEmpty ? (
|
||||||
)}
|
<View />
|
||||||
</View>
|
) : (
|
||||||
)
|
<View style={styles.suggestionsContainer}>
|
||||||
},
|
<FlatList
|
||||||
|
data={view.suggestions}
|
||||||
|
keyExtractor={item => item._reactKey}
|
||||||
|
renderItem={renderItem}
|
||||||
|
style={s.flex1}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const User = ({
|
const User = ({
|
||||||
|
|||||||
@@ -6,67 +6,70 @@ import {FeedItem} from './FeedItem'
|
|||||||
import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||||
import {ErrorMessage} from '../util/ErrorMessage'
|
import {ErrorMessage} from '../util/ErrorMessage'
|
||||||
import {EmptyState} from '../util/EmptyState'
|
import {EmptyState} from '../util/EmptyState'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
||||||
|
|
||||||
export const Feed = observer(function Feed({
|
export const Feed = register(
|
||||||
view,
|
observer(function Feed({
|
||||||
onPressTryAgain,
|
view,
|
||||||
}: {
|
onPressTryAgain,
|
||||||
view: NotificationsViewModel
|
}: {
|
||||||
onPressTryAgain?: () => void
|
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
|
// TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf
|
||||||
// renderItem function renders components that follow React performance best practices
|
// VirtualizedList: You have a large list that is slow to update - make sure your
|
||||||
// like PureComponent, shouldComponentUpdate, etc
|
// renderItem function renders components that follow React performance best practices
|
||||||
const renderItem = ({item}: {item: any}) => {
|
// like PureComponent, shouldComponentUpdate, etc
|
||||||
if (item === EMPTY_FEED_ITEM) {
|
const renderItem = ({item}: {item: any}) => {
|
||||||
return (
|
if (item === EMPTY_FEED_ITEM) {
|
||||||
<EmptyState
|
return (
|
||||||
icon="bell"
|
<EmptyState
|
||||||
message="No notifications yet!"
|
icon="bell"
|
||||||
style={{paddingVertical: 40}}
|
message="No notifications yet!"
|
||||||
/>
|
style={{paddingVertical: 40}}
|
||||||
)
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <FeedItem item={item} />
|
||||||
}
|
}
|
||||||
return <FeedItem item={item} />
|
const onRefresh = () => {
|
||||||
}
|
view.refresh().catch(err => console.error('Failed to refresh', err))
|
||||||
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 onEndReached = () => {
|
||||||
return (
|
view.loadMore().catch(err => console.error('Failed to load more', err))
|
||||||
<View style={{flex: 1}}>
|
}
|
||||||
{view.isLoading && !data && <NotificationFeedLoadingPlaceholder />}
|
let data
|
||||||
{view.hasError && (
|
if (view.hasLoaded) {
|
||||||
<ErrorMessage
|
if (view.isEmpty) {
|
||||||
dark
|
data = [EMPTY_FEED_ITEM]
|
||||||
message={view.error}
|
} else {
|
||||||
style={{margin: 6}}
|
data = view.notifications
|
||||||
onPressTryAgain={onPressTryAgain}
|
}
|
||||||
/>
|
}
|
||||||
)}
|
return (
|
||||||
{data && (
|
<View style={{flex: 1}}>
|
||||||
<FlatList
|
{view.isLoading && !data && <NotificationFeedLoadingPlaceholder />}
|
||||||
data={data}
|
{view.hasError && (
|
||||||
keyExtractor={item => item._reactKey}
|
<ErrorMessage
|
||||||
renderItem={renderItem}
|
dark
|
||||||
refreshing={view.isRefreshing}
|
message={view.error}
|
||||||
onRefresh={onRefresh}
|
style={{margin: 6}}
|
||||||
onEndReached={onEndReached}
|
onPressTryAgain={onPressTryAgain}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
{data && (
|
||||||
)
|
<FlatList
|
||||||
})
|
data={data}
|
||||||
|
keyExtractor={item => item._reactKey}
|
||||||
|
renderItem={renderItem}
|
||||||
|
refreshing={view.isRefreshing}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
onEndReached={onEndReached}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|||||||
@@ -13,185 +13,185 @@ import {ErrorMessage} from '../util/ErrorMessage'
|
|||||||
import {Post} from '../post/Post'
|
import {Post} from '../post/Post'
|
||||||
import {Link} from '../util/Link'
|
import {Link} from '../util/Link'
|
||||||
import {InviteAccepter} from './InviteAccepter'
|
import {InviteAccepter} from './InviteAccepter'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
const MAX_AUTHORS = 8
|
const MAX_AUTHORS = 8
|
||||||
|
|
||||||
export const FeedItem = observer(function FeedItem({
|
export const FeedItem = register(
|
||||||
item,
|
observer(function FeedItem({item}: {item: NotificationsViewItemModel}) {
|
||||||
}: {
|
const itemHref = useMemo(() => {
|
||||||
item: NotificationsViewItemModel
|
if (item.isUpvote || item.isRepost || item.isTrend) {
|
||||||
}) {
|
const urip = new AtUri(item.subjectUri)
|
||||||
const itemHref = useMemo(() => {
|
return `/profile/${urip.host}/post/${urip.rkey}`
|
||||||
if (item.isUpvote || item.isRepost || item.isTrend) {
|
} else if (item.isFollow || item.isAssertion) {
|
||||||
const urip = new AtUri(item.subjectUri)
|
return `/profile/${item.author.handle}`
|
||||||
return `/profile/${urip.host}/post/${urip.rkey}`
|
} else if (item.isReply) {
|
||||||
} else if (item.isFollow || item.isAssertion) {
|
const urip = new AtUri(item.uri)
|
||||||
return `/profile/${item.author.handle}`
|
return `/profile/${urip.host}/post/${urip.rkey}`
|
||||||
} else if (item.isReply) {
|
}
|
||||||
const urip = new AtUri(item.uri)
|
return ''
|
||||||
return `/profile/${urip.host}/post/${urip.rkey}`
|
}, [item])
|
||||||
}
|
const itemTitle = useMemo(() => {
|
||||||
return ''
|
if (item.isUpvote || item.isRepost) {
|
||||||
}, [item])
|
return 'Post'
|
||||||
const itemTitle = useMemo(() => {
|
} else if (item.isFollow || item.isAssertion) {
|
||||||
if (item.isUpvote || item.isRepost) {
|
return item.author.handle
|
||||||
return 'Post'
|
} else if (item.isReply) {
|
||||||
} else if (item.isFollow || item.isAssertion) {
|
return 'Post'
|
||||||
return item.author.handle
|
}
|
||||||
} else if (item.isReply) {
|
}, [item])
|
||||||
return 'Post'
|
|
||||||
}
|
|
||||||
}, [item])
|
|
||||||
|
|
||||||
if (item.additionalPost?.notFound) {
|
if (item.additionalPost?.notFound) {
|
||||||
// don't render anything if the target post was deleted or unfindable
|
// don't render anything if the target post was deleted or unfindable
|
||||||
return <View />
|
return <View />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.isReply || item.isMention) {
|
||||||
|
return (
|
||||||
|
<Link href={itemHref} title={itemTitle}>
|
||||||
|
<Post
|
||||||
|
uri={item.uri}
|
||||||
|
initView={item.additionalPost}
|
||||||
|
style={[
|
||||||
|
styles.outerMinimal,
|
||||||
|
item.isRead ? undefined : styles.outerUnread,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<Link href={itemHref} title={itemTitle}>
|
<Link
|
||||||
<Post
|
style={[styles.outer, item.isRead ? undefined : styles.outerUnread]}
|
||||||
uri={item.uri}
|
href={itemHref}
|
||||||
initView={item.additionalPost}
|
title={itemTitle}>
|
||||||
style={[
|
<View style={styles.layout}>
|
||||||
styles.outerMinimal,
|
<View style={styles.layoutIcon}>
|
||||||
item.isRead ? undefined : styles.outerUnread,
|
{icon === 'UpIconSolid' ? (
|
||||||
]}
|
<UpIconSolid size={26} style={[styles.icon, ...iconStyle]} />
|
||||||
/>
|
) : (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={icon}
|
||||||
|
size={22}
|
||||||
|
style={[styles.icon, ...iconStyle]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View style={styles.layoutContent}>
|
||||||
|
<View style={styles.avis}>
|
||||||
|
{authors.slice(0, MAX_AUTHORS).map(author => (
|
||||||
|
<Link
|
||||||
|
style={{marginRight: 3}}
|
||||||
|
key={author.href}
|
||||||
|
href={author.href}
|
||||||
|
title={`@${author.handle}`}>
|
||||||
|
<UserAvatar
|
||||||
|
size={30}
|
||||||
|
displayName={author.displayName}
|
||||||
|
handle={author.handle}
|
||||||
|
avatar={author.avatar}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
{authors.length > MAX_AUTHORS ? (
|
||||||
|
<Text style={styles.aviExtraCount}>
|
||||||
|
+{authors.length - MAX_AUTHORS}
|
||||||
|
</Text>
|
||||||
|
) : undefined}
|
||||||
|
</View>
|
||||||
|
<View style={styles.meta}>
|
||||||
|
{item.isTrend && (
|
||||||
|
<Text style={[styles.metaItem, s.f15]}>{action}</Text>
|
||||||
|
)}
|
||||||
|
<Link
|
||||||
|
key={authors[0].href}
|
||||||
|
style={styles.metaItem}
|
||||||
|
href={authors[0].href}
|
||||||
|
title={`@${authors[0].handle}`}>
|
||||||
|
<Text style={[s.f15, s.bold]}>
|
||||||
|
{authors[0].displayName || authors[0].handle}
|
||||||
|
</Text>
|
||||||
|
</Link>
|
||||||
|
{authors.length > 1 ? (
|
||||||
|
<>
|
||||||
|
<Text style={[styles.metaItem, s.f15]}>and</Text>
|
||||||
|
<Text style={[styles.metaItem, s.f15, s.bold]}>
|
||||||
|
{authors.length - 1}{' '}
|
||||||
|
{pluralize(authors.length - 1, 'other')}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
) : undefined}
|
||||||
|
{!item.isTrend && (
|
||||||
|
<Text style={[styles.metaItem, s.f15]}>{action}</Text>
|
||||||
|
)}
|
||||||
|
<Text style={[styles.metaItem, s.f15, s.gray5]}>
|
||||||
|
{ago(item.indexedAt)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{item.isUpvote || item.isRepost || item.isTrend ? (
|
||||||
|
<AdditionalPostText additionalPost={item.additionalPost} />
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{item.isInvite && (
|
||||||
|
<View style={styles.addedContainer}>
|
||||||
|
<InviteAccepter item={item} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}),
|
||||||
|
)
|
||||||
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 (
|
|
||||||
<Link
|
|
||||||
style={[styles.outer, item.isRead ? undefined : styles.outerUnread]}
|
|
||||||
href={itemHref}
|
|
||||||
title={itemTitle}>
|
|
||||||
<View style={styles.layout}>
|
|
||||||
<View style={styles.layoutIcon}>
|
|
||||||
{icon === 'UpIconSolid' ? (
|
|
||||||
<UpIconSolid size={26} style={[styles.icon, ...iconStyle]} />
|
|
||||||
) : (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={icon}
|
|
||||||
size={22}
|
|
||||||
style={[styles.icon, ...iconStyle]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<View style={styles.layoutContent}>
|
|
||||||
<View style={styles.avis}>
|
|
||||||
{authors.slice(0, MAX_AUTHORS).map(author => (
|
|
||||||
<Link
|
|
||||||
style={{marginRight: 3}}
|
|
||||||
key={author.href}
|
|
||||||
href={author.href}
|
|
||||||
title={`@${author.handle}`}>
|
|
||||||
<UserAvatar
|
|
||||||
size={30}
|
|
||||||
displayName={author.displayName}
|
|
||||||
handle={author.handle}
|
|
||||||
avatar={author.avatar}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
{authors.length > MAX_AUTHORS ? (
|
|
||||||
<Text style={styles.aviExtraCount}>
|
|
||||||
+{authors.length - MAX_AUTHORS}
|
|
||||||
</Text>
|
|
||||||
) : undefined}
|
|
||||||
</View>
|
|
||||||
<View style={styles.meta}>
|
|
||||||
{item.isTrend && (
|
|
||||||
<Text style={[styles.metaItem, s.f15]}>{action}</Text>
|
|
||||||
)}
|
|
||||||
<Link
|
|
||||||
key={authors[0].href}
|
|
||||||
style={styles.metaItem}
|
|
||||||
href={authors[0].href}
|
|
||||||
title={`@${authors[0].handle}`}>
|
|
||||||
<Text style={[s.f15, s.bold]}>
|
|
||||||
{authors[0].displayName || authors[0].handle}
|
|
||||||
</Text>
|
|
||||||
</Link>
|
|
||||||
{authors.length > 1 ? (
|
|
||||||
<>
|
|
||||||
<Text style={[styles.metaItem, s.f15]}>and</Text>
|
|
||||||
<Text style={[styles.metaItem, s.f15, s.bold]}>
|
|
||||||
{authors.length - 1} {pluralize(authors.length - 1, 'other')}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
) : undefined}
|
|
||||||
{!item.isTrend && (
|
|
||||||
<Text style={[styles.metaItem, s.f15]}>{action}</Text>
|
|
||||||
)}
|
|
||||||
<Text style={[styles.metaItem, s.f15, s.gray5]}>
|
|
||||||
{ago(item.indexedAt)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
{item.isUpvote || item.isRepost || item.isTrend ? (
|
|
||||||
<AdditionalPostText additionalPost={item.additionalPost} />
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
{item.isInvite && (
|
|
||||||
<View style={styles.addedContainer}>
|
|
||||||
<InviteAccepter item={item} />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
function AdditionalPostText({
|
function AdditionalPostText({
|
||||||
additionalPost,
|
additionalPost,
|
||||||
|
|||||||
@@ -9,8 +9,13 @@ import {useStores} from '../../../state'
|
|||||||
import {ProfileCard} from '../profile/ProfileCard'
|
import {ProfileCard} from '../profile/ProfileCard'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
import {s, colors, gradients} from '../../lib/styles'
|
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 store = useStores()
|
||||||
const [confirmationUri, setConfirmationUri] = useState<string>('')
|
const [confirmationUri, setConfirmationUri] = useState<string>('')
|
||||||
const isMember =
|
const isMember =
|
||||||
@@ -70,7 +75,7 @@ export function InviteAccepter({item}: {item: NotificationsViewItemModel}) {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {useStores} from '../../../state'
|
|||||||
import {s} from '../../lib/styles'
|
import {s} from '../../lib/styles'
|
||||||
import {SCENE_EXPLAINER, TABS_EXPLAINER} from '../../lib/assets'
|
import {SCENE_EXPLAINER, TABS_EXPLAINER} from '../../lib/assets'
|
||||||
import {TABS_ENABLED} from '../../../build-flags'
|
import {TABS_ENABLED} from '../../../build-flags'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
const Intro = () => (
|
const Intro = () => (
|
||||||
<View style={styles.explainer}>
|
<View style={styles.explainer}>
|
||||||
@@ -79,7 +80,7 @@ const SCENE_MAP = {
|
|||||||
}
|
}
|
||||||
const renderScene = SceneMap(SCENE_MAP)
|
const renderScene = SceneMap(SCENE_MAP)
|
||||||
|
|
||||||
export const FeatureExplainer = () => {
|
export const FeatureExplainer = register(() => {
|
||||||
const layout = useWindowDimensions()
|
const layout = useWindowDimensions()
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const [index, setIndex] = useState(0)
|
const [index, setIndex] = useState(0)
|
||||||
@@ -153,7 +154,7 @@ export const FeatureExplainer = () => {
|
|||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
|||||||
@@ -10,32 +10,35 @@ import {observer} from 'mobx-react-lite'
|
|||||||
import {SuggestedFollows} from '../discover/SuggestedFollows'
|
import {SuggestedFollows} from '../discover/SuggestedFollows'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {s} from '../../lib/styles'
|
import {s} from '../../lib/styles'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const Follows = observer(() => {
|
export const Follows = register(
|
||||||
const store = useStores()
|
observer(() => {
|
||||||
|
const store = useStores()
|
||||||
|
|
||||||
const onNoSuggestions = () => {
|
const onNoSuggestions = () => {
|
||||||
// no suggestions, bounce from this view
|
// no suggestions, bounce from this view
|
||||||
store.onboard.next()
|
store.onboard.next()
|
||||||
}
|
}
|
||||||
const onPressNext = () => store.onboard.next()
|
const onPressNext = () => store.onboard.next()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<Text style={styles.title}>Suggested follows</Text>
|
<Text style={styles.title}>Suggested follows</Text>
|
||||||
<SuggestedFollows onNoSuggestions={onNoSuggestions} />
|
<SuggestedFollows onNoSuggestions={onNoSuggestions} />
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<TouchableOpacity onPress={onPressNext}>
|
<TouchableOpacity onPress={onPressNext}>
|
||||||
<Text style={[s.blue3, s.f18]}>Skip</Text>
|
<Text style={[s.blue3, s.f18]}>Skip</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={s.flex1} />
|
<View style={s.flex1} />
|
||||||
<TouchableOpacity onPress={onPressNext}>
|
<TouchableOpacity onPress={onPressNext}>
|
||||||
<Text style={[s.blue3, s.f18]}>Next</Text>
|
<Text style={[s.blue3, s.f18]}>Next</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
|||||||
@@ -10,76 +10,75 @@ import {ErrorMessage} from '../util/ErrorMessage'
|
|||||||
import {Link} from '../util/Link'
|
import {Link} from '../util/Link'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {s, colors} from '../../lib/styles'
|
import {s, colors} from '../../lib/styles'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const PostRepostedBy = observer(function PostRepostedBy({
|
export const PostRepostedBy = register(
|
||||||
uri,
|
observer(function PostRepostedBy({uri}: {uri: string}) {
|
||||||
}: {
|
const store = useStores()
|
||||||
uri: string
|
const [view, setView] = useState<RepostedByViewModel | undefined>()
|
||||||
}) {
|
|
||||||
const store = useStores()
|
|
||||||
const [view, setView] = useState<RepostedByViewModel | undefined>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (view?.params.uri === uri) {
|
if (view?.params.uri === uri) {
|
||||||
console.log('Reposted by doing nothing')
|
console.log('Reposted by doing nothing')
|
||||||
return // no change needed? or trigger refresh?
|
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 = () => {
|
// loading
|
||||||
view?.refresh()
|
// =
|
||||||
}
|
if (
|
||||||
|
!view ||
|
||||||
|
(view.isLoading && !view.isRefreshing) ||
|
||||||
|
view.params.uri !== uri
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<ActivityIndicator />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// loading
|
// error
|
||||||
// =
|
// =
|
||||||
if (
|
if (view.hasError) {
|
||||||
!view ||
|
return (
|
||||||
(view.isLoading && !view.isRefreshing) ||
|
<View>
|
||||||
view.params.uri !== uri
|
<ErrorMessage
|
||||||
) {
|
dark
|
||||||
return (
|
message={view.error}
|
||||||
<View>
|
style={{margin: 6}}
|
||||||
<ActivityIndicator />
|
onPressTryAgain={onRefresh}
|
||||||
</View>
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loaded
|
||||||
|
// =
|
||||||
|
const renderItem = ({item}: {item: RepostedByViewItemModel}) => (
|
||||||
|
<RepostedByItem item={item} />
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// error
|
|
||||||
// =
|
|
||||||
if (view.hasError) {
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ErrorMessage
|
<FlatList
|
||||||
dark
|
data={view.repostedBy}
|
||||||
message={view.error}
|
keyExtractor={item => item._reactKey}
|
||||||
style={{margin: 6}}
|
renderItem={renderItem}
|
||||||
onPressTryAgain={onRefresh}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}),
|
||||||
|
)
|
||||||
// loaded
|
|
||||||
// =
|
|
||||||
const renderItem = ({item}: {item: RepostedByViewItemModel}) => (
|
|
||||||
<RepostedByItem item={item} />
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<FlatList
|
|
||||||
data={view.repostedBy}
|
|
||||||
keyExtractor={item => item._reactKey}
|
|
||||||
renderItem={renderItem}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const RepostedByItem = ({item}: {item: RepostedByViewItemModel}) => {
|
const RepostedByItem = ({item}: {item: RepostedByViewItemModel}) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,85 +8,88 @@ import {
|
|||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {PostThreadItem} from './PostThreadItem'
|
import {PostThreadItem} from './PostThreadItem'
|
||||||
import {ErrorMessage} from '../util/ErrorMessage'
|
import {ErrorMessage} from '../util/ErrorMessage'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const PostThread = observer(function PostThread({
|
export const PostThread = register(
|
||||||
uri,
|
observer(function PostThread({
|
||||||
view,
|
uri,
|
||||||
}: {
|
view,
|
||||||
uri: string
|
}: {
|
||||||
view: PostThreadViewModel
|
uri: string
|
||||||
}) {
|
view: PostThreadViewModel
|
||||||
const ref = useRef<FlatList>(null)
|
}) {
|
||||||
const posts = view.thread ? Array.from(flattenThread(view.thread)) : []
|
const ref = useRef<FlatList>(null)
|
||||||
const onRefresh = () => {
|
const posts = view.thread ? Array.from(flattenThread(view.thread)) : []
|
||||||
view?.refresh().catch(err => console.error('Failed to refresh', err))
|
const onRefresh = () => {
|
||||||
}
|
view?.refresh().catch(err => console.error('Failed to refresh', err))
|
||||||
const onLayout = () => {
|
}
|
||||||
const index = posts.findIndex(post => post._isHighlightedPost)
|
const onLayout = () => {
|
||||||
if (index !== -1) {
|
const index = posts.findIndex(post => post._isHighlightedPost)
|
||||||
ref.current?.scrollToIndex({
|
if (index !== -1) {
|
||||||
index,
|
ref.current?.scrollToIndex({
|
||||||
|
index,
|
||||||
|
animated: false,
|
||||||
|
viewOffset: 40,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const onScrollToIndexFailed = (info: {
|
||||||
|
index: number
|
||||||
|
highestMeasuredFrameIndex: number
|
||||||
|
averageItemLength: number
|
||||||
|
}) => {
|
||||||
|
ref.current?.scrollToOffset({
|
||||||
animated: false,
|
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
|
// loading
|
||||||
// =
|
// =
|
||||||
if ((view.isLoading && !view.isRefreshing) || view.params.uri !== uri) {
|
if ((view.isLoading && !view.isRefreshing) || view.params.uri !== uri) {
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
</View>
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// error
|
||||||
|
// =
|
||||||
|
if (view.hasError) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<ErrorMessage
|
||||||
|
dark
|
||||||
|
message={view.error}
|
||||||
|
style={{margin: 6}}
|
||||||
|
onPressTryAgain={onRefresh}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loaded
|
||||||
|
// =
|
||||||
|
const renderItem = ({item}: {item: PostThreadViewPostModel}) => (
|
||||||
|
<PostThreadItem item={item} onPostReply={onRefresh} />
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// error
|
|
||||||
// =
|
|
||||||
if (view.hasError) {
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<FlatList
|
||||||
<ErrorMessage
|
ref={ref}
|
||||||
dark
|
data={posts}
|
||||||
message={view.error}
|
keyExtractor={item => item._reactKey}
|
||||||
style={{margin: 6}}
|
renderItem={renderItem}
|
||||||
onPressTryAgain={onRefresh}
|
refreshing={view.isRefreshing}
|
||||||
/>
|
onRefresh={onRefresh}
|
||||||
</View>
|
onLayout={onLayout}
|
||||||
|
onScrollToIndexFailed={onScrollToIndexFailed}
|
||||||
|
style={{flex: 1}}
|
||||||
|
contentContainerStyle={{paddingBottom: 200}}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}),
|
||||||
|
)
|
||||||
// loaded
|
|
||||||
// =
|
|
||||||
const renderItem = ({item}: {item: PostThreadViewPostModel}) => (
|
|
||||||
<PostThreadItem item={item} onPostReply={onRefresh} />
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<FlatList
|
|
||||||
ref={ref}
|
|
||||||
data={posts}
|
|
||||||
keyExtractor={item => item._reactKey}
|
|
||||||
renderItem={renderItem}
|
|
||||||
refreshing={view.isRefreshing}
|
|
||||||
onRefresh={onRefresh}
|
|
||||||
onLayout={onLayout}
|
|
||||||
onScrollToIndexFailed={onScrollToIndexFailed}
|
|
||||||
style={{flex: 1}}
|
|
||||||
contentContainerStyle={{paddingBottom: 200}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
function* flattenThread(
|
function* flattenThread(
|
||||||
post: PostThreadViewPostModel,
|
post: PostThreadViewPostModel,
|
||||||
|
|||||||
@@ -18,95 +18,240 @@ import {PostMeta} from '../util/PostMeta'
|
|||||||
import {PostEmbeds} from '../util/PostEmbeds'
|
import {PostEmbeds} from '../util/PostEmbeds'
|
||||||
import {PostCtrls} from '../util/PostCtrls'
|
import {PostCtrls} from '../util/PostCtrls'
|
||||||
import {ComposePrompt} from '../composer/Prompt'
|
import {ComposePrompt} from '../composer/Prompt'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
const PARENT_REPLY_LINE_LENGTH = 8
|
const PARENT_REPLY_LINE_LENGTH = 8
|
||||||
const REPLYING_TO_LINE_LENGTH = 6
|
const REPLYING_TO_LINE_LENGTH = 6
|
||||||
|
|
||||||
export const PostThreadItem = observer(function PostThreadItem({
|
export const PostThreadItem = register(
|
||||||
item,
|
observer(function PostThreadItem({
|
||||||
onPostReply,
|
item,
|
||||||
}: {
|
onPostReply,
|
||||||
item: PostThreadViewPostModel
|
}: {
|
||||||
onPostReply: () => void
|
item: PostThreadViewPostModel
|
||||||
}) {
|
onPostReply: () => void
|
||||||
const store = useStores()
|
}) {
|
||||||
const [deleted, setDeleted] = useState(false)
|
const store = useStores()
|
||||||
const record = item.record as unknown as PostType.Record
|
const [deleted, setDeleted] = useState(false)
|
||||||
const hasEngagement = item.upvoteCount || item.repostCount
|
const record = item.record as unknown as PostType.Record
|
||||||
|
const hasEngagement = item.upvoteCount || item.repostCount
|
||||||
|
|
||||||
const itemHref = useMemo(() => {
|
const itemHref = useMemo(() => {
|
||||||
const urip = new AtUri(item.uri)
|
const urip = new AtUri(item.uri)
|
||||||
return `/profile/${item.author.handle}/post/${urip.rkey}`
|
return `/profile/${item.author.handle}/post/${urip.rkey}`
|
||||||
}, [item.uri, item.author.handle])
|
}, [item.uri, item.author.handle])
|
||||||
const itemTitle = `Post by ${item.author.handle}`
|
const itemTitle = `Post by ${item.author.handle}`
|
||||||
const authorHref = `/profile/${item.author.handle}`
|
const authorHref = `/profile/${item.author.handle}`
|
||||||
const authorTitle = item.author.handle
|
const authorTitle = item.author.handle
|
||||||
const upvotesHref = useMemo(() => {
|
const upvotesHref = useMemo(() => {
|
||||||
const urip = new AtUri(item.uri)
|
const urip = new AtUri(item.uri)
|
||||||
return `/profile/${item.author.handle}/post/${urip.rkey}/upvoted-by`
|
return `/profile/${item.author.handle}/post/${urip.rkey}/upvoted-by`
|
||||||
}, [item.uri, item.author.handle])
|
}, [item.uri, item.author.handle])
|
||||||
const upvotesTitle = 'Upvotes on this post'
|
const upvotesTitle = 'Upvotes on this post'
|
||||||
const repostsHref = useMemo(() => {
|
const repostsHref = useMemo(() => {
|
||||||
const urip = new AtUri(item.uri)
|
const urip = new AtUri(item.uri)
|
||||||
return `/profile/${item.author.handle}/post/${urip.rkey}/reposted-by`
|
return `/profile/${item.author.handle}/post/${urip.rkey}/reposted-by`
|
||||||
}, [item.uri, item.author.handle])
|
}, [item.uri, item.author.handle])
|
||||||
const repostsTitle = 'Reposts of this post'
|
const repostsTitle = 'Reposts of this post'
|
||||||
|
|
||||||
const onPressReply = () => {
|
const onPressReply = () => {
|
||||||
store.shell.openComposer({
|
store.shell.openComposer({
|
||||||
replyTo: {
|
replyTo: {
|
||||||
uri: item.uri,
|
uri: item.uri,
|
||||||
cid: item.cid,
|
cid: item.cid,
|
||||||
text: item.record.text as string,
|
text: item.record.text as string,
|
||||||
author: {
|
author: {
|
||||||
handle: item.author.handle,
|
handle: item.author.handle,
|
||||||
displayName: item.author.displayName,
|
displayName: item.author.displayName,
|
||||||
avatar: item.author.avatar,
|
avatar: item.author.avatar,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
onPost: onPostReply,
|
||||||
onPost: onPostReply,
|
})
|
||||||
})
|
}
|
||||||
}
|
const onPressToggleRepost = () => {
|
||||||
const onPressToggleRepost = () => {
|
item
|
||||||
item
|
.toggleRepost()
|
||||||
.toggleRepost()
|
.catch(e => console.error('Failed to toggle repost', record, e))
|
||||||
.catch(e => console.error('Failed to toggle repost', record, e))
|
}
|
||||||
}
|
const onPressToggleUpvote = () => {
|
||||||
const onPressToggleUpvote = () => {
|
item
|
||||||
item
|
.toggleUpvote()
|
||||||
.toggleUpvote()
|
.catch(e => console.error('Failed to toggle upvote', record, e))
|
||||||
.catch(e => console.error('Failed to toggle upvote', record, e))
|
}
|
||||||
}
|
const onCopyPostText = () => {
|
||||||
const onCopyPostText = () => {
|
Clipboard.setString(record.text)
|
||||||
Clipboard.setString(record.text)
|
Toast.show('Copied to clipboard')
|
||||||
Toast.show('Copied to clipboard')
|
}
|
||||||
}
|
const onDeletePost = () => {
|
||||||
const onDeletePost = () => {
|
item.delete().then(
|
||||||
item.delete().then(
|
() => {
|
||||||
() => {
|
setDeleted(true)
|
||||||
setDeleted(true)
|
Toast.show('Post deleted')
|
||||||
Toast.show('Post deleted')
|
},
|
||||||
},
|
e => {
|
||||||
e => {
|
console.error(e)
|
||||||
console.error(e)
|
Toast.show('Failed to delete post, please try again')
|
||||||
Toast.show('Failed to delete post, please try again')
|
},
|
||||||
},
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.outer, s.p20, s.flexRow]}>
|
<View style={[styles.outer, s.p20, s.flexRow]}>
|
||||||
<FontAwesomeIcon icon={['far', 'trash-can']} style={[s.gray4]} />
|
<FontAwesomeIcon icon={['far', 'trash-can']} style={[s.gray4]} />
|
||||||
<Text style={[s.gray5, s.ml10]}>This post has been deleted.</Text>
|
<Text style={[s.gray5, s.ml10]}>This post has been deleted.</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item._isHighlightedPost) {
|
if (item._isHighlightedPost) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View style={styles.outer}>
|
<View style={styles.outer}>
|
||||||
|
<View style={styles.layout}>
|
||||||
|
<View style={styles.layoutAvi}>
|
||||||
|
<Link href={authorHref} title={authorTitle}>
|
||||||
|
<UserAvatar
|
||||||
|
size={50}
|
||||||
|
displayName={item.author.displayName}
|
||||||
|
handle={item.author.handle}
|
||||||
|
avatar={item.author.avatar}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
<View style={styles.layoutContent}>
|
||||||
|
<View style={[styles.meta, {paddingTop: 5, paddingBottom: 0}]}>
|
||||||
|
<Link
|
||||||
|
style={styles.metaItem}
|
||||||
|
href={authorHref}
|
||||||
|
title={authorTitle}>
|
||||||
|
<Text style={[s.f16, s.bold]} numberOfLines={1}>
|
||||||
|
{item.author.displayName || item.author.handle}
|
||||||
|
</Text>
|
||||||
|
</Link>
|
||||||
|
<Text style={[styles.metaItem, s.f15, s.gray5]}>
|
||||||
|
· {ago(item.indexedAt)}
|
||||||
|
</Text>
|
||||||
|
<View style={s.flex1} />
|
||||||
|
<PostDropdownBtn
|
||||||
|
style={styles.metaItem}
|
||||||
|
itemHref={itemHref}
|
||||||
|
itemTitle={itemTitle}
|
||||||
|
isAuthor={item.author.did === store.me.did}
|
||||||
|
onCopyPostText={onCopyPostText}
|
||||||
|
onDeletePost={onDeletePost}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="ellipsis-h"
|
||||||
|
size={14}
|
||||||
|
style={[s.mt2, s.mr5]}
|
||||||
|
/>
|
||||||
|
</PostDropdownBtn>
|
||||||
|
</View>
|
||||||
|
<View style={styles.meta}>
|
||||||
|
<Link
|
||||||
|
style={styles.metaItem}
|
||||||
|
href={authorHref}
|
||||||
|
title={authorTitle}>
|
||||||
|
<Text style={[s.f15, s.gray5]} numberOfLines={1}>
|
||||||
|
@{item.author.handle}
|
||||||
|
</Text>
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={[s.pl10, s.pr10, s.pb10]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.postTextContainer,
|
||||||
|
styles.postTextLargeContainer,
|
||||||
|
]}>
|
||||||
|
<RichText
|
||||||
|
text={record.text}
|
||||||
|
entities={record.entities}
|
||||||
|
style={[styles.postText, styles.postTextLarge]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<PostEmbeds entities={record.entities} style={s.mb10} />
|
||||||
|
{item._isHighlightedPost && hasEngagement ? (
|
||||||
|
<View style={styles.expandedInfo}>
|
||||||
|
{item.repostCount ? (
|
||||||
|
<Link
|
||||||
|
style={styles.expandedInfoItem}
|
||||||
|
href={repostsHref}
|
||||||
|
title={repostsTitle}>
|
||||||
|
<Text style={[s.gray5, s.semiBold, s.f17]}>
|
||||||
|
<Text style={[s.bold, s.black, s.f17]}>
|
||||||
|
{item.repostCount}
|
||||||
|
</Text>{' '}
|
||||||
|
{pluralize(item.repostCount, 'repost')}
|
||||||
|
</Text>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
{item.upvoteCount ? (
|
||||||
|
<Link
|
||||||
|
style={styles.expandedInfoItem}
|
||||||
|
href={upvotesHref}
|
||||||
|
title={upvotesTitle}>
|
||||||
|
<Text style={[s.gray5, s.semiBold, s.f17]}>
|
||||||
|
<Text style={[s.bold, s.black, s.f17]}>
|
||||||
|
{item.upvoteCount}
|
||||||
|
</Text>{' '}
|
||||||
|
{pluralize(item.upvoteCount, 'upvote')}
|
||||||
|
</Text>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
<View style={[s.pl10, s.pb5]}>
|
||||||
|
<PostCtrls
|
||||||
|
big
|
||||||
|
isReposted={!!item.myState.repost}
|
||||||
|
isUpvoted={!!item.myState.upvote}
|
||||||
|
onPressReply={onPressReply}
|
||||||
|
onPressToggleRepost={onPressToggleRepost}
|
||||||
|
onPressToggleUpvote={onPressToggleUpvote}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<ComposePrompt
|
||||||
|
noAvi
|
||||||
|
text="Write your reply"
|
||||||
|
btn="Reply"
|
||||||
|
onPressCompose={onPressReply}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Link style={styles.outer} href={itemHref} title={itemTitle}>
|
||||||
|
{!item.replyingTo && item.record.reply && (
|
||||||
|
<View style={styles.parentReplyLine} />
|
||||||
|
)}
|
||||||
|
{item.replies?.length !== 0 && <View style={styles.childReplyLine} />}
|
||||||
|
{item.replyingTo ? (
|
||||||
|
<View style={styles.replyingTo}>
|
||||||
|
<View style={styles.replyingToLine} />
|
||||||
|
<View style={styles.replyingToAvatar}>
|
||||||
|
<UserAvatar
|
||||||
|
handle={item.replyingTo.author.handle}
|
||||||
|
displayName={item.replyingTo.author.displayName}
|
||||||
|
avatar={item.replyingTo.author.avatar}
|
||||||
|
size={30}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.replyingToText} numberOfLines={2}>
|
||||||
|
{item.replyingTo.text}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : undefined}
|
||||||
<View style={styles.layout}>
|
<View style={styles.layout}>
|
||||||
<View style={styles.layoutAvi}>
|
<View style={styles.layoutAvi}>
|
||||||
<Link href={authorHref} title={authorTitle}>
|
<Link href={authorHref} title={authorTitle}>
|
||||||
@@ -119,94 +264,32 @@ export const PostThreadItem = observer(function PostThreadItem({
|
|||||||
</Link>
|
</Link>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.layoutContent}>
|
<View style={styles.layoutContent}>
|
||||||
<View style={[styles.meta, {paddingTop: 5, paddingBottom: 0}]}>
|
<PostMeta
|
||||||
<Link
|
itemHref={itemHref}
|
||||||
style={styles.metaItem}
|
itemTitle={itemTitle}
|
||||||
href={authorHref}
|
authorHref={authorHref}
|
||||||
title={authorTitle}>
|
authorHandle={item.author.handle}
|
||||||
<Text style={[s.f16, s.bold]} numberOfLines={1}>
|
authorDisplayName={item.author.displayName}
|
||||||
{item.author.displayName || item.author.handle}
|
timestamp={item.indexedAt}
|
||||||
</Text>
|
isAuthor={item.author.did === store.me.did}
|
||||||
</Link>
|
onCopyPostText={onCopyPostText}
|
||||||
<Text style={[styles.metaItem, s.f15, s.gray5]}>
|
onDeletePost={onDeletePost}
|
||||||
· {ago(item.indexedAt)}
|
|
||||||
</Text>
|
|
||||||
<View style={s.flex1} />
|
|
||||||
<PostDropdownBtn
|
|
||||||
style={styles.metaItem}
|
|
||||||
itemHref={itemHref}
|
|
||||||
itemTitle={itemTitle}
|
|
||||||
isAuthor={item.author.did === store.me.did}
|
|
||||||
onCopyPostText={onCopyPostText}
|
|
||||||
onDeletePost={onDeletePost}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="ellipsis-h"
|
|
||||||
size={14}
|
|
||||||
style={[s.mt2, s.mr5]}
|
|
||||||
/>
|
|
||||||
</PostDropdownBtn>
|
|
||||||
</View>
|
|
||||||
<View style={styles.meta}>
|
|
||||||
<Link
|
|
||||||
style={styles.metaItem}
|
|
||||||
href={authorHref}
|
|
||||||
title={authorTitle}>
|
|
||||||
<Text style={[s.f15, s.gray5]} numberOfLines={1}>
|
|
||||||
@{item.author.handle}
|
|
||||||
</Text>
|
|
||||||
</Link>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View style={[s.pl10, s.pr10, s.pb10]}>
|
|
||||||
<View
|
|
||||||
style={[styles.postTextContainer, styles.postTextLargeContainer]}>
|
|
||||||
<RichText
|
|
||||||
text={record.text}
|
|
||||||
entities={record.entities}
|
|
||||||
style={[styles.postText, styles.postTextLarge]}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
<View style={styles.postTextContainer}>
|
||||||
<PostEmbeds entities={record.entities} style={s.mb10} />
|
<RichText
|
||||||
{item._isHighlightedPost && hasEngagement ? (
|
text={record.text}
|
||||||
<View style={styles.expandedInfo}>
|
entities={record.entities}
|
||||||
{item.repostCount ? (
|
style={[styles.postText]}
|
||||||
<Link
|
/>
|
||||||
style={styles.expandedInfoItem}
|
|
||||||
href={repostsHref}
|
|
||||||
title={repostsTitle}>
|
|
||||||
<Text style={[s.gray5, s.semiBold, s.f17]}>
|
|
||||||
<Text style={[s.bold, s.black, s.f17]}>
|
|
||||||
{item.repostCount}
|
|
||||||
</Text>{' '}
|
|
||||||
{pluralize(item.repostCount, 'repost')}
|
|
||||||
</Text>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
{item.upvoteCount ? (
|
|
||||||
<Link
|
|
||||||
style={styles.expandedInfoItem}
|
|
||||||
href={upvotesHref}
|
|
||||||
title={upvotesTitle}>
|
|
||||||
<Text style={[s.gray5, s.semiBold, s.f17]}>
|
|
||||||
<Text style={[s.bold, s.black, s.f17]}>
|
|
||||||
{item.upvoteCount}
|
|
||||||
</Text>{' '}
|
|
||||||
{pluralize(item.upvoteCount, 'upvote')}
|
|
||||||
</Text>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
) : (
|
<PostEmbeds
|
||||||
<></>
|
entities={record.entities}
|
||||||
)}
|
style={{marginBottom: 10}}
|
||||||
<View style={[s.pl10, s.pb5]}>
|
/>
|
||||||
<PostCtrls
|
<PostCtrls
|
||||||
big
|
replyCount={item.replyCount}
|
||||||
|
repostCount={item.repostCount}
|
||||||
|
upvoteCount={item.upvoteCount}
|
||||||
isReposted={!!item.myState.repost}
|
isReposted={!!item.myState.repost}
|
||||||
isUpvoted={!!item.myState.upvote}
|
isUpvoted={!!item.myState.upvote}
|
||||||
onPressReply={onPressReply}
|
onPressReply={onPressReply}
|
||||||
@@ -215,85 +298,11 @@ export const PostThreadItem = observer(function PostThreadItem({
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</Link>
|
||||||
<ComposePrompt
|
)
|
||||||
noAvi
|
}
|
||||||
text="Write your reply"
|
}),
|
||||||
btn="Reply"
|
)
|
||||||
onPressCompose={onPressReply}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Link style={styles.outer} href={itemHref} title={itemTitle}>
|
|
||||||
{!item.replyingTo && item.record.reply && (
|
|
||||||
<View style={styles.parentReplyLine} />
|
|
||||||
)}
|
|
||||||
{item.replies?.length !== 0 && <View style={styles.childReplyLine} />}
|
|
||||||
{item.replyingTo ? (
|
|
||||||
<View style={styles.replyingTo}>
|
|
||||||
<View style={styles.replyingToLine} />
|
|
||||||
<View style={styles.replyingToAvatar}>
|
|
||||||
<UserAvatar
|
|
||||||
handle={item.replyingTo.author.handle}
|
|
||||||
displayName={item.replyingTo.author.displayName}
|
|
||||||
avatar={item.replyingTo.author.avatar}
|
|
||||||
size={30}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.replyingToText} numberOfLines={2}>
|
|
||||||
{item.replyingTo.text}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
<View style={styles.layout}>
|
|
||||||
<View style={styles.layoutAvi}>
|
|
||||||
<Link href={authorHref} title={authorTitle}>
|
|
||||||
<UserAvatar
|
|
||||||
size={50}
|
|
||||||
displayName={item.author.displayName}
|
|
||||||
handle={item.author.handle}
|
|
||||||
avatar={item.author.avatar}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</View>
|
|
||||||
<View style={styles.layoutContent}>
|
|
||||||
<PostMeta
|
|
||||||
itemHref={itemHref}
|
|
||||||
itemTitle={itemTitle}
|
|
||||||
authorHref={authorHref}
|
|
||||||
authorHandle={item.author.handle}
|
|
||||||
authorDisplayName={item.author.displayName}
|
|
||||||
timestamp={item.indexedAt}
|
|
||||||
isAuthor={item.author.did === store.me.did}
|
|
||||||
onCopyPostText={onCopyPostText}
|
|
||||||
onDeletePost={onDeletePost}
|
|
||||||
/>
|
|
||||||
<View style={styles.postTextContainer}>
|
|
||||||
<RichText
|
|
||||||
text={record.text}
|
|
||||||
entities={record.entities}
|
|
||||||
style={[styles.postText]}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<PostEmbeds entities={record.entities} style={{marginBottom: 10}} />
|
|
||||||
<PostCtrls
|
|
||||||
replyCount={item.replyCount}
|
|
||||||
repostCount={item.repostCount}
|
|
||||||
upvoteCount={item.upvoteCount}
|
|
||||||
isReposted={!!item.myState.repost}
|
|
||||||
isUpvoted={!!item.myState.upvote}
|
|
||||||
onPressReply={onPressReply}
|
|
||||||
onPressToggleRepost={onPressToggleRepost}
|
|
||||||
onPressToggleUpvote={onPressToggleUpvote}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
outer: {
|
outer: {
|
||||||
|
|||||||
@@ -10,76 +10,81 @@ import {ErrorMessage} from '../util/ErrorMessage'
|
|||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {s, colors} from '../../lib/styles'
|
import {s, colors} from '../../lib/styles'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const PostVotedBy = observer(function PostVotedBy({
|
export const PostVotedBy = register(
|
||||||
uri,
|
observer(function PostVotedBy({
|
||||||
direction,
|
uri,
|
||||||
}: {
|
direction,
|
||||||
uri: string
|
}: {
|
||||||
direction: 'up' | 'down'
|
uri: string
|
||||||
}) {
|
direction: 'up' | 'down'
|
||||||
const store = useStores()
|
}) {
|
||||||
const [view, setView] = useState<VotesViewModel | undefined>()
|
const store = useStores()
|
||||||
|
const [view, setView] = useState<VotesViewModel | undefined>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (view?.params.uri === uri) {
|
if (view?.params.uri === uri) {
|
||||||
console.log('Voted by doing nothing')
|
console.log('Voted by doing nothing')
|
||||||
return // no change needed? or trigger refresh?
|
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 = () => {
|
// loading
|
||||||
view?.refresh()
|
// =
|
||||||
}
|
if (
|
||||||
|
!view ||
|
||||||
|
(view.isLoading && !view.isRefreshing) ||
|
||||||
|
view.params.uri !== uri
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<ActivityIndicator />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// loading
|
// error
|
||||||
// =
|
// =
|
||||||
if (
|
if (view.hasError) {
|
||||||
!view ||
|
return (
|
||||||
(view.isLoading && !view.isRefreshing) ||
|
<View>
|
||||||
view.params.uri !== uri
|
<ErrorMessage
|
||||||
) {
|
dark
|
||||||
return (
|
message={view.error}
|
||||||
<View>
|
style={{margin: 6}}
|
||||||
<ActivityIndicator />
|
onPressTryAgain={onRefresh}
|
||||||
</View>
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loaded
|
||||||
|
// =
|
||||||
|
const renderItem = ({item}: {item: VotesViewItemModel}) => (
|
||||||
|
<LikedByItem item={item} />
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// error
|
|
||||||
// =
|
|
||||||
if (view.hasError) {
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ErrorMessage
|
<FlatList
|
||||||
dark
|
data={view.votes}
|
||||||
message={view.error}
|
keyExtractor={item => item._reactKey}
|
||||||
style={{margin: 6}}
|
renderItem={renderItem}
|
||||||
onPressTryAgain={onRefresh}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}),
|
||||||
|
)
|
||||||
// loaded
|
|
||||||
// =
|
|
||||||
const renderItem = ({item}: {item: VotesViewItemModel}) => (
|
|
||||||
<LikedByItem item={item} />
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<FlatList
|
|
||||||
data={view.votes}
|
|
||||||
keyExtractor={item => item._reactKey}
|
|
||||||
renderItem={renderItem}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const LikedByItem = ({item}: {item: VotesViewItemModel}) => {
|
const LikedByItem = ({item}: {item: VotesViewItemModel}) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
+164
-157
@@ -22,173 +22,180 @@ import * as Toast from '../util/Toast'
|
|||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {s, colors} from '../../lib/styles'
|
import {s, colors} from '../../lib/styles'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const Post = observer(function Post({
|
export const Post = register(
|
||||||
uri,
|
observer(function Post({
|
||||||
initView,
|
uri,
|
||||||
style,
|
initView,
|
||||||
}: {
|
style,
|
||||||
uri: string
|
}: {
|
||||||
initView?: PostThreadViewModel
|
uri: string
|
||||||
style?: StyleProp<ViewStyle>
|
initView?: PostThreadViewModel
|
||||||
}) {
|
style?: StyleProp<ViewStyle>
|
||||||
const store = useStores()
|
}) {
|
||||||
const [view, setView] = useState<PostThreadViewModel | undefined>(initView)
|
const store = useStores()
|
||||||
const [deleted, setDeleted] = useState(false)
|
const [view, setView] = useState<PostThreadViewModel | undefined>(initView)
|
||||||
|
const [deleted, setDeleted] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initView || view?.params.uri === uri) {
|
if (initView || view?.params.uri === uri) {
|
||||||
return // no change needed? or trigger refresh?
|
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 <View />
|
||||||
}
|
}
|
||||||
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
|
// loading
|
||||||
// =
|
// =
|
||||||
if (deleted) {
|
if (!view || view.isLoading || view.params.uri !== uri) {
|
||||||
return <View />
|
return (
|
||||||
}
|
<View>
|
||||||
|
<ActivityIndicator />
|
||||||
// loading
|
|
||||||
// =
|
|
||||||
if (!view || view.isLoading || view.params.uri !== uri) {
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<ActivityIndicator />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// error
|
|
||||||
// =
|
|
||||||
if (view.hasError || !view.thread) {
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<Text>{view.error || 'Thread not found'}</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (
|
|
||||||
<Link style={[styles.outer, style]} href={itemHref} title={itemTitle}>
|
|
||||||
<View style={styles.layout}>
|
|
||||||
<View style={styles.layoutAvi}>
|
|
||||||
<Link href={authorHref} title={authorTitle}>
|
|
||||||
<UserAvatar
|
|
||||||
size={50}
|
|
||||||
displayName={item.author.displayName}
|
|
||||||
handle={item.author.handle}
|
|
||||||
avatar={item.author.avatar}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.layoutContent}>
|
)
|
||||||
<PostMeta
|
}
|
||||||
itemHref={itemHref}
|
|
||||||
itemTitle={itemTitle}
|
// error
|
||||||
authorHref={authorHref}
|
// =
|
||||||
authorHandle={item.author.handle}
|
if (view.hasError || !view.thread) {
|
||||||
authorDisplayName={item.author.displayName}
|
return (
|
||||||
timestamp={item.indexedAt}
|
<View>
|
||||||
isAuthor={item.author.did === store.me.did}
|
<Text>{view.error || 'Thread not found'}</Text>
|
||||||
onCopyPostText={onCopyPostText}
|
</View>
|
||||||
onDeletePost={onDeletePost}
|
)
|
||||||
/>
|
}
|
||||||
{replyHref !== '' && (
|
|
||||||
<View style={[s.flexRow, s.mb2, {alignItems: 'center'}]}>
|
// loaded
|
||||||
<FontAwesomeIcon icon="reply" size={9} style={[s.gray4, s.mr5]} />
|
// =
|
||||||
<Text style={[s.gray4, s.f12, s.mr2]}>Reply to</Text>
|
const item = view.thread
|
||||||
<Link href={replyHref} title="Parent post">
|
const record = view.thread?.record as unknown as PostType.Record
|
||||||
<UserInfoText
|
|
||||||
did={replyAuthorDid}
|
const itemUrip = new AtUri(item.uri)
|
||||||
style={[s.f12, s.gray5]}
|
const itemHref = `/profile/${item.author.handle}/post/${itemUrip.rkey}`
|
||||||
prefix="@"
|
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 (
|
||||||
|
<Link style={[styles.outer, style]} href={itemHref} title={itemTitle}>
|
||||||
|
<View style={styles.layout}>
|
||||||
|
<View style={styles.layoutAvi}>
|
||||||
|
<Link href={authorHref} title={authorTitle}>
|
||||||
|
<UserAvatar
|
||||||
|
size={50}
|
||||||
|
displayName={item.author.displayName}
|
||||||
|
handle={item.author.handle}
|
||||||
|
avatar={item.author.avatar}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
<View style={styles.layoutContent}>
|
||||||
|
<PostMeta
|
||||||
|
itemHref={itemHref}
|
||||||
|
itemTitle={itemTitle}
|
||||||
|
authorHref={authorHref}
|
||||||
|
authorHandle={item.author.handle}
|
||||||
|
authorDisplayName={item.author.displayName}
|
||||||
|
timestamp={item.indexedAt}
|
||||||
|
isAuthor={item.author.did === store.me.did}
|
||||||
|
onCopyPostText={onCopyPostText}
|
||||||
|
onDeletePost={onDeletePost}
|
||||||
|
/>
|
||||||
|
{replyHref !== '' && (
|
||||||
|
<View style={[s.flexRow, s.mb2, {alignItems: 'center'}]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="reply"
|
||||||
|
size={9}
|
||||||
|
style={[s.gray4, s.mr5]}
|
||||||
/>
|
/>
|
||||||
</Link>
|
<Text style={[s.gray4, s.f12, s.mr2]}>Reply to</Text>
|
||||||
|
<Link href={replyHref} title="Parent post">
|
||||||
|
<UserInfoText
|
||||||
|
did={replyAuthorDid}
|
||||||
|
style={[s.f12, s.gray5]}
|
||||||
|
prefix="@"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View style={styles.postTextContainer}>
|
||||||
|
<RichText
|
||||||
|
text={record.text}
|
||||||
|
entities={record.entities}
|
||||||
|
style={styles.postText}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
<PostCtrls
|
||||||
<View style={styles.postTextContainer}>
|
replyCount={item.replyCount}
|
||||||
<RichText
|
repostCount={item.repostCount}
|
||||||
text={record.text}
|
upvoteCount={item.upvoteCount}
|
||||||
entities={record.entities}
|
isReposted={!!item.myState.repost}
|
||||||
style={styles.postText}
|
isUpvoted={!!item.myState.upvote}
|
||||||
|
onPressReply={onPressReply}
|
||||||
|
onPressToggleRepost={onPressToggleRepost}
|
||||||
|
onPressToggleUpvote={onPressToggleUpvote}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<PostCtrls
|
|
||||||
replyCount={item.replyCount}
|
|
||||||
repostCount={item.repostCount}
|
|
||||||
upvoteCount={item.upvoteCount}
|
|
||||||
isReposted={!!item.myState.repost}
|
|
||||||
isUpvoted={!!item.myState.upvote}
|
|
||||||
onPressReply={onPressReply}
|
|
||||||
onPressToggleRepost={onPressToggleRepost}
|
|
||||||
onPressToggleUpvote={onPressToggleUpvote}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</Link>
|
||||||
</Link>
|
)
|
||||||
)
|
}),
|
||||||
})
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
outer: {
|
outer: {
|
||||||
|
|||||||
@@ -5,53 +5,50 @@ import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
|
|||||||
import {ErrorMessage} from '../util/ErrorMessage'
|
import {ErrorMessage} from '../util/ErrorMessage'
|
||||||
import {PostModel} from '../../../state/models/post'
|
import {PostModel} from '../../../state/models/post'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const PostText = observer(function PostText({
|
export const PostText = register(
|
||||||
uri,
|
observer(function PostText({uri, style}: {uri: string; style?: StyleProp}) {
|
||||||
style,
|
const store = useStores()
|
||||||
}: {
|
const [model, setModel] = useState<PostModel | undefined>()
|
||||||
uri: string
|
|
||||||
style?: StyleProp
|
|
||||||
}) {
|
|
||||||
const store = useStores()
|
|
||||||
const [model, setModel] = useState<PostModel | undefined>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (model?.uri === uri) {
|
if (model?.uri === uri) {
|
||||||
return // no change needed? or trigger refresh?
|
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 (
|
||||||
|
<View>
|
||||||
|
<LoadingPlaceholder width="100%" height={8} style={{marginTop: 6}} />
|
||||||
|
<LoadingPlaceholder width="100%" height={8} style={{marginTop: 6}} />
|
||||||
|
<LoadingPlaceholder width={100} height={8} style={{marginTop: 6}} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const newModel = new PostModel(store, uri)
|
|
||||||
setModel(newModel)
|
|
||||||
newModel.setup().catch(err => console.error('Failed to fetch post', err))
|
|
||||||
}, [uri, model?.uri, store])
|
|
||||||
|
|
||||||
// loading
|
// error
|
||||||
// =
|
// =
|
||||||
if (!model || model.isLoading || model.uri !== uri) {
|
if (model.hasError) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<ErrorMessage style={style} message={model.error} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loaded
|
||||||
|
// =
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<LoadingPlaceholder width="100%" height={8} style={{marginTop: 6}} />
|
<Text style={style}>{model.text}</Text>
|
||||||
<LoadingPlaceholder width="100%" height={8} style={{marginTop: 6}} />
|
|
||||||
<LoadingPlaceholder width={100} height={8} style={{marginTop: 6}} />
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}),
|
||||||
|
)
|
||||||
// error
|
|
||||||
// =
|
|
||||||
if (model.hasError) {
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<ErrorMessage style={style} message={model.error} />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// loaded
|
|
||||||
// =
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<Text style={style}>{model.text}</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|||||||
+74
-71
@@ -7,80 +7,83 @@ import {ErrorMessage} from '../util/ErrorMessage'
|
|||||||
import {FeedModel} from '../../../state/models/feed-view'
|
import {FeedModel} from '../../../state/models/feed-view'
|
||||||
import {FeedItem} from './FeedItem'
|
import {FeedItem} from './FeedItem'
|
||||||
import {ComposePrompt} from '../composer/Prompt'
|
import {ComposePrompt} from '../composer/Prompt'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
const COMPOSE_PROMPT_ITEM = {_reactKey: '__prompt__'}
|
const COMPOSE_PROMPT_ITEM = {_reactKey: '__prompt__'}
|
||||||
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
||||||
|
|
||||||
export const Feed = observer(function Feed({
|
export const Feed = register(
|
||||||
feed,
|
observer(function Feed({
|
||||||
style,
|
feed,
|
||||||
scrollElRef,
|
style,
|
||||||
onPressCompose,
|
scrollElRef,
|
||||||
onPressTryAgain,
|
onPressCompose,
|
||||||
}: {
|
onPressTryAgain,
|
||||||
feed: FeedModel
|
}: {
|
||||||
style?: StyleProp<ViewStyle>
|
feed: FeedModel
|
||||||
scrollElRef?: MutableRefObject<FlatList<any> | null>
|
style?: StyleProp<ViewStyle>
|
||||||
onPressCompose: () => void
|
scrollElRef?: MutableRefObject<FlatList<any> | null>
|
||||||
onPressTryAgain?: () => void
|
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
|
// TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf
|
||||||
// renderItem function renders components that follow React performance best practices
|
// VirtualizedList: You have a large list that is slow to update - make sure your
|
||||||
// like PureComponent, shouldComponentUpdate, etc
|
// renderItem function renders components that follow React performance best practices
|
||||||
const renderItem = ({item}: {item: any}) => {
|
// like PureComponent, shouldComponentUpdate, etc
|
||||||
if (item === COMPOSE_PROMPT_ITEM) {
|
const renderItem = ({item}: {item: any}) => {
|
||||||
return <ComposePrompt onPressCompose={onPressCompose} />
|
if (item === COMPOSE_PROMPT_ITEM) {
|
||||||
} else if (item === EMPTY_FEED_ITEM) {
|
return <ComposePrompt onPressCompose={onPressCompose} />
|
||||||
return (
|
} else if (item === EMPTY_FEED_ITEM) {
|
||||||
<EmptyState
|
return (
|
||||||
icon="bars"
|
<EmptyState
|
||||||
message="This feed is empty!"
|
icon="bars"
|
||||||
style={{paddingVertical: 40}}
|
message="This feed is empty!"
|
||||||
/>
|
style={{paddingVertical: 40}}
|
||||||
)
|
/>
|
||||||
} else {
|
)
|
||||||
return <FeedItem item={item} />
|
} else {
|
||||||
|
return <FeedItem item={item} />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
const onRefresh = () => {
|
||||||
const onRefresh = () => {
|
feed.refresh().catch(err => console.error('Failed to refresh', err))
|
||||||
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 onEndReached = () => {
|
||||||
return (
|
feed.loadMore().catch(err => console.error('Failed to load more', err))
|
||||||
<View style={style}>
|
}
|
||||||
{!data && <ComposePrompt onPressCompose={onPressCompose} />}
|
let data
|
||||||
{feed.isLoading && !data && <PostFeedLoadingPlaceholder />}
|
if (feed.hasLoaded) {
|
||||||
{feed.hasError && (
|
if (feed.isEmpty) {
|
||||||
<ErrorMessage
|
data = [COMPOSE_PROMPT_ITEM, EMPTY_FEED_ITEM]
|
||||||
dark
|
} else {
|
||||||
message={feed.error}
|
data = [COMPOSE_PROMPT_ITEM].concat(feed.feed)
|
||||||
style={{margin: 6}}
|
}
|
||||||
onPressTryAgain={onPressTryAgain}
|
}
|
||||||
/>
|
return (
|
||||||
)}
|
<View style={style}>
|
||||||
{feed.hasLoaded && data && (
|
{!data && <ComposePrompt onPressCompose={onPressCompose} />}
|
||||||
<FlatList
|
{feed.isLoading && !data && <PostFeedLoadingPlaceholder />}
|
||||||
ref={scrollElRef}
|
{feed.hasError && (
|
||||||
data={data}
|
<ErrorMessage
|
||||||
keyExtractor={item => item._reactKey}
|
dark
|
||||||
renderItem={renderItem}
|
message={feed.error}
|
||||||
refreshing={feed.isRefreshing}
|
style={{margin: 6}}
|
||||||
contentContainerStyle={{paddingBottom: 100}}
|
onPressTryAgain={onPressTryAgain}
|
||||||
onRefresh={onRefresh}
|
/>
|
||||||
onEndReached={onEndReached}
|
)}
|
||||||
/>
|
{feed.hasLoaded && data && (
|
||||||
)}
|
<FlatList
|
||||||
</View>
|
ref={scrollElRef}
|
||||||
)
|
data={data}
|
||||||
})
|
keyExtractor={item => item._reactKey}
|
||||||
|
renderItem={renderItem}
|
||||||
|
refreshing={feed.isRefreshing}
|
||||||
|
contentContainerStyle={{paddingBottom: 100}}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
onEndReached={onEndReached}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|||||||
+187
-182
@@ -16,204 +16,209 @@ import * as Toast from '../util/Toast'
|
|||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
import {s, colors} from '../../lib/styles'
|
import {s, colors} from '../../lib/styles'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
const TOP_REPLY_LINE_LENGTH = 12
|
const TOP_REPLY_LINE_LENGTH = 12
|
||||||
const REPLYING_TO_LINE_LENGTH = 8
|
const REPLYING_TO_LINE_LENGTH = 8
|
||||||
|
|
||||||
export const FeedItem = observer(function FeedItem({
|
export const FeedItem = register(
|
||||||
item,
|
observer(function FeedItem({item}: {item: FeedItemModel}) {
|
||||||
}: {
|
const store = useStores()
|
||||||
item: FeedItemModel
|
const [deleted, setDeleted] = useState(false)
|
||||||
}) {
|
const record = item.record as unknown as PostType.Record
|
||||||
const store = useStores()
|
const itemHref = useMemo(() => {
|
||||||
const [deleted, setDeleted] = useState(false)
|
const urip = new AtUri(item.uri)
|
||||||
const record = item.record as unknown as PostType.Record
|
return `/profile/${item.author.handle}/post/${urip.rkey}`
|
||||||
const itemHref = useMemo(() => {
|
}, [item.uri, item.author.handle])
|
||||||
const urip = new AtUri(item.uri)
|
const itemTitle = `Post by ${item.author.handle}`
|
||||||
return `/profile/${item.author.handle}/post/${urip.rkey}`
|
const authorHref = `/profile/${item.author.handle}`
|
||||||
}, [item.uri, item.author.handle])
|
const replyAuthorDid = useMemo(() => {
|
||||||
const itemTitle = `Post by ${item.author.handle}`
|
if (!record.reply) {
|
||||||
const authorHref = `/profile/${item.author.handle}`
|
return ''
|
||||||
const replyAuthorDid = useMemo(() => {
|
}
|
||||||
if (!record.reply) return ''
|
const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri)
|
||||||
const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri)
|
return urip.hostname
|
||||||
return urip.hostname
|
}, [record.reply])
|
||||||
}, [record.reply])
|
const replyHref = useMemo(() => {
|
||||||
const replyHref = useMemo(() => {
|
if (!record.reply) {
|
||||||
if (!record.reply) return ''
|
return ''
|
||||||
const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri)
|
}
|
||||||
return `/profile/${urip.hostname}/post/${urip.rkey}`
|
const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri)
|
||||||
}, [record.reply])
|
return `/profile/${urip.hostname}/post/${urip.rkey}`
|
||||||
|
}, [record.reply])
|
||||||
|
|
||||||
const onPressReply = () => {
|
const onPressReply = () => {
|
||||||
store.shell.openComposer({
|
store.shell.openComposer({
|
||||||
replyTo: {
|
replyTo: {
|
||||||
uri: item.uri,
|
uri: item.uri,
|
||||||
cid: item.cid,
|
cid: item.cid,
|
||||||
text: item.record.text as string,
|
text: item.record.text as string,
|
||||||
author: {
|
author: {
|
||||||
handle: item.author.handle,
|
handle: item.author.handle,
|
||||||
displayName: item.author.displayName,
|
displayName: item.author.displayName,
|
||||||
avatar: item.author.avatar,
|
avatar: item.author.avatar,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
}
|
||||||
}
|
const onPressToggleRepost = () => {
|
||||||
const onPressToggleRepost = () => {
|
item
|
||||||
item
|
.toggleRepost()
|
||||||
.toggleRepost()
|
.catch(e => console.error('Failed to toggle repost', record, e))
|
||||||
.catch(e => console.error('Failed to toggle repost', record, e))
|
}
|
||||||
}
|
const onPressToggleUpvote = () => {
|
||||||
const onPressToggleUpvote = () => {
|
item
|
||||||
item
|
.toggleUpvote()
|
||||||
.toggleUpvote()
|
.catch(e => console.error('Failed to toggle upvote', record, e))
|
||||||
.catch(e => console.error('Failed to toggle upvote', record, e))
|
}
|
||||||
}
|
const onCopyPostText = () => {
|
||||||
const onCopyPostText = () => {
|
Clipboard.setString(record.text)
|
||||||
Clipboard.setString(record.text)
|
Toast.show('Copied to clipboard')
|
||||||
Toast.show('Copied to clipboard')
|
}
|
||||||
}
|
const onDeletePost = () => {
|
||||||
const onDeletePost = () => {
|
item.delete().then(
|
||||||
item.delete().then(
|
() => {
|
||||||
() => {
|
setDeleted(true)
|
||||||
setDeleted(true)
|
Toast.show('Post deleted')
|
||||||
Toast.show('Post deleted')
|
},
|
||||||
},
|
e => {
|
||||||
e => {
|
console.error(e)
|
||||||
console.error(e)
|
Toast.show('Failed to delete post, please try again')
|
||||||
Toast.show('Failed to delete post, please try again')
|
},
|
||||||
},
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
return <View />
|
return <View />
|
||||||
}
|
}
|
||||||
|
|
||||||
const outerStyles = [
|
const outerStyles = [
|
||||||
styles.outer,
|
styles.outer,
|
||||||
item._isThreadChild ? styles.outerNoTop : undefined,
|
item._isThreadChild ? styles.outerNoTop : undefined,
|
||||||
item._isThreadParent ? styles.outerNoBottom : undefined,
|
item._isThreadParent ? styles.outerNoBottom : undefined,
|
||||||
]
|
]
|
||||||
return (
|
return (
|
||||||
<Link style={outerStyles} href={itemHref} title={itemTitle}>
|
<Link style={outerStyles} href={itemHref} title={itemTitle}>
|
||||||
{item._isThreadChild && <View style={styles.topReplyLine} />}
|
{item._isThreadChild && <View style={styles.topReplyLine} />}
|
||||||
{item._isThreadParent && (
|
{item._isThreadParent && (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.bottomReplyLine,
|
styles.bottomReplyLine,
|
||||||
item._isThreadChild ? styles.bottomReplyLineSmallAvi : undefined,
|
item._isThreadChild ? styles.bottomReplyLineSmallAvi : undefined,
|
||||||
]}
|
]}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{item.repostedBy && (
|
|
||||||
<Link
|
|
||||||
style={styles.includeReason}
|
|
||||||
href={`/profile/${item.repostedBy.handle}`}
|
|
||||||
title={item.repostedBy.displayName || item.repostedBy.handle}>
|
|
||||||
<FontAwesomeIcon icon="retweet" style={styles.includeReasonIcon} />
|
|
||||||
<Text style={[s.gray4, s.bold, s.f13]}>
|
|
||||||
Reposted by {item.repostedBy.displayName || item.repostedBy.handle}
|
|
||||||
</Text>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{item.trendedBy && (
|
|
||||||
<Link
|
|
||||||
style={styles.includeReason}
|
|
||||||
href={`/profile/${item.trendedBy.handle}`}
|
|
||||||
title={item.trendedBy.displayName || item.trendedBy.handle}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="arrow-trend-up"
|
|
||||||
style={styles.includeReasonIcon}
|
|
||||||
/>
|
/>
|
||||||
<Text style={[s.gray4, s.bold, s.f13]}>
|
)}
|
||||||
Trending with {item.trendedBy.displayName || item.trendedBy.handle}
|
{item.repostedBy && (
|
||||||
</Text>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{item.additionalParentPost ? (
|
|
||||||
<View style={styles.replyingTo}>
|
|
||||||
<View style={styles.replyingToLine} />
|
|
||||||
<View style={styles.replyingToAvatar}>
|
|
||||||
<UserAvatar
|
|
||||||
handle={item.additionalParentPost?.thread?.author.handle}
|
|
||||||
displayName={
|
|
||||||
item.additionalParentPost?.thread?.author.displayName
|
|
||||||
}
|
|
||||||
avatar={item.additionalParentPost?.thread?.author.avatar}
|
|
||||||
size={32}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={styles.replyingToTextContainer}>
|
|
||||||
<Text style={styles.replyingToText} numberOfLines={2}>
|
|
||||||
{item.additionalParentPost?.thread?.record.text}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
<View style={styles.layout}>
|
|
||||||
<View style={styles.layoutAvi}>
|
|
||||||
<Link
|
<Link
|
||||||
href={authorHref}
|
style={styles.includeReason}
|
||||||
title={item.author.handle}
|
href={`/profile/${item.repostedBy.handle}`}
|
||||||
style={item._isThreadChild ? {marginLeft: 10} : undefined}>
|
title={item.repostedBy.displayName || item.repostedBy.handle}>
|
||||||
<UserAvatar
|
<FontAwesomeIcon icon="retweet" style={styles.includeReasonIcon} />
|
||||||
size={item._isThreadChild ? 30 : 50}
|
<Text style={[s.gray4, s.bold, s.f13]}>
|
||||||
displayName={item.author.displayName}
|
Reposted by{' '}
|
||||||
handle={item.author.handle}
|
{item.repostedBy.displayName || item.repostedBy.handle}
|
||||||
avatar={item.author.avatar}
|
</Text>
|
||||||
/>
|
|
||||||
</Link>
|
</Link>
|
||||||
</View>
|
)}
|
||||||
<View style={styles.layoutContent}>
|
{item.trendedBy && (
|
||||||
{!item._isThreadChild ? (
|
<Link
|
||||||
<PostMeta
|
style={styles.includeReason}
|
||||||
itemHref={itemHref}
|
href={`/profile/${item.trendedBy.handle}`}
|
||||||
itemTitle={itemTitle}
|
title={item.trendedBy.displayName || item.trendedBy.handle}>
|
||||||
authorHref={authorHref}
|
<FontAwesomeIcon
|
||||||
authorHandle={item.author.handle}
|
icon="arrow-trend-up"
|
||||||
authorDisplayName={item.author.displayName}
|
style={styles.includeReasonIcon}
|
||||||
timestamp={item.indexedAt}
|
|
||||||
isAuthor={item.author.did === store.me.did}
|
|
||||||
onCopyPostText={onCopyPostText}
|
|
||||||
onDeletePost={onDeletePost}
|
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
<Text style={[s.gray4, s.bold, s.f13]}>
|
||||||
{!item._isThreadChild && replyHref !== '' && (
|
Trending with{' '}
|
||||||
<View style={[s.flexRow, s.mb5, {alignItems: 'center'}]}>
|
{item.trendedBy.displayName || item.trendedBy.handle}
|
||||||
<Text style={[s.gray5, s.f15, s.mr2]}>Replying to</Text>
|
</Text>
|
||||||
<Link href={replyHref} title="Parent post">
|
</Link>
|
||||||
<UserInfoText
|
)}
|
||||||
did={replyAuthorDid}
|
{item.additionalParentPost ? (
|
||||||
style={[s.f15, s.blue3]}
|
<View style={styles.replyingTo}>
|
||||||
prefix="@"
|
<View style={styles.replyingToLine} />
|
||||||
/>
|
<View style={styles.replyingToAvatar}>
|
||||||
</Link>
|
<UserAvatar
|
||||||
|
handle={item.additionalParentPost?.thread?.author.handle}
|
||||||
|
displayName={
|
||||||
|
item.additionalParentPost?.thread?.author.displayName
|
||||||
|
}
|
||||||
|
avatar={item.additionalParentPost?.thread?.author.avatar}
|
||||||
|
size={32}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
<View style={styles.replyingToTextContainer}>
|
||||||
<View style={styles.postTextContainer}>
|
<Text style={styles.replyingToText} numberOfLines={2}>
|
||||||
<RichText
|
{item.additionalParentPost?.thread?.record.text}
|
||||||
text={record.text}
|
</Text>
|
||||||
entities={record.entities}
|
</View>
|
||||||
style={styles.postText}
|
</View>
|
||||||
|
) : undefined}
|
||||||
|
<View style={styles.layout}>
|
||||||
|
<View style={styles.layoutAvi}>
|
||||||
|
<Link
|
||||||
|
href={authorHref}
|
||||||
|
title={item.author.handle}
|
||||||
|
style={item._isThreadChild ? {marginLeft: 10} : undefined}>
|
||||||
|
<UserAvatar
|
||||||
|
size={item._isThreadChild ? 30 : 50}
|
||||||
|
displayName={item.author.displayName}
|
||||||
|
handle={item.author.handle}
|
||||||
|
avatar={item.author.avatar}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
<View style={styles.layoutContent}>
|
||||||
|
{!item._isThreadChild ? (
|
||||||
|
<PostMeta
|
||||||
|
itemHref={itemHref}
|
||||||
|
itemTitle={itemTitle}
|
||||||
|
authorHref={authorHref}
|
||||||
|
authorHandle={item.author.handle}
|
||||||
|
authorDisplayName={item.author.displayName}
|
||||||
|
timestamp={item.indexedAt}
|
||||||
|
isAuthor={item.author.did === store.me.did}
|
||||||
|
onCopyPostText={onCopyPostText}
|
||||||
|
onDeletePost={onDeletePost}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
{!item._isThreadChild && replyHref !== '' && (
|
||||||
|
<View style={[s.flexRow, s.mb5, {alignItems: 'center'}]}>
|
||||||
|
<Text style={[s.gray5, s.f15, s.mr2]}>Replying to</Text>
|
||||||
|
<Link href={replyHref} title="Parent post">
|
||||||
|
<UserInfoText
|
||||||
|
did={replyAuthorDid}
|
||||||
|
style={[s.f15, s.blue3]}
|
||||||
|
prefix="@"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View style={styles.postTextContainer}>
|
||||||
|
<RichText
|
||||||
|
text={record.text}
|
||||||
|
entities={record.entities}
|
||||||
|
style={styles.postText}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<PostEmbeds entities={record.entities} style={{marginBottom: 10}} />
|
||||||
|
<PostCtrls
|
||||||
|
replyCount={item.replyCount}
|
||||||
|
repostCount={item.repostCount}
|
||||||
|
upvoteCount={item.upvoteCount}
|
||||||
|
isReposted={!!item.myState.repost}
|
||||||
|
isUpvoted={!!item.myState.upvote}
|
||||||
|
onPressReply={onPressReply}
|
||||||
|
onPressToggleRepost={onPressToggleRepost}
|
||||||
|
onPressToggleUpvote={onPressToggleUpvote}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<PostEmbeds entities={record.entities} style={{marginBottom: 10}} />
|
|
||||||
<PostCtrls
|
|
||||||
replyCount={item.replyCount}
|
|
||||||
repostCount={item.repostCount}
|
|
||||||
upvoteCount={item.upvoteCount}
|
|
||||||
isReposted={!!item.myState.repost}
|
|
||||||
isUpvoted={!!item.myState.upvote}
|
|
||||||
onPressReply={onPressReply}
|
|
||||||
onPressToggleRepost={onPressToggleRepost}
|
|
||||||
onPressToggleUpvote={onPressToggleUpvote}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</Link>
|
||||||
</Link>
|
)
|
||||||
)
|
}),
|
||||||
})
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
outer: {
|
outer: {
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
|
|||||||
import {Link} from '../util/Link'
|
import {Link} from '../util/Link'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
import {s, colors} from '../../lib/styles'
|
import {s, colors} from '../../lib/styles'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export function ProfileCard({
|
export const ProfileCard = register(function ProfileCard({
|
||||||
did,
|
did,
|
||||||
handle,
|
handle,
|
||||||
displayName,
|
displayName,
|
||||||
@@ -48,7 +49,7 @@ export function ProfileCard({
|
|||||||
</View>
|
</View>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
outer: {
|
outer: {
|
||||||
|
|||||||
@@ -10,74 +10,73 @@ import {ErrorMessage} from '../util/ErrorMessage'
|
|||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {s, colors} from '../../lib/styles'
|
import {s, colors} from '../../lib/styles'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const ProfileFollowers = observer(function ProfileFollowers({
|
export const ProfileFollowers = register(
|
||||||
name,
|
observer(function ProfileFollowers({name}: {name: string}) {
|
||||||
}: {
|
const store = useStores()
|
||||||
name: string
|
const [view, setView] = useState<UserFollowersViewModel | undefined>()
|
||||||
}) {
|
|
||||||
const store = useStores()
|
|
||||||
const [view, setView] = useState<UserFollowersViewModel | undefined>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (view?.params.user === name) {
|
if (view?.params.user === name) {
|
||||||
console.log('User followers doing nothing')
|
console.log('User followers doing nothing')
|
||||||
return // no change needed? or trigger refresh?
|
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 = () => {
|
// loading
|
||||||
view?.refresh()
|
// =
|
||||||
}
|
if (
|
||||||
|
!view ||
|
||||||
|
(view.isLoading && !view.isRefreshing) ||
|
||||||
|
view.params.user !== name
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<ActivityIndicator />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// loading
|
// error
|
||||||
// =
|
// =
|
||||||
if (
|
if (view.hasError) {
|
||||||
!view ||
|
return (
|
||||||
(view.isLoading && !view.isRefreshing) ||
|
<View>
|
||||||
view.params.user !== name
|
<ErrorMessage
|
||||||
) {
|
dark
|
||||||
|
message={view.error}
|
||||||
|
style={{margin: 6}}
|
||||||
|
onPressTryAgain={onRefresh}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loaded
|
||||||
|
// =
|
||||||
|
const renderItem = ({item}: {item: FollowerItem}) => <User item={item} />
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ActivityIndicator />
|
<FlatList
|
||||||
</View>
|
data={view.followers}
|
||||||
)
|
keyExtractor={item => item._reactKey}
|
||||||
}
|
renderItem={renderItem}
|
||||||
|
|
||||||
// error
|
|
||||||
// =
|
|
||||||
if (view.hasError) {
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<ErrorMessage
|
|
||||||
dark
|
|
||||||
message={view.error}
|
|
||||||
style={{margin: 6}}
|
|
||||||
onPressTryAgain={onRefresh}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}),
|
||||||
|
)
|
||||||
// loaded
|
|
||||||
// =
|
|
||||||
const renderItem = ({item}: {item: FollowerItem}) => <User item={item} />
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<FlatList
|
|
||||||
data={view.followers}
|
|
||||||
keyExtractor={item => item._reactKey}
|
|
||||||
renderItem={renderItem}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const User = ({item}: {item: FollowerItem}) => {
|
const User = ({item}: {item: FollowerItem}) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -10,74 +10,73 @@ import {Link} from '../util/Link'
|
|||||||
import {ErrorMessage} from '../util/ErrorMessage'
|
import {ErrorMessage} from '../util/ErrorMessage'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
import {s, colors} from '../../lib/styles'
|
import {s, colors} from '../../lib/styles'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const ProfileFollows = observer(function ProfileFollows({
|
export const ProfileFollows = register(
|
||||||
name,
|
observer(function ProfileFollows({name}: {name: string}) {
|
||||||
}: {
|
const store = useStores()
|
||||||
name: string
|
const [view, setView] = useState<UserFollowsViewModel | undefined>()
|
||||||
}) {
|
|
||||||
const store = useStores()
|
|
||||||
const [view, setView] = useState<UserFollowsViewModel | undefined>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (view?.params.user === name) {
|
if (view?.params.user === name) {
|
||||||
console.log('User follows doing nothing')
|
console.log('User follows doing nothing')
|
||||||
return // no change needed? or trigger refresh?
|
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 = () => {
|
// loading
|
||||||
view?.refresh()
|
// =
|
||||||
}
|
if (
|
||||||
|
!view ||
|
||||||
|
(view.isLoading && !view.isRefreshing) ||
|
||||||
|
view.params.user !== name
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<ActivityIndicator />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// loading
|
// error
|
||||||
// =
|
// =
|
||||||
if (
|
if (view.hasError) {
|
||||||
!view ||
|
return (
|
||||||
(view.isLoading && !view.isRefreshing) ||
|
<View>
|
||||||
view.params.user !== name
|
<ErrorMessage
|
||||||
) {
|
dark
|
||||||
|
message={view.error}
|
||||||
|
style={{margin: 6}}
|
||||||
|
onPressTryAgain={onRefresh}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loaded
|
||||||
|
// =
|
||||||
|
const renderItem = ({item}: {item: FollowItem}) => <User item={item} />
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ActivityIndicator />
|
<FlatList
|
||||||
</View>
|
data={view.follows}
|
||||||
)
|
keyExtractor={item => item._reactKey}
|
||||||
}
|
renderItem={renderItem}
|
||||||
|
|
||||||
// error
|
|
||||||
// =
|
|
||||||
if (view.hasError) {
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<ErrorMessage
|
|
||||||
dark
|
|
||||||
message={view.error}
|
|
||||||
style={{margin: 6}}
|
|
||||||
onPressTryAgain={onRefresh}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}),
|
||||||
|
)
|
||||||
// loaded
|
|
||||||
// =
|
|
||||||
const renderItem = ({item}: {item: FollowItem}) => <User item={item} />
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<FlatList
|
|
||||||
data={view.follows}
|
|
||||||
keyExtractor={item => item._reactKey}
|
|
||||||
renderItem={renderItem}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const User = ({item}: {item: FollowItem}) => {
|
const User = ({item}: {item: FollowItem}) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -21,292 +21,298 @@ import {RichText} from '../util/RichText'
|
|||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
import {UserBanner} from '../util/UserBanner'
|
import {UserBanner} from '../util/UserBanner'
|
||||||
import {UserInfoText} from '../util/UserInfoText'
|
import {UserInfoText} from '../util/UserInfoText'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const ProfileHeader = observer(function ProfileHeader({
|
export const ProfileHeader = register(
|
||||||
view,
|
observer(function ProfileHeader({
|
||||||
onRefreshAll,
|
view,
|
||||||
}: {
|
onRefreshAll,
|
||||||
view: ProfileViewModel
|
}: {
|
||||||
onRefreshAll: () => void
|
view: ProfileViewModel
|
||||||
}) {
|
onRefreshAll: () => void
|
||||||
const store = useStores()
|
}) {
|
||||||
const isMember = useMemo(
|
const store = useStores()
|
||||||
() => view.isScene && view.myState.member,
|
const isMember = useMemo(
|
||||||
[view.myState.member],
|
() => view.isScene && view.myState.member,
|
||||||
)
|
[view.myState.member],
|
||||||
|
)
|
||||||
|
|
||||||
const onPressBack = () => {
|
const onPressBack = () => {
|
||||||
store.nav.tab.goBack()
|
store.nav.tab.goBack()
|
||||||
}
|
}
|
||||||
const onPressSearch = () => {
|
const onPressSearch = () => {
|
||||||
store.nav.navigate(`/search`)
|
store.nav.navigate('/search')
|
||||||
}
|
}
|
||||||
const onPressToggleFollow = () => {
|
const onPressToggleFollow = () => {
|
||||||
view?.toggleFollowing().then(
|
view?.toggleFollowing().then(
|
||||||
() => {
|
() => {
|
||||||
Toast.show(
|
Toast.show(
|
||||||
`${view.myState.follow ? 'Following' : 'No longer following'} ${
|
`${view.myState.follow ? 'Following' : 'No longer following'} ${
|
||||||
view.displayName || view.handle
|
view.displayName || view.handle
|
||||||
}`,
|
}`,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
err => console.error('Failed to toggle follow', err),
|
err => console.error('Failed to toggle follow', err),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const onPressEditProfile = () => {
|
const onPressEditProfile = () => {
|
||||||
store.shell.openModal(new EditProfileModel(view, onRefreshAll))
|
store.shell.openModal(new EditProfileModel(view, onRefreshAll))
|
||||||
}
|
}
|
||||||
const onPressFollowers = () => {
|
const onPressFollowers = () => {
|
||||||
store.nav.navigate(`/profile/${view.handle}/followers`)
|
store.nav.navigate(`/profile/${view.handle}/followers`)
|
||||||
}
|
}
|
||||||
const onPressFollows = () => {
|
const onPressFollows = () => {
|
||||||
store.nav.navigate(`/profile/${view.handle}/follows`)
|
store.nav.navigate(`/profile/${view.handle}/follows`)
|
||||||
}
|
}
|
||||||
const onPressMembers = () => {
|
const onPressMembers = () => {
|
||||||
store.nav.navigate(`/profile/${view.handle}/members`)
|
store.nav.navigate(`/profile/${view.handle}/members`)
|
||||||
}
|
}
|
||||||
const onPressInviteMembers = () => {
|
const onPressInviteMembers = () => {
|
||||||
store.shell.openModal(new InviteToSceneModel(view))
|
store.shell.openModal(new InviteToSceneModel(view))
|
||||||
}
|
}
|
||||||
const onPressLeaveScene = () => {
|
const onPressLeaveScene = () => {
|
||||||
store.shell.openModal(
|
store.shell.openModal(
|
||||||
new ConfirmModel(
|
new ConfirmModel(
|
||||||
'Leave this scene?',
|
'Leave this scene?',
|
||||||
`You'll be able to come back unless your invite is revoked.`,
|
"You'll be able to come back unless your invite is revoked.",
|
||||||
onPressConfirmLeaveScene,
|
onPressConfirmLeaveScene,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const onPressConfirmLeaveScene = async () => {
|
const onPressConfirmLeaveScene = async () => {
|
||||||
if (view.myState.member) {
|
if (view.myState.member) {
|
||||||
await store.api.app.bsky.graph.confirmation.delete({
|
await store.api.app.bsky.graph.confirmation.delete({
|
||||||
did: store.me.did || '',
|
did: store.me.did || '',
|
||||||
rkey: new AtUri(view.myState.member).rkey,
|
rkey: new AtUri(view.myState.member).rkey,
|
||||||
})
|
})
|
||||||
Toast.show(`Scene left`)
|
Toast.show('Scene left')
|
||||||
|
}
|
||||||
|
onRefreshAll()
|
||||||
}
|
}
|
||||||
onRefreshAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
// loading
|
// loading
|
||||||
// =
|
// =
|
||||||
if (!view || !view.hasLoaded) {
|
if (!view || !view.hasLoaded) {
|
||||||
|
return (
|
||||||
|
<View style={styles.outer}>
|
||||||
|
<LoadingPlaceholder width="100%" height={120} />
|
||||||
|
<View style={styles.avi}>
|
||||||
|
<LoadingPlaceholder
|
||||||
|
width={80}
|
||||||
|
height={80}
|
||||||
|
style={{borderRadius: 40}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={styles.content}>
|
||||||
|
<View style={[styles.buttonsLine]}>
|
||||||
|
<LoadingPlaceholder
|
||||||
|
width={100}
|
||||||
|
height={31}
|
||||||
|
style={{borderRadius: 50}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={styles.displayNameLine}>
|
||||||
|
<Text style={styles.displayName}>
|
||||||
|
{view.displayName || view.handle}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// error
|
||||||
|
// =
|
||||||
|
if (view.hasError) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text>{view.error}</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<View style={styles.outer}>
|
<View style={styles.outer}>
|
||||||
<LoadingPlaceholder width="100%" height={120} />
|
<UserBanner handle={view.handle} userBanner={view.userBanner} />
|
||||||
<View style={styles.avi}>
|
<View style={styles.avi}>
|
||||||
<LoadingPlaceholder
|
<UserAvatar
|
||||||
width={80}
|
size={80}
|
||||||
height={80}
|
handle={view.handle}
|
||||||
style={{borderRadius: 40}}
|
displayName={view.displayName}
|
||||||
|
avatar={view.avatar}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<View style={[styles.buttonsLine]}>
|
<View style={[styles.buttonsLine]}>
|
||||||
<LoadingPlaceholder
|
{isMe ? (
|
||||||
width={100}
|
<TouchableOpacity
|
||||||
height={31}
|
onPress={onPressEditProfile}
|
||||||
style={{borderRadius: 50}}
|
style={[styles.btn, styles.mainBtn]}>
|
||||||
/>
|
<Text style={[s.fw400, s.f14]}>Edit Profile</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{view.myState.follow ? (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onPressToggleFollow}
|
||||||
|
style={[styles.btn, styles.mainBtn]}>
|
||||||
|
<FontAwesomeIcon icon="check" style={[s.mr5]} size={14} />
|
||||||
|
<Text style={[s.fw400, s.f14]}>Following</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : (
|
||||||
|
<TouchableOpacity onPress={onPressToggleFollow}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[gradient[1], gradient[0]]}
|
||||||
|
start={{x: 0, y: 0}}
|
||||||
|
end={{x: 1, y: 1}}
|
||||||
|
style={[styles.btn, styles.gradientBtn]}>
|
||||||
|
<FontAwesomeIcon icon="plus" style={[s.white, s.mr5]} />
|
||||||
|
<Text style={[s.white, s.fw600, s.f16]}>Follow</Text>
|
||||||
|
</LinearGradient>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{view.isScene &&
|
||||||
|
(view.myState.member || view.creator === store.me.did) ? (
|
||||||
|
<DropdownBtn
|
||||||
|
items={dropdownItems}
|
||||||
|
style={[styles.btn, styles.secondaryBtn]}>
|
||||||
|
<FontAwesomeIcon icon="ellipsis" style={[s.gray5]} />
|
||||||
|
</DropdownBtn>
|
||||||
|
) : undefined}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.displayNameLine}>
|
<View style={styles.displayNameLine}>
|
||||||
<Text style={styles.displayName}>
|
<Text style={styles.displayName}>
|
||||||
{view.displayName || view.handle}
|
{view.displayName || view.handle}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
<View style={styles.handleLine}>
|
||||||
</View>
|
{view.isScene ? (
|
||||||
)
|
<View style={styles.typeLabelWrapper}>
|
||||||
}
|
<Text style={styles.typeLabel}>Scene</Text>
|
||||||
|
</View>
|
||||||
// error
|
) : undefined}
|
||||||
// =
|
<Text style={styles.handle}>@{view.handle}</Text>
|
||||||
if (view.hasError) {
|
</View>
|
||||||
return (
|
<View style={styles.metricsLine}>
|
||||||
<View>
|
|
||||||
<Text>{view.error}</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (
|
|
||||||
<View style={styles.outer}>
|
|
||||||
<UserBanner handle={view.handle} userBanner={view.userBanner} />
|
|
||||||
<View style={styles.avi}>
|
|
||||||
<UserAvatar
|
|
||||||
size={80}
|
|
||||||
handle={view.handle}
|
|
||||||
displayName={view.displayName}
|
|
||||||
avatar={view.avatar}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={styles.content}>
|
|
||||||
<View style={[styles.buttonsLine]}>
|
|
||||||
{isMe ? (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={onPressEditProfile}
|
|
||||||
style={[styles.btn, styles.mainBtn]}>
|
|
||||||
<Text style={[s.fw400, s.f14]}>Edit Profile</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{view.myState.follow ? (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={onPressToggleFollow}
|
|
||||||
style={[styles.btn, styles.mainBtn]}>
|
|
||||||
<FontAwesomeIcon icon="check" style={[s.mr5]} size={14} />
|
|
||||||
<Text style={[s.fw400, s.f14]}>Following</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
) : (
|
|
||||||
<TouchableOpacity onPress={onPressToggleFollow}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[gradient[1], gradient[0]]}
|
|
||||||
start={{x: 0, y: 0}}
|
|
||||||
end={{x: 1, y: 1}}
|
|
||||||
style={[styles.btn, styles.gradientBtn]}>
|
|
||||||
<FontAwesomeIcon icon="plus" style={[s.white, s.mr5]} />
|
|
||||||
<Text style={[s.white, s.fw600, s.f16]}>Follow</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{view.isScene &&
|
|
||||||
(view.myState.member || view.creator === store.me.did) ? (
|
|
||||||
<DropdownBtn
|
|
||||||
items={dropdownItems}
|
|
||||||
style={[styles.btn, styles.secondaryBtn]}>
|
|
||||||
<FontAwesomeIcon icon="ellipsis" style={[s.gray5]} />
|
|
||||||
</DropdownBtn>
|
|
||||||
) : undefined}
|
|
||||||
</View>
|
|
||||||
<View style={styles.displayNameLine}>
|
|
||||||
<Text style={styles.displayName}>
|
|
||||||
{view.displayName || view.handle}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.handleLine}>
|
|
||||||
{view.isScene ? (
|
|
||||||
<View style={styles.typeLabelWrapper}>
|
|
||||||
<Text style={styles.typeLabel}>Scene</Text>
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
<Text style={styles.handle}>@{view.handle}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.metricsLine}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[s.flexRow, s.mr10]}
|
|
||||||
onPress={onPressFollowers}>
|
|
||||||
<Text style={[s.bold, s.mr2, styles.metricsText]}>
|
|
||||||
{view.followersCount}
|
|
||||||
</Text>
|
|
||||||
<Text style={[s.gray5, styles.metricsText]}>
|
|
||||||
{pluralize(view.followersCount, 'follower')}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
{view.isUser ? (
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[s.flexRow, s.mr10]}
|
style={[s.flexRow, s.mr10]}
|
||||||
onPress={onPressFollows}>
|
onPress={onPressFollowers}>
|
||||||
<Text style={[s.bold, s.mr2, styles.metricsText]}>
|
<Text style={[s.bold, s.mr2, styles.metricsText]}>
|
||||||
{view.followsCount}
|
{view.followersCount}
|
||||||
</Text>
|
|
||||||
<Text style={[s.gray5, styles.metricsText]}>following</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
) : undefined}
|
|
||||||
{view.isScene ? (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[s.flexRow, s.mr10]}
|
|
||||||
onPress={onPressMembers}>
|
|
||||||
<Text style={[s.bold, s.mr2, styles.metricsText]}>
|
|
||||||
{view.membersCount}
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[s.gray5, styles.metricsText]}>
|
<Text style={[s.gray5, styles.metricsText]}>
|
||||||
{pluralize(view.membersCount, 'member')}
|
{pluralize(view.followersCount, 'follower')}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
{view.isUser ? (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[s.flexRow, s.mr10]}
|
||||||
|
onPress={onPressFollows}>
|
||||||
|
<Text style={[s.bold, s.mr2, styles.metricsText]}>
|
||||||
|
{view.followsCount}
|
||||||
|
</Text>
|
||||||
|
<Text style={[s.gray5, styles.metricsText]}>following</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : undefined}
|
||||||
|
{view.isScene ? (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[s.flexRow, s.mr10]}
|
||||||
|
onPress={onPressMembers}>
|
||||||
|
<Text style={[s.bold, s.mr2, styles.metricsText]}>
|
||||||
|
{view.membersCount}
|
||||||
|
</Text>
|
||||||
|
<Text style={[s.gray5, styles.metricsText]}>
|
||||||
|
{pluralize(view.membersCount, 'member')}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : undefined}
|
||||||
|
<View style={[s.flexRow, s.mr10]}>
|
||||||
|
<Text style={[s.bold, s.mr2, styles.metricsText]}>
|
||||||
|
{view.postsCount}
|
||||||
|
</Text>
|
||||||
|
<Text style={[s.gray5, styles.metricsText]}>
|
||||||
|
{pluralize(view.postsCount, 'post')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{view.description ? (
|
||||||
|
<RichText
|
||||||
|
style={styles.description}
|
||||||
|
numberOfLines={3}
|
||||||
|
text={view.description}
|
||||||
|
entities={view.descriptionEntities}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
{view.isScene && view.creator ? (
|
||||||
|
<View style={styles.relationshipsLine}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={['far', 'user']}
|
||||||
|
style={[s.gray5, s.mr5]}
|
||||||
|
/>
|
||||||
|
<Text style={[s.mr2, s.gray5, s.f15]}>Created by</Text>
|
||||||
|
<UserInfoText
|
||||||
|
style={[s.blue3, s.f15]}
|
||||||
|
did={view.creator}
|
||||||
|
prefix="@"
|
||||||
|
asLink
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : undefined}
|
||||||
|
{view.isScene && view.myState.member ? (
|
||||||
|
<View style={styles.relationshipsLine}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={['far', 'circle-check']}
|
||||||
|
style={[s.gray5, s.mr5]}
|
||||||
|
/>
|
||||||
|
<Text style={[s.mr2, s.gray5, s.f15]}>You are a member</Text>
|
||||||
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<View style={[s.flexRow, s.mr10]}>
|
|
||||||
<Text style={[s.bold, s.mr2, styles.metricsText]}>
|
|
||||||
{view.postsCount}
|
|
||||||
</Text>
|
|
||||||
<Text style={[s.gray5, styles.metricsText]}>
|
|
||||||
{pluralize(view.postsCount, 'post')}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
{view.description ? (
|
{view.isScene && view.creator === store.me.did ? (
|
||||||
<RichText
|
<View style={styles.sceneAdminContainer}>
|
||||||
style={styles.description}
|
<TouchableOpacity onPress={onPressInviteMembers}>
|
||||||
numberOfLines={3}
|
<LinearGradient
|
||||||
text={view.description}
|
colors={[gradient[1], gradient[0]]}
|
||||||
entities={view.descriptionEntities}
|
start={{x: 0, y: 0}}
|
||||||
/>
|
end={{x: 1, y: 1}}
|
||||||
) : undefined}
|
style={[styles.btn, styles.gradientBtn, styles.sceneAdminBtn]}>
|
||||||
{view.isScene && view.creator ? (
|
<FontAwesomeIcon
|
||||||
<View style={styles.relationshipsLine}>
|
icon="user-plus"
|
||||||
<FontAwesomeIcon icon={['far', 'user']} style={[s.gray5, s.mr5]} />
|
style={[s.mr5, s.white]}
|
||||||
<Text style={[s.mr2, s.gray5, s.f15]}>Created by</Text>
|
size={15}
|
||||||
<UserInfoText
|
/>
|
||||||
style={[s.blue3, s.f15]}
|
<Text style={[s.bold, s.f15, s.white]}>Invite Members</Text>
|
||||||
did={view.creator}
|
</LinearGradient>
|
||||||
prefix="@"
|
</TouchableOpacity>
|
||||||
asLink
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
{view.isScene && view.myState.member ? (
|
|
||||||
<View style={styles.relationshipsLine}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={['far', 'circle-check']}
|
|
||||||
style={[s.gray5, s.mr5]}
|
|
||||||
/>
|
|
||||||
<Text style={[s.mr2, s.gray5, s.f15]}>You are a member</Text>
|
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</View>
|
</View>
|
||||||
{view.isScene && view.creator === store.me.did ? (
|
)
|
||||||
<View style={styles.sceneAdminContainer}>
|
}),
|
||||||
<TouchableOpacity onPress={onPressInviteMembers}>
|
)
|
||||||
<LinearGradient
|
|
||||||
colors={[gradient[1], gradient[0]]}
|
|
||||||
start={{x: 0, y: 0}}
|
|
||||||
end={{x: 1, y: 1}}
|
|
||||||
style={[styles.btn, styles.gradientBtn, styles.sceneAdminBtn]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="user-plus"
|
|
||||||
style={[s.mr5, s.white]}
|
|
||||||
size={15}
|
|
||||||
/>
|
|
||||||
<Text style={[s.bold, s.f15, s.white]}>Invite Members</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
outer: {
|
outer: {
|
||||||
|
|||||||
@@ -5,76 +5,77 @@ import {MembersViewModel, MemberItem} from '../../../state/models/members-view'
|
|||||||
import {ProfileCard} from './ProfileCard'
|
import {ProfileCard} from './ProfileCard'
|
||||||
import {ErrorMessage} from '../util/ErrorMessage'
|
import {ErrorMessage} from '../util/ErrorMessage'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const ProfileMembers = observer(function ProfileMembers({
|
export const ProfileMembers = register(
|
||||||
name,
|
observer(function ProfileMembers({name}: {name: string}) {
|
||||||
}: {
|
const store = useStores()
|
||||||
name: string
|
const [view, setView] = useState<MembersViewModel | undefined>()
|
||||||
}) {
|
|
||||||
const store = useStores()
|
|
||||||
const [view, setView] = useState<MembersViewModel | undefined>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (view?.params.actor === name) {
|
if (view?.params.actor === name) {
|
||||||
console.log('Members doing nothing')
|
console.log('Members doing nothing')
|
||||||
return // no change needed? or trigger refresh?
|
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 = () => {
|
// loading
|
||||||
view?.refresh()
|
// =
|
||||||
}
|
if (
|
||||||
|
!view ||
|
||||||
|
(view.isLoading && !view.isRefreshing) ||
|
||||||
|
view.params.actor !== name
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<ActivityIndicator />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// loading
|
// error
|
||||||
// =
|
// =
|
||||||
if (
|
if (view.hasError) {
|
||||||
!view ||
|
return (
|
||||||
(view.isLoading && !view.isRefreshing) ||
|
<View>
|
||||||
view.params.actor !== name
|
<ErrorMessage
|
||||||
) {
|
dark
|
||||||
return (
|
message={view.error}
|
||||||
<View>
|
style={{margin: 6}}
|
||||||
<ActivityIndicator />
|
onPressTryAgain={onRefresh}
|
||||||
</View>
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loaded
|
||||||
|
// =
|
||||||
|
const renderItem = ({item}: {item: MemberItem}) => (
|
||||||
|
<ProfileCard
|
||||||
|
did={item.did}
|
||||||
|
handle={item.handle}
|
||||||
|
displayName={item.displayName}
|
||||||
|
avatar={item.avatar}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// error
|
|
||||||
// =
|
|
||||||
if (view.hasError) {
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ErrorMessage
|
<FlatList
|
||||||
dark
|
data={view.members}
|
||||||
message={view.error}
|
keyExtractor={item => item._reactKey}
|
||||||
style={{margin: 6}}
|
renderItem={renderItem}
|
||||||
onPressTryAgain={onRefresh}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}),
|
||||||
|
)
|
||||||
// loaded
|
|
||||||
// =
|
|
||||||
const renderItem = ({item}: {item: MemberItem}) => (
|
|
||||||
<ProfileCard
|
|
||||||
did={item.did}
|
|
||||||
handle={item.handle}
|
|
||||||
displayName={item.displayName}
|
|
||||||
avatar={item.avatar}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<FlatList
|
|
||||||
data={view.members}
|
|
||||||
keyExtractor={item => item._reactKey}
|
|
||||||
renderItem={renderItem}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {toShareUrl} from '../../../lib/strings'
|
|||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {ConfirmModel} from '../../../state/models/shell-ui'
|
import {ConfirmModel} from '../../../state/models/shell-ui'
|
||||||
import {TABS_ENABLED} from '../../../build-flags'
|
import {TABS_ENABLED} from '../../../build-flags'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
|
const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
|
||||||
|
|
||||||
@@ -26,52 +27,54 @@ export interface DropdownItem {
|
|||||||
onPress: () => void
|
onPress: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownBtn({
|
export const DropdownBtn = register(
|
||||||
style,
|
({
|
||||||
items,
|
style,
|
||||||
menuWidth,
|
items,
|
||||||
children,
|
menuWidth,
|
||||||
}: {
|
children,
|
||||||
style?: StyleProp<ViewStyle>
|
}: {
|
||||||
items: DropdownItem[]
|
style?: StyleProp<ViewStyle>
|
||||||
menuWidth?: number
|
items: DropdownItem[]
|
||||||
children?: React.ReactNode
|
menuWidth?: number
|
||||||
}) {
|
children?: React.ReactNode
|
||||||
const ref = useRef<TouchableOpacity>(null)
|
}) => {
|
||||||
|
const ref = useRef<TouchableOpacity>(null)
|
||||||
|
|
||||||
const onPress = () => {
|
const onPress = () => {
|
||||||
ref.current?.measure(
|
ref.current?.measure(
|
||||||
(
|
(
|
||||||
_x: number,
|
_x: number,
|
||||||
_y: number,
|
_y: number,
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
pageX: number,
|
pageX: number,
|
||||||
pageY: number,
|
pageY: number,
|
||||||
) => {
|
) => {
|
||||||
if (!menuWidth) {
|
if (!menuWidth) {
|
||||||
menuWidth = 200
|
menuWidth = 200
|
||||||
}
|
}
|
||||||
createDropdownMenu(
|
createDropdownMenu(
|
||||||
pageX + width - menuWidth,
|
pageX + width - menuWidth,
|
||||||
pageY + height,
|
pageY + height,
|
||||||
menuWidth,
|
menuWidth,
|
||||||
items,
|
items,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={style}
|
||||||
|
onPress={onPress}
|
||||||
|
hitSlop={HITSLOP}
|
||||||
|
ref={ref}>
|
||||||
|
{children}
|
||||||
|
</TouchableOpacity>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={style}
|
|
||||||
onPress={onPress}
|
|
||||||
hitSlop={HITSLOP}
|
|
||||||
ref={ref}>
|
|
||||||
{children}
|
|
||||||
</TouchableOpacity>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PostDropdownBtn({
|
export function PostDropdownBtn({
|
||||||
style,
|
style,
|
||||||
|
|||||||
@@ -5,28 +5,30 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|||||||
import {UserGroupIcon} from '../../lib/icons'
|
import {UserGroupIcon} from '../../lib/icons'
|
||||||
import {colors} from '../../lib/styles'
|
import {colors} from '../../lib/styles'
|
||||||
|
|
||||||
export function EmptyState({
|
export const EmptyState = register(
|
||||||
icon,
|
({
|
||||||
message,
|
icon,
|
||||||
style,
|
message,
|
||||||
}: {
|
style,
|
||||||
icon: IconProp | 'user-group'
|
}: {
|
||||||
message: string
|
icon: IconProp | 'user-group'
|
||||||
style?: StyleProp<ViewStyle>
|
message: string
|
||||||
}) {
|
style?: StyleProp<ViewStyle>
|
||||||
return (
|
}) => {
|
||||||
<View style={[styles.container, style]}>
|
return (
|
||||||
<View style={styles.iconContainer}>
|
<View style={[styles.container, style]}>
|
||||||
{icon === 'user-group' ? (
|
<View style={styles.iconContainer}>
|
||||||
<UserGroupIcon size="64" style={styles.icon} />
|
{icon === 'user-group' ? (
|
||||||
) : (
|
<UserGroupIcon size="64" style={styles.icon} />
|
||||||
<FontAwesomeIcon icon={icon} size={64} style={styles.icon} />
|
) : (
|
||||||
)}
|
<FontAwesomeIcon icon={icon} size={64} style={styles.icon} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text style={styles.text}>{message}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.text}>{message}</Text>
|
)
|
||||||
</View>
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
|||||||
@@ -10,58 +10,62 @@ import {
|
|||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
import {colors, gradients} from '../../lib/styles'
|
import {colors, gradients} from '../../lib/styles'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export function ErrorMessage({
|
export const ErrorMessage = register(
|
||||||
message,
|
({
|
||||||
numberOfLines,
|
message,
|
||||||
dark,
|
numberOfLines,
|
||||||
style,
|
dark,
|
||||||
onPressTryAgain,
|
style,
|
||||||
}: {
|
onPressTryAgain,
|
||||||
message: string
|
}: {
|
||||||
numberOfLines?: number
|
message: string
|
||||||
dark?: boolean
|
numberOfLines?: number
|
||||||
style?: StyleProp<ViewStyle>
|
dark?: boolean
|
||||||
onPressTryAgain?: () => void
|
style?: StyleProp<ViewStyle>
|
||||||
}) {
|
onPressTryAgain?: () => void
|
||||||
const inner = (
|
}) => {
|
||||||
<>
|
const inner = (
|
||||||
<View style={[styles.errorIcon, dark ? styles.darkErrorIcon : undefined]}>
|
<>
|
||||||
<FontAwesomeIcon
|
<View
|
||||||
icon="exclamation"
|
style={[styles.errorIcon, dark ? styles.darkErrorIcon : undefined]}>
|
||||||
style={{color: dark ? colors.red3 : colors.white}}
|
|
||||||
size={16}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<Text
|
|
||||||
style={[styles.message, dark ? styles.darkMessage : undefined]}
|
|
||||||
numberOfLines={numberOfLines}>
|
|
||||||
{message}
|
|
||||||
</Text>
|
|
||||||
{onPressTryAgain && (
|
|
||||||
<TouchableOpacity style={styles.btn} onPress={onPressTryAgain}>
|
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="arrows-rotate"
|
icon="exclamation"
|
||||||
style={{color: dark ? colors.white : colors.red4}}
|
style={{color: dark ? colors.red3 : colors.white}}
|
||||||
size={16}
|
size={16}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
)}
|
<Text
|
||||||
</>
|
style={[styles.message, dark ? styles.darkMessage : undefined]}
|
||||||
)
|
numberOfLines={numberOfLines}>
|
||||||
if (dark) {
|
{message}
|
||||||
return (
|
</Text>
|
||||||
<LinearGradient
|
{onPressTryAgain && (
|
||||||
colors={[gradients.error.start, gradients.error.end]}
|
<TouchableOpacity style={styles.btn} onPress={onPressTryAgain}>
|
||||||
start={{x: 0.5, y: 0}}
|
<FontAwesomeIcon
|
||||||
end={{x: 1, y: 1}}
|
icon="arrows-rotate"
|
||||||
style={[styles.outer, style]}>
|
style={{color: dark ? colors.white : colors.red4}}
|
||||||
{inner}
|
size={16}
|
||||||
</LinearGradient>
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
if (dark) {
|
||||||
return <View style={[styles.outer, style]}>{inner}</View>
|
return (
|
||||||
}
|
<LinearGradient
|
||||||
|
colors={[gradients.error.start, gradients.error.end]}
|
||||||
|
start={{x: 0.5, y: 0}}
|
||||||
|
end={{x: 1, y: 1}}
|
||||||
|
style={[styles.outer, style]}>
|
||||||
|
{inner}
|
||||||
|
</LinearGradient>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <View style={[styles.outer, style]}>{inner}</View>
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
outer: {
|
outer: {
|
||||||
|
|||||||
@@ -2,47 +2,50 @@ import React from 'react'
|
|||||||
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
|
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {colors} from '../../lib/styles'
|
import {colors} from '../../lib/styles'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export function ErrorScreen({
|
export const ErrorScreen = register(
|
||||||
title,
|
({
|
||||||
message,
|
title,
|
||||||
details,
|
message,
|
||||||
onPressTryAgain,
|
details,
|
||||||
}: {
|
onPressTryAgain,
|
||||||
title: string
|
}: {
|
||||||
message: string
|
title: string
|
||||||
details?: string
|
message: string
|
||||||
onPressTryAgain?: () => void
|
details?: string
|
||||||
}) {
|
onPressTryAgain?: () => void
|
||||||
return (
|
}) => {
|
||||||
<View style={styles.outer}>
|
return (
|
||||||
<View style={styles.errorIconContainer}>
|
<View style={styles.outer}>
|
||||||
<View style={styles.errorIcon}>
|
<View style={styles.errorIconContainer}>
|
||||||
<FontAwesomeIcon
|
<View style={styles.errorIcon}>
|
||||||
icon="exclamation"
|
|
||||||
style={{color: colors.white}}
|
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.title}>{title}</Text>
|
|
||||||
<Text style={styles.message}>{message}</Text>
|
|
||||||
{details && <Text style={styles.details}>{details}</Text>}
|
|
||||||
{onPressTryAgain && (
|
|
||||||
<View style={styles.btnContainer}>
|
|
||||||
<TouchableOpacity style={styles.btn} onPress={onPressTryAgain}>
|
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="arrows-rotate"
|
icon="exclamation"
|
||||||
style={{color: colors.white}}
|
style={{color: colors.white}}
|
||||||
size={16}
|
size={24}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.btnText}>Try again</Text>
|
</View>
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
<Text style={styles.title}>{title}</Text>
|
||||||
</View>
|
<Text style={styles.message}>{message}</Text>
|
||||||
)
|
{details && <Text style={styles.details}>{details}</Text>}
|
||||||
}
|
{onPressTryAgain && (
|
||||||
|
<View style={styles.btnContainer}>
|
||||||
|
<TouchableOpacity style={styles.btn} onPress={onPressTryAgain}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="arrows-rotate"
|
||||||
|
style={{color: colors.white}}
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
<Text style={styles.btnText}>Try again</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
outer: {
|
outer: {
|
||||||
|
|||||||
+31
-28
@@ -10,35 +10,38 @@ import {
|
|||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {useStores, RootStoreModel} from '../../../state'
|
import {useStores, RootStoreModel} from '../../../state'
|
||||||
import {convertBskyAppUrlIfNeeded} from '../../../lib/strings'
|
import {convertBskyAppUrlIfNeeded} from '../../../lib/strings'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const Link = observer(function Link({
|
export const Link = register(
|
||||||
style,
|
observer(function Link({
|
||||||
href,
|
style,
|
||||||
title,
|
href,
|
||||||
children,
|
title,
|
||||||
}: {
|
children,
|
||||||
style?: StyleProp<ViewStyle>
|
}: {
|
||||||
href: string
|
style?: StyleProp<ViewStyle>
|
||||||
title?: string
|
href: string
|
||||||
children?: React.ReactNode
|
title?: string
|
||||||
}) {
|
children?: React.ReactNode
|
||||||
const store = useStores()
|
}) {
|
||||||
const onPress = () => {
|
const store = useStores()
|
||||||
handleLink(store, href, false)
|
const onPress = () => {
|
||||||
}
|
handleLink(store, href, false)
|
||||||
const onLongPress = () => {
|
}
|
||||||
handleLink(store, href, true)
|
const onLongPress = () => {
|
||||||
}
|
handleLink(store, href, true)
|
||||||
return (
|
}
|
||||||
<TouchableOpacity
|
return (
|
||||||
style={style}
|
<TouchableOpacity
|
||||||
onPress={onPress}
|
style={style}
|
||||||
onLongPress={onLongPress}
|
onPress={onPress}
|
||||||
delayPressIn={50}>
|
onLongPress={onLongPress}
|
||||||
{children ? children : <Text>{title || 'link'}</Text>}
|
delayPressIn={50}>
|
||||||
</TouchableOpacity>
|
{children ? children : <Text>{title || 'link'}</Text>}
|
||||||
)
|
</TouchableOpacity>
|
||||||
})
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
export const TextLink = observer(function Link({
|
export const TextLink = observer(function Link({
|
||||||
style,
|
style,
|
||||||
|
|||||||
@@ -4,37 +4,39 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|||||||
import {UpIcon} from '../../lib/icons'
|
import {UpIcon} from '../../lib/icons'
|
||||||
import {s, colors} from '../../lib/styles'
|
import {s, colors} from '../../lib/styles'
|
||||||
|
|
||||||
export function LoadingPlaceholder({
|
export const LoadingPlaceholder = register(
|
||||||
width,
|
({
|
||||||
height,
|
width,
|
||||||
style,
|
height,
|
||||||
}: {
|
style,
|
||||||
width: string | number
|
}: {
|
||||||
height: string | number
|
width: string | number
|
||||||
style?: StyleProp<ViewStyle>
|
height: string | number
|
||||||
}) {
|
style?: StyleProp<ViewStyle>
|
||||||
return (
|
}) => {
|
||||||
<View
|
return (
|
||||||
style={[
|
|
||||||
{
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
backgroundColor: '#e7e9ea',
|
|
||||||
borderRadius: 6,
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
style,
|
|
||||||
]}>
|
|
||||||
<View
|
<View
|
||||||
style={{
|
style={[
|
||||||
width,
|
{
|
||||||
height,
|
width,
|
||||||
backgroundColor: '#e7e9ea',
|
height,
|
||||||
}}
|
backgroundColor: '#e7e9ea',
|
||||||
/>
|
borderRadius: 6,
|
||||||
</View>
|
overflow: 'hidden',
|
||||||
)
|
},
|
||||||
}
|
style,
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
backgroundColor: '#e7e9ea',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export function PostLoadingPlaceholder({
|
export function PostLoadingPlaceholder({
|
||||||
style,
|
style,
|
||||||
@@ -63,7 +65,7 @@ export function PostLoadingPlaceholder({
|
|||||||
<View style={s.flex1}>
|
<View style={s.flex1}>
|
||||||
<UpIcon style={s.gray3} size={17} strokeWidth={1.7} />
|
<UpIcon style={s.gray3} size={17} strokeWidth={1.7} />
|
||||||
</View>
|
</View>
|
||||||
<View style={s.flex1}></View>
|
<View style={s.flex1} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -33,45 +33,47 @@ interface PickerOpts {
|
|||||||
|
|
||||||
const MENU_WIDTH = 200
|
const MENU_WIDTH = 200
|
||||||
|
|
||||||
export function Picker({
|
export const Picker = register(
|
||||||
style,
|
({
|
||||||
labelStyle,
|
style,
|
||||||
iconStyle,
|
labelStyle,
|
||||||
items,
|
iconStyle,
|
||||||
value,
|
items,
|
||||||
onChange,
|
value,
|
||||||
enabled,
|
onChange,
|
||||||
}: PickerOpts) {
|
enabled,
|
||||||
const ref = useRef<View>(null)
|
}: PickerOpts) => {
|
||||||
const valueLabel = items.find(item => item.value === value)?.label || value
|
const ref = useRef<View>(null)
|
||||||
const onPress = () => {
|
const valueLabel = items.find(item => item.value === value)?.label || value
|
||||||
if (!enabled) {
|
const onPress = () => {
|
||||||
return
|
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(
|
return (
|
||||||
(
|
<TouchableWithoutFeedback onPress={onPress}>
|
||||||
_x: number,
|
<View style={[styles.outer, style]} ref={ref}>
|
||||||
_y: number,
|
<View style={styles.label}>
|
||||||
width: number,
|
<Text style={labelStyle}>{valueLabel}</Text>
|
||||||
height: number,
|
</View>
|
||||||
pageX: number,
|
<FontAwesomeIcon icon="angle-down" style={[styles.icon, iconStyle]} />
|
||||||
pageY: number,
|
|
||||||
) => {
|
|
||||||
createDropdownMenu(pageX, pageY + height, MENU_WIDTH, items, onChange)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<TouchableWithoutFeedback onPress={onPress}>
|
|
||||||
<View style={[styles.outer, style]} ref={ref}>
|
|
||||||
<View style={styles.label}>
|
|
||||||
<Text style={labelStyle}>{valueLabel}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<FontAwesomeIcon icon="angle-down" style={[styles.icon, iconStyle]} />
|
</TouchableWithoutFeedback>
|
||||||
</View>
|
)
|
||||||
</TouchableWithoutFeedback>
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
function createDropdownMenu(
|
function createDropdownMenu(
|
||||||
x: number,
|
x: number,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|||||||
import {UpIcon, UpIconSolid} from '../../lib/icons'
|
import {UpIcon, UpIconSolid} from '../../lib/icons'
|
||||||
import {s, colors} from '../../lib/styles'
|
import {s, colors} from '../../lib/styles'
|
||||||
import {useAnimatedValue} from '../../lib/useAnimatedValue'
|
import {useAnimatedValue} from '../../lib/useAnimatedValue'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
interface PostCtrlsOpts {
|
interface PostCtrlsOpts {
|
||||||
big?: boolean
|
big?: boolean
|
||||||
@@ -21,7 +22,7 @@ const redgray = '#7A6161'
|
|||||||
const sRedgray = {color: redgray}
|
const sRedgray = {color: redgray}
|
||||||
const HITSLOP = {top: 10, left: 10, bottom: 10, right: 10}
|
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 interp1 = useAnimatedValue(0)
|
||||||
const interp2 = useAnimatedValue(0)
|
const interp2 = useAnimatedValue(0)
|
||||||
|
|
||||||
@@ -168,7 +169,7 @@ export function PostCtrls(opts: PostCtrlsOpts) {
|
|||||||
<View style={s.flex1}></View>
|
<View style={s.flex1}></View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
ctrls: {
|
ctrls: {
|
||||||
|
|||||||
@@ -12,60 +12,58 @@ import {Link} from '../util/Link'
|
|||||||
import {LinkMeta, getLikelyType, LikelyType} from '../../../lib/link-meta'
|
import {LinkMeta, getLikelyType, LikelyType} from '../../../lib/link-meta'
|
||||||
import {colors} from '../../lib/styles'
|
import {colors} from '../../lib/styles'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export function PostEmbeds({
|
export const PostEmbeds = register(
|
||||||
entities,
|
({entities, style}: {entities?: Entity[]; style?: StyleProp<ViewStyle>}) => {
|
||||||
style,
|
const store = useStores()
|
||||||
}: {
|
const [linkMeta, setLinkMeta] = useState<LinkMeta | undefined>(undefined)
|
||||||
entities?: Entity[]
|
const link = entities?.find(
|
||||||
style?: StyleProp<ViewStyle>
|
ent =>
|
||||||
}) {
|
ent.type === 'link' &&
|
||||||
const store = useStores()
|
getLikelyType(ent.value || '') === LikelyType.HTML,
|
||||||
const [linkMeta, setLinkMeta] = useState<LinkMeta | undefined>(undefined)
|
)
|
||||||
const link = entities?.find(
|
|
||||||
ent =>
|
|
||||||
ent.type === 'link' && getLikelyType(ent.value || '') === LikelyType.HTML,
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let aborted = false
|
let aborted = false
|
||||||
store.linkMetas.getLinkMeta(link?.value || '').then(linkMeta => {
|
store.linkMetas.getLinkMeta(link?.value || '').then(linkMeta => {
|
||||||
if (!aborted) {
|
if (!aborted) {
|
||||||
setLinkMeta(linkMeta)
|
setLinkMeta(linkMeta)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
aborted = true
|
||||||
}
|
}
|
||||||
})
|
}, [link])
|
||||||
|
|
||||||
return () => {
|
if (!link) {
|
||||||
aborted = true
|
return <View />
|
||||||
}
|
}
|
||||||
}, [link])
|
|
||||||
|
|
||||||
if (!link) {
|
return (
|
||||||
return <View />
|
<Link style={[styles.outer, style]} href={link.value}>
|
||||||
}
|
{linkMeta ? (
|
||||||
|
<>
|
||||||
return (
|
<Text numberOfLines={1} style={styles.title}>
|
||||||
<Link style={[styles.outer, style]} href={link.value}>
|
{linkMeta.title || linkMeta.url}
|
||||||
{linkMeta ? (
|
|
||||||
<>
|
|
||||||
<Text numberOfLines={1} style={styles.title}>
|
|
||||||
{linkMeta.title || linkMeta.url}
|
|
||||||
</Text>
|
|
||||||
<Text numberOfLines={1} style={styles.url}>
|
|
||||||
{linkMeta.url}
|
|
||||||
</Text>
|
|
||||||
{linkMeta.description ? (
|
|
||||||
<Text numberOfLines={2} style={styles.description}>
|
|
||||||
{linkMeta.description}
|
|
||||||
</Text>
|
</Text>
|
||||||
) : undefined}
|
<Text numberOfLines={1} style={styles.url}>
|
||||||
</>
|
{linkMeta.url}
|
||||||
) : (
|
</Text>
|
||||||
<ActivityIndicator />
|
{linkMeta.description ? (
|
||||||
)}
|
<Text numberOfLines={2} style={styles.description}>
|
||||||
</Link>
|
{linkMeta.description}
|
||||||
)
|
</Text>
|
||||||
}
|
) : undefined}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<ActivityIndicator />
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
outer: {
|
outer: {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {Link} from '../util/Link'
|
|||||||
import {PostDropdownBtn} from '../util/DropdownBtn'
|
import {PostDropdownBtn} from '../util/DropdownBtn'
|
||||||
import {s} from '../../lib/styles'
|
import {s} from '../../lib/styles'
|
||||||
import {ago} from '../../../lib/strings'
|
import {ago} from '../../../lib/strings'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
interface PostMetaOpts {
|
interface PostMetaOpts {
|
||||||
itemHref: string
|
itemHref: string
|
||||||
@@ -18,7 +19,7 @@ interface PostMetaOpts {
|
|||||||
onDeletePost: () => void
|
onDeletePost: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PostMeta(opts: PostMetaOpts) {
|
export const PostMeta = register((opts: PostMetaOpts) => {
|
||||||
return (
|
return (
|
||||||
<View style={styles.meta}>
|
<View style={styles.meta}>
|
||||||
<Link
|
<Link
|
||||||
@@ -47,7 +48,7 @@ export function PostMeta(opts: PostMetaOpts) {
|
|||||||
</PostDropdownBtn>
|
</PostDropdownBtn>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
meta: {
|
meta: {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {Text, TextStyle, StyleProp} from 'react-native'
|
|||||||
import {TextLink} from './Link'
|
import {TextLink} from './Link'
|
||||||
import {s} from '../../lib/styles'
|
import {s} from '../../lib/styles'
|
||||||
import {toShortUrl} from '../../../lib/strings'
|
import {toShortUrl} from '../../../lib/strings'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
type TextSlice = {start: number; end: number}
|
type TextSlice = {start: number; end: number}
|
||||||
type Entity = {
|
type Entity = {
|
||||||
@@ -11,65 +12,70 @@ type Entity = {
|
|||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RichText({
|
export const RichText = register(
|
||||||
text,
|
({
|
||||||
entities,
|
text,
|
||||||
style,
|
entities,
|
||||||
numberOfLines,
|
style,
|
||||||
}: {
|
numberOfLines,
|
||||||
text: string
|
}: {
|
||||||
entities?: Entity[]
|
text: string
|
||||||
style?: StyleProp<TextStyle>
|
entities?: Entity[]
|
||||||
numberOfLines?: number
|
style?: StyleProp<TextStyle>
|
||||||
}) {
|
numberOfLines?: number
|
||||||
if (!entities?.length) {
|
}) => {
|
||||||
if (/^\p{Extended_Pictographic}+$/u.test(text) && text.length <= 5) {
|
if (!entities?.length) {
|
||||||
style = {
|
if (/^\p{Extended_Pictographic}+$/u.test(text) && text.length <= 5) {
|
||||||
fontSize: 26,
|
style = {
|
||||||
lineHeight: 30,
|
fontSize: 26,
|
||||||
|
lineHeight: 30,
|
||||||
|
}
|
||||||
|
return <Text style={style}>{text}</Text>
|
||||||
}
|
}
|
||||||
return <Text style={style}>{text}</Text>
|
return <Text style={style}>{text}</Text>
|
||||||
}
|
}
|
||||||
return <Text style={style}>{text}</Text>
|
if (!style) {
|
||||||
}
|
style = []
|
||||||
if (!style) style = []
|
} else if (!Array.isArray(style)) {
|
||||||
else if (!Array.isArray(style)) style = [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(
|
|
||||||
<TextLink
|
|
||||||
key={key}
|
|
||||||
text={segment.text}
|
|
||||||
href={`/profile/${segment.entity.value}`}
|
|
||||||
style={[style, s.blue3]}
|
|
||||||
/>,
|
|
||||||
)
|
|
||||||
} else if (segment.entity.type === 'link') {
|
|
||||||
els.push(
|
|
||||||
<TextLink
|
|
||||||
key={key}
|
|
||||||
text={toShortUrl(segment.text)}
|
|
||||||
href={segment.entity.value}
|
|
||||||
style={[style, s.blue3]}
|
|
||||||
/>,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
key++
|
entities.sort(sortByIndex)
|
||||||
}
|
const segments = Array.from(toSegments(text, entities))
|
||||||
return (
|
const els = []
|
||||||
<Text style={style} numberOfLines={numberOfLines}>
|
let key = 0
|
||||||
{els}
|
for (const segment of segments) {
|
||||||
</Text>
|
if (typeof segment === 'string') {
|
||||||
)
|
els.push(segment)
|
||||||
}
|
} else {
|
||||||
|
if (segment.entity.type === 'mention') {
|
||||||
|
els.push(
|
||||||
|
<TextLink
|
||||||
|
key={key}
|
||||||
|
text={segment.text}
|
||||||
|
href={`/profile/${segment.entity.value}`}
|
||||||
|
style={[style, s.blue3]}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
} else if (segment.entity.type === 'link') {
|
||||||
|
els.push(
|
||||||
|
<TextLink
|
||||||
|
key={key}
|
||||||
|
text={toShortUrl(segment.text)}
|
||||||
|
href={segment.entity.value}
|
||||||
|
style={[style, s.blue3]}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key++
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Text style={style} numberOfLines={numberOfLines}>
|
||||||
|
{els}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
function sortByIndex(a: Entity, b: Entity) {
|
function sortByIndex(a: Entity, b: Entity) {
|
||||||
return a.index.start - b.index.start
|
return a.index.start - b.index.start
|
||||||
|
|||||||
@@ -7,100 +7,104 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {colors} from '../../lib/styles'
|
import {colors} from '../../lib/styles'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
interface Layout {
|
interface Layout {
|
||||||
x: number
|
x: number
|
||||||
width: number
|
width: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Selector({
|
export const Selector = register(
|
||||||
selectedIndex,
|
({
|
||||||
items,
|
selectedIndex,
|
||||||
panX,
|
items,
|
||||||
onSelect,
|
panX,
|
||||||
}: {
|
onSelect,
|
||||||
selectedIndex: number
|
}: {
|
||||||
items: string[]
|
selectedIndex: number
|
||||||
panX: Animated.Value
|
items: string[]
|
||||||
onSelect?: (index: number) => void
|
panX: Animated.Value
|
||||||
}) {
|
onSelect?: (index: number) => void
|
||||||
const [itemLayouts, setItemLayouts] = useState<undefined | Layout[]>(
|
}) => {
|
||||||
undefined,
|
const [itemLayouts, setItemLayouts] = useState<undefined | Layout[]>(
|
||||||
)
|
undefined,
|
||||||
const itemRefs = useMemo(
|
)
|
||||||
() => Array.from({length: items.length}).map(() => createRef<View>()),
|
const itemRefs = useMemo(
|
||||||
[items.length],
|
() => Array.from({length: items.length}).map(() => createRef<View>()),
|
||||||
)
|
[items.length],
|
||||||
|
)
|
||||||
|
|
||||||
const currentLayouts = useMemo(() => {
|
const currentLayouts = useMemo(() => {
|
||||||
const left = itemLayouts?.[selectedIndex - 1] || {x: 0, width: 0}
|
const left = itemLayouts?.[selectedIndex - 1] || {x: 0, width: 0}
|
||||||
const middle = itemLayouts?.[selectedIndex] || {x: 0, width: 0}
|
const middle = itemLayouts?.[selectedIndex] || {x: 0, width: 0}
|
||||||
const right = itemLayouts?.[selectedIndex + 1] || {
|
const right = itemLayouts?.[selectedIndex + 1] || {
|
||||||
x: middle.x + 20,
|
x: middle.x + 20,
|
||||||
width: middle.width,
|
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 = {
|
const onLayout = () => {
|
||||||
left: panX.interpolate({
|
const promises = []
|
||||||
inputRange: [-1, 0, 1],
|
for (let i = 0; i < items.length; i++) {
|
||||||
outputRange: [
|
promises.push(
|
||||||
currentLayouts[0].x,
|
new Promise<Layout>(resolve => {
|
||||||
currentLayouts[1].x,
|
itemRefs[i].current?.measure(
|
||||||
currentLayouts[2].x,
|
(x: number, _y: number, width: number) => {
|
||||||
],
|
resolve({x, width})
|
||||||
}),
|
},
|
||||||
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<Layout>(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 (
|
|
||||||
<View style={[styles.outer]} onLayout={onLayout}>
|
|
||||||
<Animated.View style={[styles.underline, underlineStyle]} />
|
|
||||||
{items.map((item, i) => {
|
|
||||||
const selected = i === selectedIndex
|
|
||||||
return (
|
|
||||||
<TouchableWithoutFeedback key={i} onPress={() => onPressItem(i)}>
|
|
||||||
<View style={styles.item} ref={itemRefs[i]}>
|
|
||||||
<Text style={selected ? styles.labelSelected : styles.itemLabel}>
|
|
||||||
{item}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</TouchableWithoutFeedback>
|
|
||||||
)
|
)
|
||||||
})}
|
}
|
||||||
</View>
|
Promise.all(promises).then((layouts: Layout[]) => {
|
||||||
)
|
setItemLayouts(layouts)
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPressItem = (index: number) => {
|
||||||
|
onSelect?.(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.outer]} onLayout={onLayout}>
|
||||||
|
<Animated.View style={[styles.underline, underlineStyle]} />
|
||||||
|
{items.map((item, i) => {
|
||||||
|
const selected = i === selectedIndex
|
||||||
|
return (
|
||||||
|
<TouchableWithoutFeedback key={i} onPress={() => onPressItem(i)}>
|
||||||
|
<View style={styles.item} ref={itemRefs[i]}>
|
||||||
|
<Text
|
||||||
|
style={selected ? styles.labelSelected : styles.itemLabel}>
|
||||||
|
{item}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
outer: {
|
outer: {
|
||||||
|
|||||||
@@ -10,110 +10,113 @@ import {
|
|||||||
} from 'react-native-image-crop-picker'
|
} from 'react-native-image-crop-picker'
|
||||||
import {getGradient} from '../../lib/asset-gen'
|
import {getGradient} from '../../lib/asset-gen'
|
||||||
import {colors} from '../../lib/styles'
|
import {colors} from '../../lib/styles'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export function UserAvatar({
|
export const UserAvatar = register(
|
||||||
size,
|
({
|
||||||
handle,
|
size,
|
||||||
avatar,
|
handle,
|
||||||
displayName,
|
avatar,
|
||||||
onSelectNewAvatar,
|
displayName,
|
||||||
}: {
|
onSelectNewAvatar,
|
||||||
size: number
|
}: {
|
||||||
handle: string
|
size: number
|
||||||
displayName: string | undefined
|
handle: string
|
||||||
avatar?: string | null
|
displayName: string | undefined
|
||||||
onSelectNewAvatar?: (img: PickedImage) => void
|
avatar?: string | null
|
||||||
}) {
|
onSelectNewAvatar?: (img: PickedImage) => void
|
||||||
const initials = getInitials(displayName || handle)
|
}) => {
|
||||||
const gradient = getGradient(handle)
|
const initials = getInitials(displayName || handle)
|
||||||
|
const gradient = getGradient(handle)
|
||||||
|
|
||||||
const handleEditAvatar = useCallback(() => {
|
const handleEditAvatar = useCallback(() => {
|
||||||
Alert.alert('Select upload method', '', [
|
Alert.alert('Select upload method', '', [
|
||||||
{
|
{
|
||||||
text: 'Take a new photo',
|
text: 'Take a new photo',
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
openCamera({
|
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({
|
|
||||||
mediaType: 'photo',
|
mediaType: 'photo',
|
||||||
path: item.path,
|
cropping: true,
|
||||||
width: 400,
|
width: 400,
|
||||||
height: 400,
|
height: 400,
|
||||||
cropperCircleOverlay: true,
|
cropperCircleOverlay: true,
|
||||||
forceJpg: true, // ios only
|
forceJpg: true, // ios only
|
||||||
compressImageQuality: 0.7,
|
compressImageQuality: 0.7,
|
||||||
}).then(onSelectNewAvatar)
|
}).then(onSelectNewAvatar)
|
||||||
})
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
])
|
text: 'Select from gallery',
|
||||||
}, [onSelectNewAvatar])
|
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) => (
|
const renderSvg = (size: number, initials: string) => (
|
||||||
<Svg width={size} height={size} viewBox="0 0 100 100">
|
<Svg width={size} height={size} viewBox="0 0 100 100">
|
||||||
<Defs>
|
<Defs>
|
||||||
<LinearGradient id="grad" x1="0" y1="0" x2="1" y2="1">
|
<LinearGradient id="grad" x1="0" y1="0" x2="1" y2="1">
|
||||||
<Stop offset="0" stopColor={gradient[0]} stopOpacity="1" />
|
<Stop offset="0" stopColor={gradient[0]} stopOpacity="1" />
|
||||||
<Stop offset="1" stopColor={gradient[1]} stopOpacity="1" />
|
<Stop offset="1" stopColor={gradient[1]} stopOpacity="1" />
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</Defs>
|
</Defs>
|
||||||
<Circle cx="50" cy="50" r="50" fill="url(#grad)" />
|
<Circle cx="50" cy="50" r="50" fill="url(#grad)" />
|
||||||
<Text
|
<Text
|
||||||
fill="white"
|
fill="white"
|
||||||
fontSize="50"
|
fontSize="50"
|
||||||
fontWeight="bold"
|
fontWeight="bold"
|
||||||
x="50"
|
x="50"
|
||||||
y="67"
|
y="67"
|
||||||
textAnchor="middle">
|
textAnchor="middle">
|
||||||
{initials}
|
{initials}
|
||||||
</Text>
|
</Text>
|
||||||
</Svg>
|
</Svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
// onSelectNewAvatar is only passed as prop on the EditProfile component
|
// onSelectNewAvatar is only passed as prop on the EditProfile component
|
||||||
return onSelectNewAvatar ? (
|
return onSelectNewAvatar ? (
|
||||||
<TouchableOpacity onPress={handleEditAvatar}>
|
<TouchableOpacity onPress={handleEditAvatar}>
|
||||||
{avatar ? (
|
{avatar ? (
|
||||||
<Image
|
<Image
|
||||||
style={{width: size, height: size, borderRadius: (size / 2) | 0}}
|
style={{width: size, height: size, borderRadius: (size / 2) | 0}}
|
||||||
source={{uri: avatar}}
|
source={{uri: avatar}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
renderSvg(size, initials)
|
renderSvg(size, initials)
|
||||||
)}
|
)}
|
||||||
<View style={styles.editButtonContainer}>
|
<View style={styles.editButtonContainer}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="camera"
|
icon="camera"
|
||||||
size={12}
|
size={12}
|
||||||
style={{color: colors.white}}
|
style={{color: colors.white}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : avatar ? (
|
) : avatar ? (
|
||||||
<Image
|
<Image
|
||||||
style={{width: size, height: size, borderRadius: (size / 2) | 0}}
|
style={{width: size, height: size, borderRadius: (size / 2) | 0}}
|
||||||
resizeMode="stretch"
|
resizeMode="stretch"
|
||||||
source={{uri: avatar}}
|
source={{uri: avatar}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
renderSvg(size, initials)
|
renderSvg(size, initials)
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
|
||||||
function getInitials(str: string): string {
|
function getInitials(str: string): string {
|
||||||
const tokens = str
|
const tokens = str
|
||||||
|
|||||||
@@ -10,100 +10,103 @@ import {
|
|||||||
openPicker,
|
openPicker,
|
||||||
} from 'react-native-image-crop-picker'
|
} from 'react-native-image-crop-picker'
|
||||||
import {IMAGES_ENABLED} from '../../../build-flags'
|
import {IMAGES_ENABLED} from '../../../build-flags'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export function UserBanner({
|
export const UserBanner = register(
|
||||||
handle,
|
({
|
||||||
userBanner,
|
handle,
|
||||||
setUserBanner,
|
userBanner,
|
||||||
}: {
|
setUserBanner,
|
||||||
handle: string
|
}: {
|
||||||
userBanner?: string | null
|
handle: string
|
||||||
setUserBanner?: React.Dispatch<React.SetStateAction<string | null>>
|
userBanner?: string | null
|
||||||
}) {
|
setUserBanner?: React.Dispatch<React.SetStateAction<string | null>>
|
||||||
const gradient = getGradient(handle)
|
}) => {
|
||||||
|
const gradient = getGradient(handle)
|
||||||
|
|
||||||
const handleEditBanner = useCallback(() => {
|
const handleEditBanner = useCallback(() => {
|
||||||
Alert.alert('Select upload method', '', [
|
Alert.alert('Select upload method', '', [
|
||||||
{
|
{
|
||||||
text: 'Take a new photo',
|
text: 'Take a new photo',
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
openCamera({
|
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({
|
|
||||||
mediaType: 'photo',
|
mediaType: 'photo',
|
||||||
path: item.path,
|
cropping: true,
|
||||||
width: 1500,
|
width: 1500,
|
||||||
height: 500,
|
height: 500,
|
||||||
}).then(croppedItem => {
|
}).then(item => {
|
||||||
if (setUserBanner != null) {
|
if (setUserBanner != null) {
|
||||||
setUserBanner(croppedItem.path)
|
setUserBanner(item.path)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
])
|
text: 'Select from gallery',
|
||||||
}, [setUserBanner])
|
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 = () => (
|
||||||
<Svg width="100%" height="120" viewBox="50 0 200 100">
|
<Svg width="100%" height="120" viewBox="50 0 200 100">
|
||||||
<Defs>
|
<Defs>
|
||||||
<LinearGradient id="grad" x1="0" y1="0" x2="1" y2="1">
|
<LinearGradient id="grad" x1="0" y1="0" x2="1" y2="1">
|
||||||
<Stop offset="0" stopColor={gradient[0]} stopOpacity="1" />
|
<Stop offset="0" stopColor={gradient[0]} stopOpacity="1" />
|
||||||
<Stop offset="1" stopColor={gradient[1]} stopOpacity="1" />
|
<Stop offset="1" stopColor={gradient[1]} stopOpacity="1" />
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
<LinearGradient id="grad2" x1="0" y1="0" x2="0" y2="1">
|
<LinearGradient id="grad2" x1="0" y1="0" x2="0" y2="1">
|
||||||
<Stop offset="0" stopColor="#fff" stopOpacity="0" />
|
<Stop offset="0" stopColor="#fff" stopOpacity="0" />
|
||||||
<Stop offset="1" stopColor="#fff" stopOpacity="0.3" />
|
<Stop offset="1" stopColor="#fff" stopOpacity="0.3" />
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</Defs>
|
</Defs>
|
||||||
<Rect x="0" y="0" width="400" height="100" fill="url(#grad)" />
|
<Rect x="0" y="0" width="400" height="100" fill="url(#grad)" />
|
||||||
<Rect x="0" y="0" width="400" height="100" fill="url(#grad2)" />
|
<Rect x="0" y="0" width="400" height="100" fill="url(#grad2)" />
|
||||||
</Svg>
|
</Svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
// setUserBanner is only passed as prop on the EditProfile component
|
// setUserBanner is only passed as prop on the EditProfile component
|
||||||
return setUserBanner != null && IMAGES_ENABLED ? (
|
return setUserBanner != null && IMAGES_ENABLED ? (
|
||||||
<TouchableOpacity onPress={handleEditBanner}>
|
<TouchableOpacity onPress={handleEditBanner}>
|
||||||
{userBanner ? (
|
{userBanner ? (
|
||||||
<Image style={styles.bannerImage} source={{uri: userBanner}} />
|
<Image style={styles.bannerImage} source={{uri: userBanner}} />
|
||||||
) : (
|
) : (
|
||||||
renderSvg()
|
renderSvg()
|
||||||
)}
|
)}
|
||||||
<View style={styles.editButtonContainer}>
|
<View style={styles.editButtonContainer}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="camera"
|
icon="camera"
|
||||||
size={12}
|
size={12}
|
||||||
style={{color: colors.white}}
|
style={{color: colors.white}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : userBanner ? (
|
) : userBanner ? (
|
||||||
<Image
|
<Image
|
||||||
style={styles.bannerImage}
|
style={styles.bannerImage}
|
||||||
resizeMode="stretch"
|
resizeMode="stretch"
|
||||||
source={{uri: userBanner}}
|
source={{uri: userBanner}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
renderSvg()
|
renderSvg()
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
editButtonContainer: {
|
editButtonContainer: {
|
||||||
|
|||||||
@@ -4,76 +4,83 @@ import {StyleProp, Text, TextStyle} from 'react-native'
|
|||||||
import {Link} from './Link'
|
import {Link} from './Link'
|
||||||
import {LoadingPlaceholder} from './LoadingPlaceholder'
|
import {LoadingPlaceholder} from './LoadingPlaceholder'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export function UserInfoText({
|
export const UserInfoText = register(
|
||||||
did,
|
({
|
||||||
attr,
|
did,
|
||||||
loading,
|
attr,
|
||||||
failed,
|
loading,
|
||||||
prefix,
|
failed,
|
||||||
style,
|
prefix,
|
||||||
asLink,
|
style,
|
||||||
}: {
|
asLink,
|
||||||
did: string
|
}: {
|
||||||
attr?: keyof GetProfile.OutputSchema
|
did: string
|
||||||
loading?: string
|
attr?: keyof GetProfile.OutputSchema
|
||||||
failed?: string
|
loading?: string
|
||||||
prefix?: string
|
failed?: string
|
||||||
style?: StyleProp<TextStyle>
|
prefix?: string
|
||||||
asLink?: boolean
|
style?: StyleProp<TextStyle>
|
||||||
}) {
|
asLink?: boolean
|
||||||
attr = attr || 'handle'
|
}) => {
|
||||||
loading = loading || '...'
|
attr = attr || 'handle'
|
||||||
failed = failed || 'user'
|
loading = loading || '...'
|
||||||
|
failed = failed || 'user'
|
||||||
|
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const [profile, setProfile] = useState<undefined | GetProfile.OutputSchema>(
|
const [profile, setProfile] = useState<undefined | GetProfile.OutputSchema>(
|
||||||
undefined,
|
undefined,
|
||||||
)
|
|
||||||
const [didFail, setFailed] = useState<boolean>(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let aborted = false
|
|
||||||
store.profiles.getProfile(did).then(
|
|
||||||
v => {
|
|
||||||
if (aborted) return
|
|
||||||
setProfile(v.data)
|
|
||||||
},
|
|
||||||
_err => {
|
|
||||||
if (aborted) return
|
|
||||||
setFailed(true)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
return () => {
|
const [didFail, setFailed] = useState<boolean>(false)
|
||||||
aborted = true
|
|
||||||
|
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 = <Text style={style}>{failed}</Text>
|
||||||
|
} else if (profile) {
|
||||||
|
inner = <Text style={style}>{`${prefix || ''}${profile[attr]}`}</Text>
|
||||||
|
} else {
|
||||||
|
inner = (
|
||||||
|
<LoadingPlaceholder
|
||||||
|
width={80}
|
||||||
|
height={8}
|
||||||
|
style={{position: 'relative', top: 1, left: 2}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}, [did, store.api.app.bsky])
|
|
||||||
|
|
||||||
let inner
|
if (asLink) {
|
||||||
if (didFail) {
|
const title = profile?.displayName || profile?.handle || 'User'
|
||||||
inner = <Text style={style}>{failed}</Text>
|
return (
|
||||||
} else if (profile) {
|
<Link
|
||||||
inner = <Text style={style}>{`${prefix || ''}${profile[attr]}`}</Text>
|
href={`/profile/${profile?.handle ? profile.handle : did}`}
|
||||||
} else {
|
title={title}>
|
||||||
inner = (
|
{inner}
|
||||||
<LoadingPlaceholder
|
</Link>
|
||||||
width={80}
|
)
|
||||||
height={8}
|
}
|
||||||
style={{position: 'relative', top: 1, left: 2}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (asLink) {
|
return inner
|
||||||
const title = profile?.displayName || profile?.handle || 'User'
|
},
|
||||||
return (
|
)
|
||||||
<Link
|
|
||||||
href={`/profile/${profile?.handle ? profile.handle : did}`}
|
|
||||||
title={title}>
|
|
||||||
{inner}
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return inner
|
|
||||||
}
|
|
||||||
|
|||||||
+109
-105
@@ -1,5 +1,4 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -11,116 +10,121 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|||||||
import {s, colors} from '../../lib/styles'
|
import {s, colors} from '../../lib/styles'
|
||||||
import {MagnifyingGlassIcon} from '../../lib/icons'
|
import {MagnifyingGlassIcon} from '../../lib/icons'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
|
const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
|
||||||
const BACK_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
|
const BACK_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
|
||||||
|
|
||||||
export const ViewHeader = observer(function ViewHeader({
|
export const ViewHeader = register(
|
||||||
title,
|
observer(
|
||||||
subtitle,
|
({
|
||||||
onPost,
|
title,
|
||||||
}: {
|
subtitle,
|
||||||
title: string
|
onPost,
|
||||||
subtitle?: string
|
}: {
|
||||||
onPost?: () => void
|
title: string
|
||||||
}) {
|
subtitle?: string
|
||||||
const store = useStores()
|
onPost?: () => void
|
||||||
const onPressBack = () => {
|
}) => {
|
||||||
store.nav.tab.goBack()
|
const store = useStores()
|
||||||
}
|
const onPressBack = () => {
|
||||||
const onPressMenu = () => {
|
store.nav.tab.goBack()
|
||||||
store.shell.setMainMenuOpen(true)
|
}
|
||||||
}
|
const onPressMenu = () => {
|
||||||
const onPressCompose = () => {
|
store.shell.setMainMenuOpen(true)
|
||||||
store.shell.openComposer({onPost})
|
}
|
||||||
}
|
const onPressCompose = () => {
|
||||||
const onPressSearch = () => {
|
store.shell.openComposer({onPost})
|
||||||
store.nav.navigate(`/search`)
|
}
|
||||||
}
|
const onPressSearch = () => {
|
||||||
const onPressReconnect = () => {
|
store.nav.navigate('/search')
|
||||||
store.session.connect().catch(e => {
|
}
|
||||||
// log for debugging but ignore otherwise
|
const onPressReconnect = () => {
|
||||||
console.log(e)
|
store.session.connect().catch(e => {
|
||||||
})
|
// log for debugging but ignore otherwise
|
||||||
}
|
console.log(e)
|
||||||
const canGoBack = store.nav.tab.canGoBack
|
})
|
||||||
return (
|
}
|
||||||
<>
|
const canGoBack = store.nav.tab.canGoBack
|
||||||
<View style={styles.header}>
|
return (
|
||||||
<TouchableOpacity
|
<>
|
||||||
onPress={canGoBack ? onPressBack : onPressMenu}
|
<View style={styles.header}>
|
||||||
hitSlop={BACK_HITSLOP}
|
<TouchableOpacity
|
||||||
style={styles.backIcon}>
|
onPress={canGoBack ? onPressBack : onPressMenu}
|
||||||
<FontAwesomeIcon
|
hitSlop={BACK_HITSLOP}
|
||||||
size={18}
|
style={styles.backIcon}>
|
||||||
icon={canGoBack ? 'angle-left' : 'bars'}
|
|
||||||
style={{marginTop: 6}}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<View style={styles.titleContainer} pointerEvents="none">
|
|
||||||
<Text style={styles.title}>{title}</Text>
|
|
||||||
{subtitle ? (
|
|
||||||
<Text style={styles.subtitle} numberOfLines={1}>
|
|
||||||
{subtitle}
|
|
||||||
</Text>
|
|
||||||
) : undefined}
|
|
||||||
</View>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={onPressCompose}
|
|
||||||
hitSlop={HITSLOP}
|
|
||||||
style={styles.btn}>
|
|
||||||
<FontAwesomeIcon size={18} icon="plus" />
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={onPressSearch}
|
|
||||||
hitSlop={HITSLOP}
|
|
||||||
style={[styles.btn, {marginLeft: 8}]}>
|
|
||||||
<MagnifyingGlassIcon
|
|
||||||
size={18}
|
|
||||||
strokeWidth={3}
|
|
||||||
style={styles.searchBtnIcon}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
{!store.session.online ? (
|
|
||||||
<TouchableOpacity style={styles.offline} onPress={onPressReconnect}>
|
|
||||||
{store.session.attemptingConnect ? (
|
|
||||||
<>
|
|
||||||
<ActivityIndicator />
|
|
||||||
<Text style={[s.gray1, s.bold, s.flex1, s.pl5, s.pt5, s.pb5]}>
|
|
||||||
Connecting...
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<FontAwesomeIcon icon="signal" style={[s.gray2]} size={18} />
|
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="x"
|
size={18}
|
||||||
style={[
|
icon={canGoBack ? 'angle-left' : 'bars'}
|
||||||
s.red4,
|
style={{marginTop: 6}}
|
||||||
{
|
|
||||||
backgroundColor: colors.gray6,
|
|
||||||
position: 'relative',
|
|
||||||
left: -4,
|
|
||||||
top: 6,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
border
|
|
||||||
size={12}
|
|
||||||
/>
|
/>
|
||||||
<Text style={[s.gray1, s.bold, s.flex1, s.pl2]}>
|
</TouchableOpacity>
|
||||||
Unable to connect
|
<View style={styles.titleContainer} pointerEvents="none">
|
||||||
</Text>
|
<Text style={styles.title}>{title}</Text>
|
||||||
<View style={styles.offlineBtn}>
|
{subtitle ? (
|
||||||
<Text style={styles.offlineBtnText}>Try again</Text>
|
<Text style={styles.subtitle} numberOfLines={1}>
|
||||||
</View>
|
{subtitle}
|
||||||
</>
|
</Text>
|
||||||
)}
|
) : undefined}
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
) : undefined}
|
<TouchableOpacity
|
||||||
</>
|
onPress={onPressCompose}
|
||||||
)
|
hitSlop={HITSLOP}
|
||||||
})
|
style={styles.btn}>
|
||||||
|
<FontAwesomeIcon size={18} icon="plus" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onPressSearch}
|
||||||
|
hitSlop={HITSLOP}
|
||||||
|
style={[styles.btn, {marginLeft: 8}]}>
|
||||||
|
<MagnifyingGlassIcon
|
||||||
|
size={18}
|
||||||
|
strokeWidth={3}
|
||||||
|
style={styles.searchBtnIcon}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
{!store.session.online ? (
|
||||||
|
<TouchableOpacity style={styles.offline} onPress={onPressReconnect}>
|
||||||
|
{store.session.attemptingConnect ? (
|
||||||
|
<>
|
||||||
|
<ActivityIndicator />
|
||||||
|
<Text style={[s.gray1, s.bold, s.flex1, s.pl5, s.pt5, s.pb5]}>
|
||||||
|
Connecting...
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FontAwesomeIcon icon="signal" style={[s.gray2]} size={18} />
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="x"
|
||||||
|
style={[
|
||||||
|
s.red4,
|
||||||
|
{
|
||||||
|
backgroundColor: colors.gray6,
|
||||||
|
position: 'relative',
|
||||||
|
left: -4,
|
||||||
|
top: 6,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
border
|
||||||
|
size={12}
|
||||||
|
/>
|
||||||
|
<Text style={[s.gray1, s.bold, s.flex1, s.pl2]}>
|
||||||
|
Unable to connect
|
||||||
|
</Text>
|
||||||
|
<View style={styles.offlineBtn}>
|
||||||
|
<Text style={styles.offlineBtnText}>Try again</Text>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : undefined}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
header: {
|
header: {
|
||||||
|
|||||||
@@ -4,92 +4,95 @@ import {Selector} from './Selector'
|
|||||||
import {HorzSwipe} from './gestures/HorzSwipe'
|
import {HorzSwipe} from './gestures/HorzSwipe'
|
||||||
import {useAnimatedValue} from '../../lib/useAnimatedValue'
|
import {useAnimatedValue} from '../../lib/useAnimatedValue'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
const HEADER_ITEM = {_reactKey: '__header__'}
|
const HEADER_ITEM = {_reactKey: '__header__'}
|
||||||
const SELECTOR_ITEM = {_reactKey: '__selector__'}
|
const SELECTOR_ITEM = {_reactKey: '__selector__'}
|
||||||
const STICKY_HEADER_INDICES = [1]
|
const STICKY_HEADER_INDICES = [1]
|
||||||
|
|
||||||
export function ViewSelector({
|
export const ViewSelector = register(
|
||||||
sections,
|
({
|
||||||
items,
|
sections,
|
||||||
refreshing,
|
items,
|
||||||
swipeEnabled,
|
refreshing,
|
||||||
renderHeader,
|
swipeEnabled,
|
||||||
renderItem,
|
renderHeader,
|
||||||
onSelectView,
|
renderItem,
|
||||||
onRefresh,
|
onSelectView,
|
||||||
onEndReached,
|
onRefresh,
|
||||||
}: {
|
onEndReached,
|
||||||
sections: string[]
|
}: {
|
||||||
items: any[]
|
sections: string[]
|
||||||
refreshing?: boolean
|
items: any[]
|
||||||
swipeEnabled?: boolean
|
refreshing?: boolean
|
||||||
renderHeader?: () => JSX.Element
|
swipeEnabled?: boolean
|
||||||
renderItem: (item: any) => JSX.Element
|
renderHeader?: () => JSX.Element
|
||||||
onSelectView?: (viewIndex: number) => void
|
renderItem: (item: any) => JSX.Element
|
||||||
onRefresh?: () => void
|
onSelectView?: (viewIndex: number) => void
|
||||||
onEndReached?: (info: {distanceFromEnd: number}) => void
|
onRefresh?: () => void
|
||||||
}) {
|
onEndReached?: (info: {distanceFromEnd: number}) => void
|
||||||
const store = useStores()
|
}) => {
|
||||||
const [selectedIndex, setSelectedIndex] = useState<number>(0)
|
const store = useStores()
|
||||||
const panX = useAnimatedValue(0)
|
const [selectedIndex, setSelectedIndex] = useState<number>(0)
|
||||||
|
const panX = useAnimatedValue(0)
|
||||||
|
|
||||||
// events
|
// events
|
||||||
// =
|
// =
|
||||||
|
|
||||||
const onSwipeEnd = (dx: number) => {
|
const onSwipeEnd = (dx: number) => {
|
||||||
if (dx !== 0) {
|
if (dx !== 0) {
|
||||||
setSelectedIndex(selectedIndex + dx)
|
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()
|
|
||||||
}
|
}
|
||||||
return <View />
|
|
||||||
} else if (item === SELECTOR_ITEM) {
|
|
||||||
return (
|
|
||||||
<Selector
|
|
||||||
items={sections}
|
|
||||||
panX={panX}
|
|
||||||
selectedIndex={selectedIndex}
|
|
||||||
onSelect={onPressSelection}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return renderItem(item)
|
|
||||||
}
|
}
|
||||||
}
|
const onPressSelection = (index: number) => setSelectedIndex(index)
|
||||||
|
useEffect(() => {
|
||||||
|
onSelectView?.(selectedIndex)
|
||||||
|
}, [selectedIndex])
|
||||||
|
|
||||||
const data = [HEADER_ITEM, SELECTOR_ITEM, ...items]
|
// rendering
|
||||||
return (
|
// =
|
||||||
<HorzSwipe
|
|
||||||
hasPriority
|
const renderItemInternal = ({item}: {item: any}) => {
|
||||||
panX={panX}
|
if (item === HEADER_ITEM) {
|
||||||
swipeEnabled={swipeEnabled || false}
|
if (renderHeader) {
|
||||||
canSwipeLeft={selectedIndex > 0}
|
return renderHeader()
|
||||||
canSwipeRight={selectedIndex < sections.length - 1}
|
}
|
||||||
onSwipeEnd={onSwipeEnd}>
|
return <View />
|
||||||
<FlatList
|
} else if (item === SELECTOR_ITEM) {
|
||||||
data={data}
|
return (
|
||||||
keyExtractor={item => item._reactKey}
|
<Selector
|
||||||
renderItem={renderItemInternal}
|
items={sections}
|
||||||
stickyHeaderIndices={STICKY_HEADER_INDICES}
|
panX={panX}
|
||||||
refreshing={refreshing}
|
selectedIndex={selectedIndex}
|
||||||
onRefresh={onRefresh}
|
onSelect={onPressSelection}
|
||||||
onEndReached={onEndReached}
|
/>
|
||||||
/>
|
)
|
||||||
</HorzSwipe>
|
} else {
|
||||||
)
|
return renderItem(item)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = [HEADER_ITEM, SELECTOR_ITEM, ...items]
|
||||||
|
return (
|
||||||
|
<HorzSwipe
|
||||||
|
hasPriority
|
||||||
|
panX={panX}
|
||||||
|
swipeEnabled={swipeEnabled || false}
|
||||||
|
canSwipeLeft={selectedIndex > 0}
|
||||||
|
canSwipeRight={selectedIndex < sections.length - 1}
|
||||||
|
onSwipeEnd={onSwipeEnd}>
|
||||||
|
<FlatList
|
||||||
|
data={data}
|
||||||
|
keyExtractor={item => item._reactKey}
|
||||||
|
renderItem={renderItemInternal}
|
||||||
|
stickyHeaderIndices={STICKY_HEADER_INDICES}
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
onEndReached={onEndReached}
|
||||||
|
/>
|
||||||
|
</HorzSwipe>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({})
|
const styles = StyleSheet.create({})
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import {colors} from '../lib/styles'
|
|||||||
import {ScreenParams} from '../routes'
|
import {ScreenParams} from '../routes'
|
||||||
import {useStores} from '../../state'
|
import {useStores} from '../../state'
|
||||||
import {useAnimatedValue} from '../lib/useAnimatedValue'
|
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 store = useStores()
|
||||||
const selectorInterp = useAnimatedValue(0)
|
const selectorInterp = useAnimatedValue(0)
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ export const Contacts = ({navIdx, visible, params}: ScreenParams) => {
|
|||||||
{!!store.me.handle && <ProfileFollowsComponent name={store.me.handle} />}
|
{!!store.me.handle && <ProfileFollowsComponent name={store.me.handle} />}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
section: {
|
section: {
|
||||||
|
|||||||
+91
-90
@@ -9,105 +9,106 @@ import {useStores} from '../../state'
|
|||||||
import {FeedModel} from '../../state/models/feed-view'
|
import {FeedModel} from '../../state/models/feed-view'
|
||||||
import {ScreenParams} from '../routes'
|
import {ScreenParams} from '../routes'
|
||||||
import {s, colors} from '../lib/styles'
|
import {s, colors} from '../lib/styles'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20}
|
const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20}
|
||||||
|
|
||||||
export const Home = observer(function Home({
|
export const Home = register(
|
||||||
navIdx,
|
observer(({navIdx, visible, scrollElRef}: ScreenParams) => {
|
||||||
visible,
|
const store = useStores()
|
||||||
scrollElRef,
|
const [hasSetup, setHasSetup] = useState<boolean>(false)
|
||||||
}: ScreenParams) {
|
const {appState} = useAppState({
|
||||||
const store = useStores()
|
onForeground: () => doPoll(true),
|
||||||
const [hasSetup, setHasSetup] = useState<boolean>(false)
|
|
||||||
const {appState} = useAppState({
|
|
||||||
onForeground: () => doPoll(true),
|
|
||||||
})
|
|
||||||
const defaultFeedView = useMemo<FeedModel>(
|
|
||||||
() =>
|
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
}
|
const defaultFeedView = useMemo<FeedModel>(
|
||||||
|
() =>
|
||||||
|
new FeedModel(store, 'home', {
|
||||||
|
algorithm: 'reverse-chronological',
|
||||||
|
}),
|
||||||
|
[store],
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
const doPoll = (knownActive = false) => {
|
||||||
let aborted = false
|
if ((!knownActive && appState !== 'active') || !visible) {
|
||||||
const pollInterval = setInterval(() => doPoll(), 15e3)
|
return
|
||||||
if (!visible) {
|
}
|
||||||
return
|
if (defaultFeedView.isLoading) {
|
||||||
}
|
return
|
||||||
|
}
|
||||||
if (hasSetup) {
|
console.log('Polling home feed')
|
||||||
console.log('Updating home feed')
|
defaultFeedView.checkForLatest().catch(e => {
|
||||||
defaultFeedView.update()
|
console.error('Failed to poll feed', e)
|
||||||
} else {
|
|
||||||
store.nav.setTitle(navIdx, 'Home')
|
|
||||||
console.log('Fetching home feed')
|
|
||||||
defaultFeedView.setup().then(() => {
|
|
||||||
if (aborted) return
|
|
||||||
setHasSetup(true)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return () => {
|
|
||||||
clearInterval(pollInterval)
|
useEffect(() => {
|
||||||
aborted = true
|
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 = () => {
|
return (
|
||||||
store.shell.openComposer({onPost: onCreatePost})
|
<View style={s.flex1}>
|
||||||
}
|
<ViewHeader
|
||||||
const onCreatePost = () => {
|
title="Bluesky"
|
||||||
defaultFeedView.loadLatest()
|
subtitle="Private Beta"
|
||||||
}
|
onPost={onCreatePost}
|
||||||
const onPressTryAgain = () => {
|
/>
|
||||||
defaultFeedView.refresh()
|
<Feed
|
||||||
}
|
key="default"
|
||||||
const onPressLoadLatest = () => {
|
feed={defaultFeedView}
|
||||||
defaultFeedView.refresh()
|
scrollElRef={scrollElRef}
|
||||||
scrollElRef?.current?.scrollToOffset({offset: 0})
|
style={{flex: 1}}
|
||||||
}
|
onPressCompose={onPressCompose}
|
||||||
|
onPressTryAgain={onPressTryAgain}
|
||||||
return (
|
/>
|
||||||
<View style={s.flex1}>
|
{defaultFeedView.hasNewLatest ? (
|
||||||
<ViewHeader
|
<TouchableOpacity
|
||||||
title="Bluesky"
|
style={styles.loadLatest}
|
||||||
subtitle="Private Beta"
|
onPress={onPressLoadLatest}
|
||||||
onPost={onCreatePost}
|
hitSlop={HITSLOP}>
|
||||||
/>
|
<FontAwesomeIcon icon="arrow-up" style={{color: colors.white}} />
|
||||||
<Feed
|
<Text style={styles.loadLatestText}>Load new posts</Text>
|
||||||
key="default"
|
</TouchableOpacity>
|
||||||
feed={defaultFeedView}
|
) : undefined}
|
||||||
scrollElRef={scrollElRef}
|
</View>
|
||||||
style={{flex: 1}}
|
)
|
||||||
onPressCompose={onPressCompose}
|
}),
|
||||||
onPressTryAgain={onPressTryAgain}
|
)
|
||||||
/>
|
|
||||||
{defaultFeedView.hasNewLatest ? (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.loadLatest}
|
|
||||||
onPress={onPressLoadLatest}
|
|
||||||
hitSlop={HITSLOP}>
|
|
||||||
<FontAwesomeIcon icon="arrow-up" style={{color: colors.white}} />
|
|
||||||
<Text style={styles.loadLatestText}>Load new posts</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
) : undefined}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
loadLatest: {
|
loadLatest: {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {ServiceDescription} from '../../state/models/session'
|
|||||||
import {ServerInputModel} from '../../state/models/shell-ui'
|
import {ServerInputModel} from '../../state/models/shell-ui'
|
||||||
import {ComAtprotoAccountCreate} from '../../third-party/api/index'
|
import {ComAtprotoAccountCreate} from '../../third-party/api/index'
|
||||||
import {isNetworkError} from '../../lib/errors'
|
import {isNetworkError} from '../../lib/errors'
|
||||||
|
import {investigate} from 'react-native-bundle-splitter/dist/utils'
|
||||||
|
|
||||||
enum ScreenState {
|
enum ScreenState {
|
||||||
SigninOrCreateAccount,
|
SigninOrCreateAccount,
|
||||||
@@ -589,6 +590,12 @@ export const Login = observer(
|
|||||||
ScreenState.SigninOrCreateAccount,
|
ScreenState.SigninOrCreateAccount,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`loaded: ${investigate().loaded.length} \n waiting: ${
|
||||||
|
investigate().waiting.length
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.outer}>
|
<View style={styles.outer}>
|
||||||
{screenState === ScreenState.SigninOrCreateAccount ? (
|
{screenState === ScreenState.SigninOrCreateAccount ? (
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import React from 'react'
|
|||||||
import {Text, Button, View} from 'react-native'
|
import {Text, Button, View} from 'react-native'
|
||||||
import {ViewHeader} from '../com/util/ViewHeader'
|
import {ViewHeader} from '../com/util/ViewHeader'
|
||||||
import {useStores} from '../../state'
|
import {useStores} from '../../state'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const NotFound = () => {
|
export const NotFound = register(() => {
|
||||||
const stores = useStores()
|
const stores = useStores()
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
@@ -19,4 +20,4 @@ export const NotFound = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import {Feed} from '../com/notifications/Feed'
|
|||||||
import {useStores} from '../../state'
|
import {useStores} from '../../state'
|
||||||
import {NotificationsViewModel} from '../../state/models/notifications-view'
|
import {NotificationsViewModel} from '../../state/models/notifications-view'
|
||||||
import {ScreenParams} from '../routes'
|
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()
|
const store = useStores()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -36,4 +37,4 @@ export const Notifications = ({navIdx, visible}: ScreenParams) => {
|
|||||||
<Feed view={store.me.notifications} onPressTryAgain={onPressTryAgain} />
|
<Feed view={store.me.notifications} onPressTryAgain={onPressTryAgain} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|||||||
@@ -5,29 +5,32 @@ import {FeatureExplainer} from '../com/onboard/FeatureExplainer'
|
|||||||
import {Follows} from '../com/onboard/Follows'
|
import {Follows} from '../com/onboard/Follows'
|
||||||
import {OnboardStage, OnboardStageOrder} from '../../state/models/onboard'
|
import {OnboardStage, OnboardStageOrder} from '../../state/models/onboard'
|
||||||
import {useStores} from '../../state'
|
import {useStores} from '../../state'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const Onboard = observer(() => {
|
export const Onboard = register(
|
||||||
const store = useStores()
|
observer(() => {
|
||||||
|
const store = useStores()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// sanity check - bounce out of onboarding if the stage is wrong somehow
|
// sanity check - bounce out of onboarding if the stage is wrong somehow
|
||||||
if (!OnboardStageOrder.includes(store.onboard.stage)) {
|
if (!OnboardStageOrder.includes(store.onboard.stage)) {
|
||||||
store.onboard.stop()
|
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
|
return (
|
||||||
if (store.onboard.stage === OnboardStage.Explainers) {
|
<View style={{flex: 1, backgroundColor: '#fff'}}>
|
||||||
Com = FeatureExplainer
|
<Com />
|
||||||
} else if (store.onboard.stage === OnboardStage.Follows) {
|
</View>
|
||||||
Com = Follows
|
)
|
||||||
} else {
|
}),
|
||||||
Com = View
|
)
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={{flex: 1, backgroundColor: '#fff'}}>
|
|
||||||
<Com />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -5,22 +5,25 @@ import {PostVotedBy as PostLikedByComponent} from '../com/post-thread/PostVotedB
|
|||||||
import {ScreenParams} from '../routes'
|
import {ScreenParams} from '../routes'
|
||||||
import {useStores} from '../../state'
|
import {useStores} from '../../state'
|
||||||
import {makeRecordUri} from '../../lib/strings'
|
import {makeRecordUri} from '../../lib/strings'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const PostDownvotedBy = ({navIdx, visible, params}: ScreenParams) => {
|
export const PostDownvotedBy = register(
|
||||||
const store = useStores()
|
({navIdx, visible, params}: ScreenParams) => {
|
||||||
const {name, rkey} = params
|
const store = useStores()
|
||||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
const {name, rkey} = params
|
||||||
|
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
store.nav.setTitle(navIdx, 'Downvoted by')
|
store.nav.setTitle(navIdx, 'Downvoted by')
|
||||||
}
|
}
|
||||||
}, [store, visible])
|
}, [store, visible])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ViewHeader title="Downvoted by" />
|
<ViewHeader title="Downvoted by" />
|
||||||
<PostLikedByComponent uri={uri} direction="down" />
|
<PostLikedByComponent uri={uri} direction="down" />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -5,22 +5,25 @@ import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/Post
|
|||||||
import {ScreenParams} from '../routes'
|
import {ScreenParams} from '../routes'
|
||||||
import {useStores} from '../../state'
|
import {useStores} from '../../state'
|
||||||
import {makeRecordUri} from '../../lib/strings'
|
import {makeRecordUri} from '../../lib/strings'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const PostRepostedBy = ({navIdx, visible, params}: ScreenParams) => {
|
export const PostRepostedBy = register(
|
||||||
const store = useStores()
|
({navIdx, visible, params}: ScreenParams) => {
|
||||||
const {name, rkey} = params
|
const store = useStores()
|
||||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
const {name, rkey} = params
|
||||||
|
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
store.nav.setTitle(navIdx, 'Reposted by')
|
store.nav.setTitle(navIdx, 'Reposted by')
|
||||||
}
|
}
|
||||||
}, [store, visible])
|
}, [store, visible])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ViewHeader title="Reposted by" />
|
<ViewHeader title="Reposted by" />
|
||||||
<PostRepostedByComponent uri={uri} />
|
<PostRepostedByComponent uri={uri} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -6,53 +6,56 @@ import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
|
|||||||
import {PostThreadViewModel} from '../../state/models/post-thread-view'
|
import {PostThreadViewModel} from '../../state/models/post-thread-view'
|
||||||
import {ScreenParams} from '../routes'
|
import {ScreenParams} from '../routes'
|
||||||
import {useStores} from '../../state'
|
import {useStores} from '../../state'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
|
export const PostThread = register(
|
||||||
const store = useStores()
|
({navIdx, visible, params}: ScreenParams) => {
|
||||||
const {name, rkey} = params
|
const store = useStores()
|
||||||
const [viewSubtitle, setViewSubtitle] = useState<string>(`by ${name}`)
|
const {name, rkey} = params
|
||||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
const [viewSubtitle, setViewSubtitle] = useState<string>(`by ${name}`)
|
||||||
const view = useMemo<PostThreadViewModel>(
|
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||||
() => new PostThreadViewModel(store, {uri}),
|
const view = useMemo<PostThreadViewModel>(
|
||||||
[uri],
|
() => new PostThreadViewModel(store, {uri}),
|
||||||
)
|
[uri],
|
||||||
|
)
|
||||||
|
|
||||||
const setTitle = () => {
|
const setTitle = () => {
|
||||||
const author = view.thread?.author
|
const author = view.thread?.author
|
||||||
const niceName = author?.handle || name
|
const niceName = author?.handle || name
|
||||||
setViewSubtitle(`by ${niceName}`)
|
setViewSubtitle(`by ${niceName}`)
|
||||||
store.nav.setTitle(navIdx, `Post by ${niceName}`)
|
store.nav.setTitle(navIdx, `Post by ${niceName}`)
|
||||||
}
|
|
||||||
useEffect(() => {
|
|
||||||
let aborted = false
|
|
||||||
if (!visible) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
setTitle()
|
useEffect(() => {
|
||||||
if (!view.hasLoaded && !view.isLoading) {
|
let aborted = false
|
||||||
console.log('Fetching post thread', uri)
|
if (!visible) {
|
||||||
view.setup().then(
|
return
|
||||||
() => {
|
}
|
||||||
if (!aborted) {
|
setTitle()
|
||||||
setTitle()
|
if (!view.hasLoaded && !view.isLoading) {
|
||||||
}
|
console.log('Fetching post thread', uri)
|
||||||
},
|
view.setup().then(
|
||||||
err => {
|
() => {
|
||||||
console.error('Failed to fetch thread', err)
|
if (!aborted) {
|
||||||
},
|
setTitle()
|
||||||
)
|
}
|
||||||
}
|
},
|
||||||
return () => {
|
err => {
|
||||||
aborted = true
|
console.error('Failed to fetch thread', err)
|
||||||
}
|
},
|
||||||
}, [visible, store.nav, name])
|
)
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
aborted = true
|
||||||
|
}
|
||||||
|
}, [visible, store.nav, name])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{flex: 1}}>
|
|
||||||
<ViewHeader title="Post" subtitle={viewSubtitle} />
|
|
||||||
<View style={{flex: 1}}>
|
<View style={{flex: 1}}>
|
||||||
<PostThreadComponent uri={uri} view={view} />
|
<ViewHeader title="Post" subtitle={viewSubtitle} />
|
||||||
|
<View style={{flex: 1}}>
|
||||||
|
<PostThreadComponent uri={uri} view={view} />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
)
|
||||||
)
|
},
|
||||||
}
|
)
|
||||||
|
|||||||
@@ -5,22 +5,25 @@ import {PostVotedBy as PostLikedByComponent} from '../com/post-thread/PostVotedB
|
|||||||
import {ScreenParams} from '../routes'
|
import {ScreenParams} from '../routes'
|
||||||
import {useStores} from '../../state'
|
import {useStores} from '../../state'
|
||||||
import {makeRecordUri} from '../../lib/strings'
|
import {makeRecordUri} from '../../lib/strings'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const PostUpvotedBy = ({navIdx, visible, params}: ScreenParams) => {
|
export const PostUpvotedBy = register(
|
||||||
const store = useStores()
|
({navIdx, visible, params}: ScreenParams) => {
|
||||||
const {name, rkey} = params
|
const store = useStores()
|
||||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
const {name, rkey} = params
|
||||||
|
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
store.nav.setTitle(navIdx, 'Upvoted by')
|
store.nav.setTitle(navIdx, 'Upvoted by')
|
||||||
}
|
}
|
||||||
}, [store, visible])
|
}, [store, visible])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ViewHeader title="Upvoted by" />
|
<ViewHeader title="Upvoted by" />
|
||||||
<PostLikedByComponent uri={uri} direction="up" />
|
<PostLikedByComponent uri={uri} direction="up" />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
|||||||
+214
-209
@@ -18,243 +18,248 @@ import {EmptyState} from '../com/util/EmptyState'
|
|||||||
import {ViewHeader} from '../com/util/ViewHeader'
|
import {ViewHeader} from '../com/util/ViewHeader'
|
||||||
import * as Toast from '../com/util/Toast'
|
import * as Toast from '../com/util/Toast'
|
||||||
import {s, colors} from '../lib/styles'
|
import {s, colors} from '../lib/styles'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
const LOADING_ITEM = {_reactKey: '__loading__'}
|
const LOADING_ITEM = {_reactKey: '__loading__'}
|
||||||
const END_ITEM = {_reactKey: '__end__'}
|
const END_ITEM = {_reactKey: '__end__'}
|
||||||
const EMPTY_ITEM = {_reactKey: '__empty__'}
|
const EMPTY_ITEM = {_reactKey: '__empty__'}
|
||||||
|
|
||||||
export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
|
export const Profile = register(
|
||||||
const store = useStores()
|
observer(({navIdx, visible, params}: ScreenParams) => {
|
||||||
const [hasSetup, setHasSetup] = useState<boolean>(false)
|
const store = useStores()
|
||||||
const uiState = useMemo(
|
const [hasSetup, setHasSetup] = useState<boolean>(false)
|
||||||
() => new ProfileUiModel(store, {user: params.name}),
|
const uiState = useMemo(
|
||||||
[params.user],
|
() => 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`)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// 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 =
|
// events
|
||||||
uiState.isScene && store.me.did === uiState.profile.creator
|
// =
|
||||||
|
|
||||||
const renderHeader = () => {
|
const onSelectView = (index: number) => {
|
||||||
if (!uiState) {
|
uiState.setSelectedViewIndex(index)
|
||||||
return <View />
|
|
||||||
}
|
}
|
||||||
return <ProfileHeader view={uiState.profile} onRefreshAll={onRefresh} />
|
const onRefresh = () => {
|
||||||
}
|
uiState
|
||||||
let renderItem
|
.refresh()
|
||||||
let items: any[] = []
|
.catch((err: any) => console.error('Failed to refresh', err))
|
||||||
if (uiState) {
|
}
|
||||||
if (uiState.isInitialLoading) {
|
const onEndReached = () => {
|
||||||
items.push(LOADING_ITEM)
|
uiState
|
||||||
renderItem = () => <PostFeedLoadingPlaceholder />
|
.loadMore()
|
||||||
} else if (uiState.currentView.hasError) {
|
.catch((err: any) => console.error('Failed to load more', err))
|
||||||
items.push({
|
}
|
||||||
_reactKey: '__error__',
|
const onPressTryAgain = () => {
|
||||||
error: uiState.currentView.error,
|
uiState.setup()
|
||||||
})
|
}
|
||||||
renderItem = (item: any) => (
|
const onPressRemoveMember = (membership: MembershipItem) => {
|
||||||
<View style={s.p5}>
|
store.shell.openModal(
|
||||||
<ErrorMessage
|
new ConfirmModel(
|
||||||
dark
|
`Remove ${membership.displayName || membership.handle}?`,
|
||||||
message={item.error}
|
"You'll be able to invite them again if you change your mind.",
|
||||||
onPressTryAgain={onPressTryAgain}
|
async () => {
|
||||||
/>
|
await uiState.members.removeMember(membership.did)
|
||||||
</View>
|
Toast.show('User removed')
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
} else {
|
}
|
||||||
if (
|
|
||||||
uiState.selectedView === Sections.Posts ||
|
// rendering
|
||||||
uiState.selectedView === Sections.PostsWithReplies ||
|
// =
|
||||||
uiState.selectedView === Sections.Trending
|
|
||||||
) {
|
const isSceneCreator =
|
||||||
if (uiState.feed.hasContent) {
|
uiState.isScene && store.me.did === uiState.profile.creator
|
||||||
if (uiState.selectedView === Sections.Posts) {
|
|
||||||
items = uiState.feed.nonReplyFeed
|
const renderHeader = () => {
|
||||||
} else {
|
if (!uiState) {
|
||||||
items = uiState.feed.feed.slice()
|
return <View />
|
||||||
}
|
}
|
||||||
if (!uiState.feed.hasMore) {
|
return <ProfileHeader view={uiState.profile} onRefreshAll={onRefresh} />
|
||||||
items.push(END_ITEM)
|
}
|
||||||
}
|
let renderItem
|
||||||
renderItem = (item: any) => {
|
let items: any[] = []
|
||||||
if (item === END_ITEM) {
|
if (uiState) {
|
||||||
return <Text style={styles.endItem}>- end of feed -</Text>
|
if (uiState.isInitialLoading) {
|
||||||
|
items.push(LOADING_ITEM)
|
||||||
|
renderItem = () => <PostFeedLoadingPlaceholder />
|
||||||
|
} else if (uiState.currentView.hasError) {
|
||||||
|
items.push({
|
||||||
|
_reactKey: '__error__',
|
||||||
|
error: uiState.currentView.error,
|
||||||
|
})
|
||||||
|
renderItem = (item: any) => (
|
||||||
|
<View style={s.p5}>
|
||||||
|
<ErrorMessage
|
||||||
|
dark
|
||||||
|
message={item.error}
|
||||||
|
onPressTryAgain={onPressTryAgain}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
} 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 <Text style={styles.endItem}>- end of feed -</Text>
|
||||||
|
}
|
||||||
|
return <FeedItem item={item} />
|
||||||
|
}
|
||||||
|
} else if (uiState.feed.isEmpty) {
|
||||||
|
items.push(EMPTY_ITEM)
|
||||||
|
if (uiState.profile.isScene) {
|
||||||
|
renderItem = () => (
|
||||||
|
<EmptyState
|
||||||
|
icon="user-group"
|
||||||
|
message="As members upvote posts, they will trend here. Follow the scene to see its trending posts in your timeline."
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
renderItem = () => (
|
||||||
|
<EmptyState
|
||||||
|
icon={['far', 'message']}
|
||||||
|
message="No posts yet!"
|
||||||
|
style={{paddingVertical: 40}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return <FeedItem item={item} />
|
|
||||||
}
|
}
|
||||||
} else if (uiState.feed.isEmpty) {
|
} else if (uiState.selectedView === Sections.Scenes) {
|
||||||
items.push(EMPTY_ITEM)
|
if (uiState.memberships.hasContent) {
|
||||||
if (uiState.profile.isScene) {
|
items = uiState.memberships.memberships.slice()
|
||||||
|
renderItem = (item: any) => {
|
||||||
|
return (
|
||||||
|
<ProfileCard
|
||||||
|
did={item.did}
|
||||||
|
handle={item.handle}
|
||||||
|
displayName={item.displayName}
|
||||||
|
avatar={item.avatar}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (uiState.memberships.isEmpty) {
|
||||||
|
items.push(EMPTY_ITEM)
|
||||||
renderItem = () => (
|
renderItem = () => (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon="user-group"
|
icon="user-group"
|
||||||
message="As members upvote posts, they will trend here. Follow the scene to see its trending posts in your timeline."
|
message="This user hasn't joined any scenes."
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} 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
|
||||||
|
? () => (
|
||||||
|
<>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="user-xmark"
|
||||||
|
style={[s.mr5]}
|
||||||
|
size={14}
|
||||||
|
/>
|
||||||
|
<Text style={[s.fw400, s.f14]}>Remove</Text>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
return (
|
||||||
|
<ProfileCard
|
||||||
|
did={item.did}
|
||||||
|
handle={item.handle}
|
||||||
|
displayName={item.displayName}
|
||||||
|
avatar={item.avatar}
|
||||||
|
renderButton={renderButton}
|
||||||
|
onPressButton={() => onPressRemoveMember(item)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (uiState.members.isEmpty) {
|
||||||
|
items.push(EMPTY_ITEM)
|
||||||
renderItem = () => (
|
renderItem = () => (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={['far', 'message']}
|
icon="user-group"
|
||||||
message="No posts yet!"
|
message="This scene doesn't have any members."
|
||||||
style={{paddingVertical: 40}}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
} else if (uiState.selectedView === Sections.Scenes) {
|
|
||||||
if (uiState.memberships.hasContent) {
|
|
||||||
items = uiState.memberships.memberships.slice()
|
|
||||||
renderItem = (item: any) => {
|
|
||||||
return (
|
|
||||||
<ProfileCard
|
|
||||||
did={item.did}
|
|
||||||
handle={item.handle}
|
|
||||||
displayName={item.displayName}
|
|
||||||
avatar={item.avatar}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (uiState.memberships.isEmpty) {
|
|
||||||
items.push(EMPTY_ITEM)
|
items.push(EMPTY_ITEM)
|
||||||
renderItem = () => (
|
renderItem = () => <Text>TODO</Text>
|
||||||
<EmptyState
|
|
||||||
icon="user-group"
|
|
||||||
message="This user hasn't joined any scenes."
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} 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
|
|
||||||
? () => (
|
|
||||||
<>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="user-xmark"
|
|
||||||
style={[s.mr5]}
|
|
||||||
size={14}
|
|
||||||
/>
|
|
||||||
<Text style={[s.fw400, s.f14]}>Remove</Text>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
return (
|
|
||||||
<ProfileCard
|
|
||||||
did={item.did}
|
|
||||||
handle={item.handle}
|
|
||||||
displayName={item.displayName}
|
|
||||||
avatar={item.avatar}
|
|
||||||
renderButton={renderButton}
|
|
||||||
onPressButton={() => onPressRemoveMember(item)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (uiState.members.isEmpty) {
|
|
||||||
items.push(EMPTY_ITEM)
|
|
||||||
renderItem = () => (
|
|
||||||
<EmptyState
|
|
||||||
icon="user-group"
|
|
||||||
message="This scene doesn't have any members."
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
items.push(EMPTY_ITEM)
|
|
||||||
renderItem = () => <Text>TODO</Text>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (!renderItem) {
|
||||||
if (!renderItem) {
|
renderItem = () => <View />
|
||||||
renderItem = () => <View />
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const title =
|
const title =
|
||||||
uiState.profile.displayName || uiState.profile.handle || params.name
|
uiState.profile.displayName || uiState.profile.handle || params.name
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<ViewHeader title={title} />
|
<ViewHeader title={title} />
|
||||||
{uiState.profile.hasError ? (
|
{uiState.profile.hasError ? (
|
||||||
<ErrorScreen
|
<ErrorScreen
|
||||||
title="Failed to load profile"
|
title="Failed to load profile"
|
||||||
message={`There was an issue when attempting to load ${params.name}`}
|
message={`There was an issue when attempting to load ${params.name}`}
|
||||||
details={uiState.profile.error}
|
details={uiState.profile.error}
|
||||||
onPressTryAgain={onPressTryAgain}
|
onPressTryAgain={onPressTryAgain}
|
||||||
/>
|
/>
|
||||||
) : uiState.profile.hasLoaded ? (
|
) : uiState.profile.hasLoaded ? (
|
||||||
<ViewSelector
|
<ViewSelector
|
||||||
swipeEnabled
|
swipeEnabled
|
||||||
sections={uiState.selectorItems}
|
sections={uiState.selectorItems}
|
||||||
items={items}
|
items={items}
|
||||||
renderHeader={renderHeader}
|
renderHeader={renderHeader}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
refreshing={uiState.isRefreshing || false}
|
refreshing={uiState.isRefreshing || false}
|
||||||
onSelectView={onSelectView}
|
onSelectView={onSelectView}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
renderHeader()
|
renderHeader()
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
|||||||
@@ -4,21 +4,24 @@ import {ViewHeader} from '../com/util/ViewHeader'
|
|||||||
import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/ProfileFollowers'
|
import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/ProfileFollowers'
|
||||||
import {ScreenParams} from '../routes'
|
import {ScreenParams} from '../routes'
|
||||||
import {useStores} from '../../state'
|
import {useStores} from '../../state'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const ProfileFollowers = ({navIdx, visible, params}: ScreenParams) => {
|
export const ProfileFollowers = register(
|
||||||
const store = useStores()
|
({navIdx, visible, params}: ScreenParams) => {
|
||||||
const {name} = params
|
const store = useStores()
|
||||||
|
const {name} = params
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
store.nav.setTitle(navIdx, `Followers of ${name}`)
|
store.nav.setTitle(navIdx, `Followers of ${name}`)
|
||||||
}
|
}
|
||||||
}, [store, visible, name])
|
}, [store, visible, name])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ViewHeader title="Followers" subtitle={`of ${name}`} />
|
<ViewHeader title="Followers" subtitle={`of ${name}`} />
|
||||||
<ProfileFollowersComponent name={name} />
|
<ProfileFollowersComponent name={name} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,21 +4,24 @@ import {ViewHeader} from '../com/util/ViewHeader'
|
|||||||
import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows'
|
import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows'
|
||||||
import {ScreenParams} from '../routes'
|
import {ScreenParams} from '../routes'
|
||||||
import {useStores} from '../../state'
|
import {useStores} from '../../state'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const ProfileFollows = ({navIdx, visible, params}: ScreenParams) => {
|
export const ProfileFollows = register(
|
||||||
const store = useStores()
|
({navIdx, visible, params}: ScreenParams) => {
|
||||||
const {name} = params
|
const store = useStores()
|
||||||
|
const {name} = params
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
store.nav.setTitle(navIdx, `Followed by ${name}`)
|
store.nav.setTitle(navIdx, `Followed by ${name}`)
|
||||||
}
|
}
|
||||||
}, [store, visible, name])
|
}, [store, visible, name])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ViewHeader title="Followed" subtitle={`by ${name}`} />
|
<ViewHeader title="Followed" subtitle={`by ${name}`} />
|
||||||
<ProfileFollowsComponent name={name} />
|
<ProfileFollowsComponent name={name} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,21 +4,24 @@ import {ViewHeader} from '../com/util/ViewHeader'
|
|||||||
import {ProfileMembers as ProfileMembersComponent} from '../com/profile/ProfileMembers'
|
import {ProfileMembers as ProfileMembersComponent} from '../com/profile/ProfileMembers'
|
||||||
import {ScreenParams} from '../routes'
|
import {ScreenParams} from '../routes'
|
||||||
import {useStores} from '../../state'
|
import {useStores} from '../../state'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const ProfileMembers = ({navIdx, visible, params}: ScreenParams) => {
|
export const ProfileMembers = register(
|
||||||
const store = useStores()
|
({navIdx, visible, params}: ScreenParams) => {
|
||||||
const {name} = params
|
const store = useStores()
|
||||||
|
const {name} = params
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
store.nav.setTitle(navIdx, `Members of ${name}`)
|
store.nav.setTitle(navIdx, `Members of ${name}`)
|
||||||
}
|
}
|
||||||
}, [store, visible, name])
|
}, [store, visible, name])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ViewHeader title="Members" subtitle={`of ${name}`} />
|
<ViewHeader title="Members" subtitle={`of ${name}`} />
|
||||||
<ProfileMembersComponent name={name} />
|
<ProfileMembersComponent name={name} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ import {useStores} from '../../state'
|
|||||||
import {UserAutocompleteViewModel} from '../../state/models/user-autocomplete-view'
|
import {UserAutocompleteViewModel} from '../../state/models/user-autocomplete-view'
|
||||||
import {s, colors} from '../lib/styles'
|
import {s, colors} from '../lib/styles'
|
||||||
import {MagnifyingGlassIcon} from '../lib/icons'
|
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 store = useStores()
|
||||||
const textInput = useRef<TextInput>(null)
|
const textInput = useRef<TextInput>(null)
|
||||||
const [query, setQuery] = useState<string>('')
|
const [query, setQuery] = useState<string>('')
|
||||||
@@ -92,7 +93,7 @@ export const Search = ({navIdx, visible, params}: ScreenParams) => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
|||||||
@@ -7,55 +7,55 @@ import {s, colors} from '../lib/styles'
|
|||||||
import {ViewHeader} from '../com/util/ViewHeader'
|
import {ViewHeader} from '../com/util/ViewHeader'
|
||||||
import {Link} from '../com/util/Link'
|
import {Link} from '../com/util/Link'
|
||||||
import {UserAvatar} from '../com/util/UserAvatar'
|
import {UserAvatar} from '../com/util/UserAvatar'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const Settings = observer(function Settings({
|
export const Settings = register(
|
||||||
navIdx,
|
observer(function Settings({navIdx, visible}: ScreenParams) {
|
||||||
visible,
|
const store = useStores()
|
||||||
}: ScreenParams) {
|
|
||||||
const store = useStores()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
store.nav.setTitle(navIdx, 'Settings')
|
||||||
|
}, [visible, store])
|
||||||
|
|
||||||
|
const onPressSignout = () => {
|
||||||
|
store.session.logout()
|
||||||
}
|
}
|
||||||
store.nav.setTitle(navIdx, 'Settings')
|
|
||||||
}, [visible, store])
|
|
||||||
|
|
||||||
const onPressSignout = () => {
|
return (
|
||||||
store.session.logout()
|
<View style={[s.flex1]}>
|
||||||
}
|
<ViewHeader title="Settings" />
|
||||||
|
<View style={[s.mt10, s.pl10, s.pr10]}>
|
||||||
return (
|
<View style={[s.flexRow]}>
|
||||||
<View style={[s.flex1]}>
|
<Text>Signed in as</Text>
|
||||||
<ViewHeader title="Settings" />
|
<View style={s.flex1} />
|
||||||
<View style={[s.mt10, s.pl10, s.pr10]}>
|
<TouchableOpacity onPress={onPressSignout}>
|
||||||
<View style={[s.flexRow]}>
|
<Text style={[s.blue3, s.bold]}>Sign out</Text>
|
||||||
<Text>Signed in as</Text>
|
</TouchableOpacity>
|
||||||
<View style={s.flex1} />
|
|
||||||
<TouchableOpacity onPress={onPressSignout}>
|
|
||||||
<Text style={[s.blue3, s.bold]}>Sign out</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
<Link href={`/profile/${store.me.handle}`} title="Your profile">
|
|
||||||
<View style={styles.profile}>
|
|
||||||
<UserAvatar
|
|
||||||
size={40}
|
|
||||||
displayName={store.me.displayName}
|
|
||||||
handle={store.me.handle || ''}
|
|
||||||
avatar={store.me.avatar}
|
|
||||||
/>
|
|
||||||
<View style={[s.ml10]}>
|
|
||||||
<Text style={[s.f18]}>
|
|
||||||
{store.me.displayName || store.me.handle}
|
|
||||||
</Text>
|
|
||||||
<Text style={[s.gray5]}>@{store.me.handle}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</Link>
|
<Link href={`/profile/${store.me.handle}`} title="Your profile">
|
||||||
|
<View style={styles.profile}>
|
||||||
|
<UserAvatar
|
||||||
|
size={40}
|
||||||
|
displayName={store.me.displayName}
|
||||||
|
handle={store.me.handle || ''}
|
||||||
|
avatar={store.me.avatar}
|
||||||
|
/>
|
||||||
|
<View style={[s.ml10]}>
|
||||||
|
<Text style={[s.f18]}>
|
||||||
|
{store.me.displayName || store.me.handle}
|
||||||
|
</Text>
|
||||||
|
<Text style={[s.gray5]}>@{store.me.handle}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
)
|
||||||
)
|
}),
|
||||||
})
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
title: {
|
title: {
|
||||||
|
|||||||
@@ -4,62 +4,65 @@ import {Animated, Easing, StyleSheet, View} from 'react-native'
|
|||||||
import {ComposePost} from '../../com/composer/ComposePost'
|
import {ComposePost} from '../../com/composer/ComposePost'
|
||||||
import {ComposerOpts} from '../../../state/models/shell-ui'
|
import {ComposerOpts} from '../../../state/models/shell-ui'
|
||||||
import {useAnimatedValue} from '../../lib/useAnimatedValue'
|
import {useAnimatedValue} from '../../lib/useAnimatedValue'
|
||||||
|
import {register} from 'react-native-bundle-splitter'
|
||||||
|
|
||||||
export const Composer = observer(
|
export const Composer = register(
|
||||||
({
|
observer(
|
||||||
active,
|
({
|
||||||
winHeight,
|
active,
|
||||||
replyTo,
|
winHeight,
|
||||||
onPost,
|
replyTo,
|
||||||
onClose,
|
onPost,
|
||||||
}: {
|
onClose,
|
||||||
active: boolean
|
}: {
|
||||||
winHeight: number
|
active: boolean
|
||||||
replyTo?: ComposerOpts['replyTo']
|
winHeight: number
|
||||||
onPost?: ComposerOpts['onPost']
|
replyTo?: ComposerOpts['replyTo']
|
||||||
onClose: () => void
|
onPost?: ComposerOpts['onPost']
|
||||||
}) => {
|
onClose: () => void
|
||||||
const initInterp = useAnimatedValue(0)
|
}) => {
|
||||||
|
const initInterp = useAnimatedValue(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (active) {
|
if (active) {
|
||||||
Animated.timing(initInterp, {
|
Animated.timing(initInterp, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 300,
|
duration: 300,
|
||||||
easing: Easing.out(Easing.exp),
|
easing: Easing.out(Easing.exp),
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}).start()
|
}).start()
|
||||||
} else {
|
} else {
|
||||||
initInterp.setValue(0)
|
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) {
|
if (!active) {
|
||||||
return <View />
|
return <View />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View style={[styles.wrapper, wrapperAnimStyle]}>
|
<Animated.View style={[styles.wrapper, wrapperAnimStyle]}>
|
||||||
<ComposePost replyTo={replyTo} onPost={onPost} onClose={onClose} />
|
<ComposePost replyTo={replyTo} onPost={onPost} onClose={onClose} />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
|||||||
@@ -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"
|
resolved "https://registry.yarnpkg.com/react-native-appstate-hook/-/react-native-appstate-hook-1.0.6.tgz#cbc16e7b89cfaea034cabd999f00e99053cabd06"
|
||||||
integrity sha512-0hPVyf5yLxCSVrrNEuGqN1ZnSSj3Ye2gZex0NtcK/AHYwMc0rXWFNZjBKOoZSouspqu3hXBbQ6NOUSTzrME1AQ==
|
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:
|
react-native-codegen@^0.0.17:
|
||||||
version "0.0.17"
|
version "0.0.17"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.0.17.tgz#83fb814d94061cbd46667f510d2ddba35ffb50ac"
|
resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.0.17.tgz#83fb814d94061cbd46667f510d2ddba35ffb50ac"
|
||||||
|
|||||||
Reference in New Issue
Block a user