parent
00595591c4
commit
a5b89dffa6
|
@ -2,12 +2,14 @@ module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
extends: [
|
extends: [
|
||||||
'@react-native-community',
|
'@react-native-community',
|
||||||
|
'plugin:react/recommended',
|
||||||
'plugin:react-native-a11y/ios',
|
'plugin:react-native-a11y/ios',
|
||||||
'prettier',
|
'prettier',
|
||||||
],
|
],
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
plugins: ['@typescript-eslint', 'detox'],
|
plugins: ['@typescript-eslint', 'detox', 'react'],
|
||||||
rules: {
|
rules: {
|
||||||
|
'react/no-unescaped-entities': 0,
|
||||||
'react-native/no-inline-styles': 0,
|
'react-native/no-inline-styles': 0,
|
||||||
},
|
},
|
||||||
ignorePatterns: [
|
ignorePatterns: [
|
||||||
|
|
|
@ -181,6 +181,7 @@
|
||||||
"eslint": "^8.19.0",
|
"eslint": "^8.19.0",
|
||||||
"eslint-plugin-detox": "^1.0.0",
|
"eslint-plugin-detox": "^1.0.0",
|
||||||
"eslint-plugin-ft-flow": "^2.0.3",
|
"eslint-plugin-ft-flow": "^2.0.3",
|
||||||
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-react-native-a11y": "^3.3.0",
|
"eslint-plugin-react-native-a11y": "^3.3.0",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
|
|
|
@ -51,181 +51,179 @@ interface Selection {
|
||||||
end: number
|
end: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextInput = forwardRef(
|
export const TextInput = forwardRef(function TextInputImpl(
|
||||||
(
|
{
|
||||||
{
|
richtext,
|
||||||
richtext,
|
placeholder,
|
||||||
placeholder,
|
suggestedLinks,
|
||||||
suggestedLinks,
|
autocompleteView,
|
||||||
autocompleteView,
|
setRichText,
|
||||||
setRichText,
|
onPhotoPasted,
|
||||||
onPhotoPasted,
|
onSuggestedLinksChanged,
|
||||||
onSuggestedLinksChanged,
|
onError,
|
||||||
onError,
|
...props
|
||||||
...props
|
}: TextInputProps,
|
||||||
}: TextInputProps,
|
ref,
|
||||||
ref,
|
) {
|
||||||
) => {
|
const pal = usePalette('default')
|
||||||
const pal = usePalette('default')
|
const textInput = useRef<PasteInputRef>(null)
|
||||||
const textInput = useRef<PasteInputRef>(null)
|
const textInputSelection = useRef<Selection>({start: 0, end: 0})
|
||||||
const textInputSelection = useRef<Selection>({start: 0, end: 0})
|
const theme = useTheme()
|
||||||
const theme = useTheme()
|
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
React.useImperativeHandle(ref, () => ({
|
||||||
focus: () => textInput.current?.focus(),
|
focus: () => textInput.current?.focus(),
|
||||||
blur: () => {
|
blur: () => {
|
||||||
textInput.current?.blur()
|
textInput.current?.blur()
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const onChangeText = useCallback(
|
const onChangeText = useCallback(
|
||||||
(newText: string) => {
|
(newText: string) => {
|
||||||
/*
|
/*
|
||||||
* This is a hack to bump the rendering of our styled
|
* This is a hack to bump the rendering of our styled
|
||||||
* `textDecorated` to _after_ whatever processing is happening
|
* `textDecorated` to _after_ whatever processing is happening
|
||||||
* within the `PasteInput` library. Without this, the elements in
|
* within the `PasteInput` library. Without this, the elements in
|
||||||
* `textDecorated` are not correctly painted to screen.
|
* `textDecorated` are not correctly painted to screen.
|
||||||
*
|
*
|
||||||
* NB: we tried a `0` timeout as well, but only positive values worked.
|
* NB: we tried a `0` timeout as well, but only positive values worked.
|
||||||
*
|
*
|
||||||
* @see https://github.com/bluesky-social/social-app/issues/929
|
* @see https://github.com/bluesky-social/social-app/issues/929
|
||||||
*/
|
*/
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const newRt = new RichText({text: newText})
|
const newRt = new RichText({text: newText})
|
||||||
newRt.detectFacetsWithoutResolution()
|
newRt.detectFacetsWithoutResolution()
|
||||||
setRichText(newRt)
|
setRichText(newRt)
|
||||||
|
|
||||||
const prefix = getMentionAt(
|
const prefix = getMentionAt(
|
||||||
newText,
|
newText,
|
||||||
textInputSelection.current?.start || 0,
|
textInputSelection.current?.start || 0,
|
||||||
)
|
)
|
||||||
if (prefix) {
|
if (prefix) {
|
||||||
autocompleteView.setActive(true)
|
autocompleteView.setActive(true)
|
||||||
autocompleteView.setPrefix(prefix.value)
|
autocompleteView.setPrefix(prefix.value)
|
||||||
} else {
|
} else {
|
||||||
autocompleteView.setActive(false)
|
autocompleteView.setActive(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const set: Set<string> = new Set()
|
const set: Set<string> = new Set()
|
||||||
|
|
||||||
if (newRt.facets) {
|
if (newRt.facets) {
|
||||||
for (const facet of newRt.facets) {
|
for (const facet of newRt.facets) {
|
||||||
for (const feature of facet.features) {
|
for (const feature of facet.features) {
|
||||||
if (AppBskyRichtextFacet.isLink(feature)) {
|
if (AppBskyRichtextFacet.isLink(feature)) {
|
||||||
if (isUriImage(feature.uri)) {
|
if (isUriImage(feature.uri)) {
|
||||||
const res = await downloadAndResize({
|
const res = await downloadAndResize({
|
||||||
uri: feature.uri,
|
uri: feature.uri,
|
||||||
width: POST_IMG_MAX.width,
|
width: POST_IMG_MAX.width,
|
||||||
height: POST_IMG_MAX.height,
|
height: POST_IMG_MAX.height,
|
||||||
mode: 'contain',
|
mode: 'contain',
|
||||||
maxSize: POST_IMG_MAX.size,
|
maxSize: POST_IMG_MAX.size,
|
||||||
timeout: 15e3,
|
timeout: 15e3,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res !== undefined) {
|
if (res !== undefined) {
|
||||||
onPhotoPasted(res.path)
|
onPhotoPasted(res.path)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
set.add(feature.uri)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
set.add(feature.uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isEqual(set, suggestedLinks)) {
|
|
||||||
onSuggestedLinksChanged(set)
|
|
||||||
}
|
|
||||||
}, 1)
|
|
||||||
},
|
|
||||||
[
|
|
||||||
setRichText,
|
|
||||||
autocompleteView,
|
|
||||||
suggestedLinks,
|
|
||||||
onSuggestedLinksChanged,
|
|
||||||
onPhotoPasted,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
const onPaste = useCallback(
|
|
||||||
async (err: string | undefined, files: PastedFile[]) => {
|
|
||||||
if (err) {
|
|
||||||
return onError(cleanError(err))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const uris = files.map(f => f.uri)
|
if (!isEqual(set, suggestedLinks)) {
|
||||||
const uri = uris.find(isUriImage)
|
onSuggestedLinksChanged(set)
|
||||||
|
|
||||||
if (uri) {
|
|
||||||
onPhotoPasted(uri)
|
|
||||||
}
|
}
|
||||||
},
|
}, 1)
|
||||||
[onError, onPhotoPasted],
|
},
|
||||||
)
|
[
|
||||||
|
setRichText,
|
||||||
|
autocompleteView,
|
||||||
|
suggestedLinks,
|
||||||
|
onSuggestedLinksChanged,
|
||||||
|
onPhotoPasted,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
const onSelectionChange = useCallback(
|
const onPaste = useCallback(
|
||||||
(evt: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
|
async (err: string | undefined, files: PastedFile[]) => {
|
||||||
// NOTE we track the input selection using a ref to avoid excessive renders -prf
|
if (err) {
|
||||||
textInputSelection.current = evt.nativeEvent.selection
|
return onError(cleanError(err))
|
||||||
},
|
}
|
||||||
[textInputSelection],
|
|
||||||
)
|
|
||||||
|
|
||||||
const onSelectAutocompleteItem = useCallback(
|
const uris = files.map(f => f.uri)
|
||||||
(item: string) => {
|
const uri = uris.find(isUriImage)
|
||||||
onChangeText(
|
|
||||||
insertMentionAt(
|
|
||||||
richtext.text,
|
|
||||||
textInputSelection.current?.start || 0,
|
|
||||||
item,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
autocompleteView.setActive(false)
|
|
||||||
},
|
|
||||||
[onChangeText, richtext, autocompleteView],
|
|
||||||
)
|
|
||||||
|
|
||||||
const textDecorated = useMemo(() => {
|
if (uri) {
|
||||||
let i = 0
|
onPhotoPasted(uri)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onError, onPhotoPasted],
|
||||||
|
)
|
||||||
|
|
||||||
return Array.from(richtext.segments()).map(segment => (
|
const onSelectionChange = useCallback(
|
||||||
<Text
|
(evt: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
|
||||||
key={i++}
|
// NOTE we track the input selection using a ref to avoid excessive renders -prf
|
||||||
style={[
|
textInputSelection.current = evt.nativeEvent.selection
|
||||||
!segment.facet ? pal.text : pal.link,
|
},
|
||||||
styles.textInputFormatting,
|
[textInputSelection],
|
||||||
]}>
|
)
|
||||||
{segment.text}
|
|
||||||
</Text>
|
|
||||||
))
|
|
||||||
}, [richtext, pal.link, pal.text])
|
|
||||||
|
|
||||||
return (
|
const onSelectAutocompleteItem = useCallback(
|
||||||
<View style={styles.container}>
|
(item: string) => {
|
||||||
<PasteInput
|
onChangeText(
|
||||||
testID="composerTextInput"
|
insertMentionAt(
|
||||||
ref={textInput}
|
richtext.text,
|
||||||
onChangeText={onChangeText}
|
textInputSelection.current?.start || 0,
|
||||||
onPaste={onPaste}
|
item,
|
||||||
onSelectionChange={onSelectionChange}
|
),
|
||||||
placeholder={placeholder}
|
)
|
||||||
placeholderTextColor={pal.colors.textLight}
|
autocompleteView.setActive(false)
|
||||||
keyboardAppearance={theme.colorScheme}
|
},
|
||||||
autoFocus={true}
|
[onChangeText, richtext, autocompleteView],
|
||||||
allowFontScaling
|
)
|
||||||
multiline
|
|
||||||
style={[pal.text, styles.textInput, styles.textInputFormatting]}
|
const textDecorated = useMemo(() => {
|
||||||
{...props}>
|
let i = 0
|
||||||
{textDecorated}
|
|
||||||
</PasteInput>
|
return Array.from(richtext.segments()).map(segment => (
|
||||||
<Autocomplete
|
<Text
|
||||||
view={autocompleteView}
|
key={i++}
|
||||||
onSelect={onSelectAutocompleteItem}
|
style={[
|
||||||
/>
|
!segment.facet ? pal.text : pal.link,
|
||||||
</View>
|
styles.textInputFormatting,
|
||||||
)
|
]}>
|
||||||
},
|
{segment.text}
|
||||||
)
|
</Text>
|
||||||
|
))
|
||||||
|
}, [richtext, pal.link, pal.text])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<PasteInput
|
||||||
|
testID="composerTextInput"
|
||||||
|
ref={textInput}
|
||||||
|
onChangeText={onChangeText}
|
||||||
|
onPaste={onPaste}
|
||||||
|
onSelectionChange={onSelectionChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
placeholderTextColor={pal.colors.textLight}
|
||||||
|
keyboardAppearance={theme.colorScheme}
|
||||||
|
autoFocus={true}
|
||||||
|
allowFontScaling
|
||||||
|
multiline
|
||||||
|
style={[pal.text, styles.textInput, styles.textInputFormatting]}
|
||||||
|
{...props}>
|
||||||
|
{textDecorated}
|
||||||
|
</PasteInput>
|
||||||
|
<Autocomplete
|
||||||
|
view={autocompleteView}
|
||||||
|
onSelect={onSelectAutocompleteItem}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
|
|
@ -37,135 +37,130 @@ interface TextInputProps {
|
||||||
|
|
||||||
export const textInputWebEmitter = new EventEmitter()
|
export const textInputWebEmitter = new EventEmitter()
|
||||||
|
|
||||||
export const TextInput = React.forwardRef(
|
export const TextInput = React.forwardRef(function TextInputImpl(
|
||||||
(
|
{
|
||||||
|
richtext,
|
||||||
|
placeholder,
|
||||||
|
suggestedLinks,
|
||||||
|
autocompleteView,
|
||||||
|
setRichText,
|
||||||
|
onPhotoPasted,
|
||||||
|
onPressPublish,
|
||||||
|
onSuggestedLinksChanged,
|
||||||
|
}: // onError, TODO
|
||||||
|
TextInputProps,
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
|
const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark')
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
textInputWebEmitter.addListener('publish', onPressPublish)
|
||||||
|
return () => {
|
||||||
|
textInputWebEmitter.removeListener('publish', onPressPublish)
|
||||||
|
}
|
||||||
|
}, [onPressPublish])
|
||||||
|
React.useEffect(() => {
|
||||||
|
textInputWebEmitter.addListener('photo-pasted', onPhotoPasted)
|
||||||
|
return () => {
|
||||||
|
textInputWebEmitter.removeListener('photo-pasted', onPhotoPasted)
|
||||||
|
}
|
||||||
|
}, [onPhotoPasted])
|
||||||
|
|
||||||
|
const editor = useEditor(
|
||||||
{
|
{
|
||||||
richtext,
|
extensions: [
|
||||||
placeholder,
|
Document,
|
||||||
suggestedLinks,
|
LinkDecorator,
|
||||||
autocompleteView,
|
Mention.configure({
|
||||||
setRichText,
|
HTMLAttributes: {
|
||||||
onPhotoPasted,
|
class: 'mention',
|
||||||
onPressPublish,
|
|
||||||
onSuggestedLinksChanged,
|
|
||||||
}: // onError, TODO
|
|
||||||
TextInputProps,
|
|
||||||
ref,
|
|
||||||
) => {
|
|
||||||
const modeClass = useColorSchemeStyle(
|
|
||||||
'ProseMirror-light',
|
|
||||||
'ProseMirror-dark',
|
|
||||||
)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
textInputWebEmitter.addListener('publish', onPressPublish)
|
|
||||||
return () => {
|
|
||||||
textInputWebEmitter.removeListener('publish', onPressPublish)
|
|
||||||
}
|
|
||||||
}, [onPressPublish])
|
|
||||||
React.useEffect(() => {
|
|
||||||
textInputWebEmitter.addListener('photo-pasted', onPhotoPasted)
|
|
||||||
return () => {
|
|
||||||
textInputWebEmitter.removeListener('photo-pasted', onPhotoPasted)
|
|
||||||
}
|
|
||||||
}, [onPhotoPasted])
|
|
||||||
|
|
||||||
const editor = useEditor(
|
|
||||||
{
|
|
||||||
extensions: [
|
|
||||||
Document,
|
|
||||||
LinkDecorator,
|
|
||||||
Mention.configure({
|
|
||||||
HTMLAttributes: {
|
|
||||||
class: 'mention',
|
|
||||||
},
|
|
||||||
suggestion: createSuggestion({autocompleteView}),
|
|
||||||
}),
|
|
||||||
Paragraph,
|
|
||||||
Placeholder.configure({
|
|
||||||
placeholder,
|
|
||||||
}),
|
|
||||||
Text,
|
|
||||||
History,
|
|
||||||
Hardbreak,
|
|
||||||
],
|
|
||||||
editorProps: {
|
|
||||||
attributes: {
|
|
||||||
class: modeClass,
|
|
||||||
},
|
|
||||||
handlePaste: (_, event) => {
|
|
||||||
const items = event.clipboardData?.items
|
|
||||||
|
|
||||||
if (items === undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
getImageFromUri(items, (uri: string) => {
|
|
||||||
textInputWebEmitter.emit('photo-pasted', uri)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleKeyDown: (_, event) => {
|
|
||||||
if ((event.metaKey || event.ctrlKey) && event.code === 'Enter') {
|
|
||||||
textInputWebEmitter.emit('publish')
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
suggestion: createSuggestion({autocompleteView}),
|
||||||
|
}),
|
||||||
|
Paragraph,
|
||||||
|
Placeholder.configure({
|
||||||
|
placeholder,
|
||||||
|
}),
|
||||||
|
Text,
|
||||||
|
History,
|
||||||
|
Hardbreak,
|
||||||
|
],
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
class: modeClass,
|
||||||
},
|
},
|
||||||
content: textToEditorJson(richtext.text.toString()),
|
handlePaste: (_, event) => {
|
||||||
autofocus: 'end',
|
const items = event.clipboardData?.items
|
||||||
editable: true,
|
|
||||||
injectCSS: true,
|
|
||||||
onUpdate({editor: editorProp}) {
|
|
||||||
const json = editorProp.getJSON()
|
|
||||||
|
|
||||||
const newRt = new RichText({text: editorJsonToText(json).trim()})
|
if (items === undefined) {
|
||||||
newRt.detectFacetsWithoutResolution()
|
return
|
||||||
setRichText(newRt)
|
}
|
||||||
|
|
||||||
const set: Set<string> = new Set()
|
getImageFromUri(items, (uri: string) => {
|
||||||
|
textInputWebEmitter.emit('photo-pasted', uri)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleKeyDown: (_, event) => {
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.code === 'Enter') {
|
||||||
|
textInputWebEmitter.emit('publish')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
content: textToEditorJson(richtext.text.toString()),
|
||||||
|
autofocus: 'end',
|
||||||
|
editable: true,
|
||||||
|
injectCSS: true,
|
||||||
|
onUpdate({editor: editorProp}) {
|
||||||
|
const json = editorProp.getJSON()
|
||||||
|
|
||||||
if (newRt.facets) {
|
const newRt = new RichText({text: editorJsonToText(json).trim()})
|
||||||
for (const facet of newRt.facets) {
|
newRt.detectFacetsWithoutResolution()
|
||||||
for (const feature of facet.features) {
|
setRichText(newRt)
|
||||||
if (AppBskyRichtextFacet.isLink(feature)) {
|
|
||||||
set.add(feature.uri)
|
const set: Set<string> = new Set()
|
||||||
}
|
|
||||||
|
if (newRt.facets) {
|
||||||
|
for (const facet of newRt.facets) {
|
||||||
|
for (const feature of facet.features) {
|
||||||
|
if (AppBskyRichtextFacet.isLink(feature)) {
|
||||||
|
set.add(feature.uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!isEqual(set, suggestedLinks)) {
|
if (!isEqual(set, suggestedLinks)) {
|
||||||
onSuggestedLinksChanged(set)
|
onSuggestedLinksChanged(set)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
},
|
||||||
[modeClass],
|
},
|
||||||
)
|
[modeClass],
|
||||||
|
)
|
||||||
|
|
||||||
const onEmojiInserted = React.useCallback(
|
const onEmojiInserted = React.useCallback(
|
||||||
(emoji: Emoji) => {
|
(emoji: Emoji) => {
|
||||||
editor?.chain().focus().insertContent(emoji.native).run()
|
editor?.chain().focus().insertContent(emoji.native).run()
|
||||||
},
|
},
|
||||||
[editor],
|
[editor],
|
||||||
)
|
)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted)
|
textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted)
|
||||||
return () => {
|
return () => {
|
||||||
textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted)
|
textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted)
|
||||||
}
|
}
|
||||||
}, [onEmojiInserted])
|
}, [onEmojiInserted])
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
React.useImperativeHandle(ref, () => ({
|
||||||
focus: () => {}, // TODO
|
focus: () => {}, // TODO
|
||||||
blur: () => {}, // TODO
|
blur: () => {}, // TODO
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<EditorContent editor={editor} />
|
<EditorContent editor={editor} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
|
||||||
function editorJsonToText(json: JSONContent): string {
|
function editorJsonToText(json: JSONContent): string {
|
||||||
let text = ''
|
let text = ''
|
||||||
|
|
|
@ -94,7 +94,7 @@ export function createSuggestion({
|
||||||
}
|
}
|
||||||
|
|
||||||
const MentionList = forwardRef<MentionListRef, SuggestionProps>(
|
const MentionList = forwardRef<MentionListRef, SuggestionProps>(
|
||||||
(props: SuggestionProps, ref) => {
|
function MentionListImpl(props: SuggestionProps, ref) {
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {getGraphemeString} = useGrapheme()
|
const {getGraphemeString} = useGrapheme()
|
||||||
|
|
|
@ -24,7 +24,7 @@ interface Props {
|
||||||
testID?: string
|
testID?: string
|
||||||
}
|
}
|
||||||
export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
|
export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
|
||||||
(
|
function PagerImpl(
|
||||||
{
|
{
|
||||||
children,
|
children,
|
||||||
tabBarPosition = 'top',
|
tabBarPosition = 'top',
|
||||||
|
@ -34,7 +34,7 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
|
||||||
testID,
|
testID,
|
||||||
}: React.PropsWithChildren<Props>,
|
}: React.PropsWithChildren<Props>,
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) {
|
||||||
const [selectedPage, setSelectedPage] = React.useState(0)
|
const [selectedPage, setSelectedPage] = React.useState(0)
|
||||||
const pagerView = React.useRef<PagerView>(null)
|
const pagerView = React.useRef<PagerView>(null)
|
||||||
|
|
||||||
|
|
|
@ -14,51 +14,49 @@ interface Props {
|
||||||
renderTabBar: RenderTabBarFn
|
renderTabBar: RenderTabBarFn
|
||||||
onPageSelected?: (index: number) => void
|
onPageSelected?: (index: number) => void
|
||||||
}
|
}
|
||||||
export const Pager = React.forwardRef(
|
export const Pager = React.forwardRef(function PagerImpl(
|
||||||
(
|
{
|
||||||
{
|
children,
|
||||||
children,
|
tabBarPosition = 'top',
|
||||||
tabBarPosition = 'top',
|
initialPage = 0,
|
||||||
initialPage = 0,
|
renderTabBar,
|
||||||
renderTabBar,
|
onPageSelected,
|
||||||
onPageSelected,
|
}: React.PropsWithChildren<Props>,
|
||||||
}: React.PropsWithChildren<Props>,
|
ref,
|
||||||
ref,
|
) {
|
||||||
) => {
|
const [selectedPage, setSelectedPage] = React.useState(initialPage)
|
||||||
const [selectedPage, setSelectedPage] = React.useState(initialPage)
|
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
React.useImperativeHandle(ref, () => ({
|
||||||
setPage: (index: number) => setSelectedPage(index),
|
setPage: (index: number) => setSelectedPage(index),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const onTabBarSelect = React.useCallback(
|
const onTabBarSelect = React.useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
setSelectedPage(index)
|
setSelectedPage(index)
|
||||||
onPageSelected?.(index)
|
onPageSelected?.(index)
|
||||||
},
|
},
|
||||||
[setSelectedPage, onPageSelected],
|
[setSelectedPage, onPageSelected],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
{tabBarPosition === 'top' &&
|
{tabBarPosition === 'top' &&
|
||||||
renderTabBar({
|
renderTabBar({
|
||||||
selectedPage,
|
selectedPage,
|
||||||
onSelect: onTabBarSelect,
|
onSelect: onTabBarSelect,
|
||||||
})}
|
})}
|
||||||
{React.Children.map(children, (child, i) => (
|
{React.Children.map(children, (child, i) => (
|
||||||
<View
|
<View
|
||||||
style={selectedPage === i ? undefined : s.hidden}
|
style={selectedPage === i ? undefined : s.hidden}
|
||||||
key={`page-${i}`}>
|
key={`page-${i}`}>
|
||||||
{child}
|
{child}
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
{tabBarPosition === 'bottom' &&
|
{tabBarPosition === 'bottom' &&
|
||||||
renderTabBar({
|
renderTabBar({
|
||||||
selectedPage,
|
selectedPage,
|
||||||
onSelect: onTabBarSelect,
|
onSelect: onTabBarSelect,
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
|
|
@ -39,179 +39,177 @@ interface ProfileView {
|
||||||
type Item = Heading | RefWrapper | SuggestWrapper | ProfileView
|
type Item = Heading | RefWrapper | SuggestWrapper | ProfileView
|
||||||
|
|
||||||
export const Suggestions = observer(
|
export const Suggestions = observer(
|
||||||
forwardRef(
|
forwardRef(function SuggestionsImpl(
|
||||||
(
|
{
|
||||||
{
|
foafs,
|
||||||
foafs,
|
suggestedActors,
|
||||||
suggestedActors,
|
}: {
|
||||||
}: {
|
foafs: FoafsModel
|
||||||
foafs: FoafsModel
|
suggestedActors: SuggestedActorsModel
|
||||||
suggestedActors: SuggestedActorsModel
|
|
||||||
},
|
|
||||||
flatListRef: ForwardedRef<FlatList>,
|
|
||||||
) => {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const [refreshing, setRefreshing] = React.useState(false)
|
|
||||||
const data = React.useMemo(() => {
|
|
||||||
let items: Item[] = []
|
|
||||||
|
|
||||||
if (foafs.popular.length > 0) {
|
|
||||||
items = items
|
|
||||||
.concat([
|
|
||||||
{
|
|
||||||
_reactKey: '__popular_heading__',
|
|
||||||
type: 'heading',
|
|
||||||
title: 'In Your Network',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.concat(
|
|
||||||
foafs.popular.map(ref => ({
|
|
||||||
_reactKey: `popular-${ref.did}`,
|
|
||||||
type: 'ref',
|
|
||||||
ref,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (suggestedActors.hasContent) {
|
|
||||||
items = items
|
|
||||||
.concat([
|
|
||||||
{
|
|
||||||
_reactKey: '__suggested_heading__',
|
|
||||||
type: 'heading',
|
|
||||||
title: 'Suggested Follows',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.concat(
|
|
||||||
suggestedActors.suggestions.map(suggested => ({
|
|
||||||
_reactKey: `suggested-${suggested.did}`,
|
|
||||||
type: 'suggested',
|
|
||||||
suggested,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
for (const source of foafs.sources) {
|
|
||||||
const item = foafs.foafs.get(source)
|
|
||||||
if (!item || item.follows.length === 0) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
items = items
|
|
||||||
.concat([
|
|
||||||
{
|
|
||||||
_reactKey: `__${item.did}_heading__`,
|
|
||||||
type: 'heading',
|
|
||||||
title: `Followed by ${sanitizeDisplayName(
|
|
||||||
item.displayName || sanitizeHandle(item.handle),
|
|
||||||
)}`,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.concat(
|
|
||||||
item.follows.slice(0, 10).map(view => ({
|
|
||||||
_reactKey: `${item.did}-${view.did}`,
|
|
||||||
type: 'profile-view',
|
|
||||||
view,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
|
||||||
}, [
|
|
||||||
foafs.popular,
|
|
||||||
suggestedActors.hasContent,
|
|
||||||
suggestedActors.suggestions,
|
|
||||||
foafs.sources,
|
|
||||||
foafs.foafs,
|
|
||||||
])
|
|
||||||
|
|
||||||
const onRefresh = React.useCallback(async () => {
|
|
||||||
setRefreshing(true)
|
|
||||||
try {
|
|
||||||
await foafs.fetch()
|
|
||||||
} finally {
|
|
||||||
setRefreshing(false)
|
|
||||||
}
|
|
||||||
}, [foafs, setRefreshing])
|
|
||||||
|
|
||||||
const renderItem = React.useCallback(
|
|
||||||
({item}: {item: Item}) => {
|
|
||||||
if (item.type === 'heading') {
|
|
||||||
return (
|
|
||||||
<Text type="title" style={[styles.heading, pal.text]}>
|
|
||||||
{item.title}
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (item.type === 'ref') {
|
|
||||||
return (
|
|
||||||
<View style={[styles.card, pal.view, pal.border]}>
|
|
||||||
<ProfileCardWithFollowBtn
|
|
||||||
key={item.ref.did}
|
|
||||||
profile={item.ref}
|
|
||||||
noBg
|
|
||||||
noBorder
|
|
||||||
followers={
|
|
||||||
item.ref.followers
|
|
||||||
? (item.ref.followers as AppBskyActorDefs.ProfileView[])
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (item.type === 'profile-view') {
|
|
||||||
return (
|
|
||||||
<View style={[styles.card, pal.view, pal.border]}>
|
|
||||||
<ProfileCardWithFollowBtn
|
|
||||||
key={item.view.did}
|
|
||||||
profile={item.view}
|
|
||||||
noBg
|
|
||||||
noBorder
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (item.type === 'suggested') {
|
|
||||||
return (
|
|
||||||
<View style={[styles.card, pal.view, pal.border]}>
|
|
||||||
<ProfileCardWithFollowBtn
|
|
||||||
key={item.suggested.did}
|
|
||||||
profile={item.suggested}
|
|
||||||
noBg
|
|
||||||
noBorder
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
[pal],
|
|
||||||
)
|
|
||||||
|
|
||||||
if (foafs.isLoading || suggestedActors.isLoading) {
|
|
||||||
return (
|
|
||||||
<CenteredView>
|
|
||||||
<ProfileCardFeedLoadingPlaceholder />
|
|
||||||
</CenteredView>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<FlatList
|
|
||||||
ref={flatListRef}
|
|
||||||
data={data}
|
|
||||||
keyExtractor={item => item._reactKey}
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={refreshing}
|
|
||||||
onRefresh={onRefresh}
|
|
||||||
tintColor={pal.colors.text}
|
|
||||||
titleColor={pal.colors.text}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
renderItem={renderItem}
|
|
||||||
initialNumToRender={15}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
),
|
flatListRef: ForwardedRef<FlatList>,
|
||||||
|
) {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const [refreshing, setRefreshing] = React.useState(false)
|
||||||
|
const data = React.useMemo(() => {
|
||||||
|
let items: Item[] = []
|
||||||
|
|
||||||
|
if (foafs.popular.length > 0) {
|
||||||
|
items = items
|
||||||
|
.concat([
|
||||||
|
{
|
||||||
|
_reactKey: '__popular_heading__',
|
||||||
|
type: 'heading',
|
||||||
|
title: 'In Your Network',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.concat(
|
||||||
|
foafs.popular.map(ref => ({
|
||||||
|
_reactKey: `popular-${ref.did}`,
|
||||||
|
type: 'ref',
|
||||||
|
ref,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (suggestedActors.hasContent) {
|
||||||
|
items = items
|
||||||
|
.concat([
|
||||||
|
{
|
||||||
|
_reactKey: '__suggested_heading__',
|
||||||
|
type: 'heading',
|
||||||
|
title: 'Suggested Follows',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.concat(
|
||||||
|
suggestedActors.suggestions.map(suggested => ({
|
||||||
|
_reactKey: `suggested-${suggested.did}`,
|
||||||
|
type: 'suggested',
|
||||||
|
suggested,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for (const source of foafs.sources) {
|
||||||
|
const item = foafs.foafs.get(source)
|
||||||
|
if (!item || item.follows.length === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
items = items
|
||||||
|
.concat([
|
||||||
|
{
|
||||||
|
_reactKey: `__${item.did}_heading__`,
|
||||||
|
type: 'heading',
|
||||||
|
title: `Followed by ${sanitizeDisplayName(
|
||||||
|
item.displayName || sanitizeHandle(item.handle),
|
||||||
|
)}`,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.concat(
|
||||||
|
item.follows.slice(0, 10).map(view => ({
|
||||||
|
_reactKey: `${item.did}-${view.did}`,
|
||||||
|
type: 'profile-view',
|
||||||
|
view,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}, [
|
||||||
|
foafs.popular,
|
||||||
|
suggestedActors.hasContent,
|
||||||
|
suggestedActors.suggestions,
|
||||||
|
foafs.sources,
|
||||||
|
foafs.foafs,
|
||||||
|
])
|
||||||
|
|
||||||
|
const onRefresh = React.useCallback(async () => {
|
||||||
|
setRefreshing(true)
|
||||||
|
try {
|
||||||
|
await foafs.fetch()
|
||||||
|
} finally {
|
||||||
|
setRefreshing(false)
|
||||||
|
}
|
||||||
|
}, [foafs, setRefreshing])
|
||||||
|
|
||||||
|
const renderItem = React.useCallback(
|
||||||
|
({item}: {item: Item}) => {
|
||||||
|
if (item.type === 'heading') {
|
||||||
|
return (
|
||||||
|
<Text type="title" style={[styles.heading, pal.text]}>
|
||||||
|
{item.title}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (item.type === 'ref') {
|
||||||
|
return (
|
||||||
|
<View style={[styles.card, pal.view, pal.border]}>
|
||||||
|
<ProfileCardWithFollowBtn
|
||||||
|
key={item.ref.did}
|
||||||
|
profile={item.ref}
|
||||||
|
noBg
|
||||||
|
noBorder
|
||||||
|
followers={
|
||||||
|
item.ref.followers
|
||||||
|
? (item.ref.followers as AppBskyActorDefs.ProfileView[])
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (item.type === 'profile-view') {
|
||||||
|
return (
|
||||||
|
<View style={[styles.card, pal.view, pal.border]}>
|
||||||
|
<ProfileCardWithFollowBtn
|
||||||
|
key={item.view.did}
|
||||||
|
profile={item.view}
|
||||||
|
noBg
|
||||||
|
noBorder
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (item.type === 'suggested') {
|
||||||
|
return (
|
||||||
|
<View style={[styles.card, pal.view, pal.border]}>
|
||||||
|
<ProfileCardWithFollowBtn
|
||||||
|
key={item.suggested.did}
|
||||||
|
profile={item.suggested}
|
||||||
|
noBg
|
||||||
|
noBorder
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
[pal],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (foafs.isLoading || suggestedActors.isLoading) {
|
||||||
|
return (
|
||||||
|
<CenteredView>
|
||||||
|
<ProfileCardFeedLoadingPlaceholder />
|
||||||
|
</CenteredView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
ref={flatListRef}
|
||||||
|
data={data}
|
||||||
|
keyExtractor={item => item._reactKey}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
tintColor={pal.colors.text}
|
||||||
|
titleColor={pal.colors.text}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
renderItem={renderItem}
|
||||||
|
initialNumToRender={15}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
|
|
@ -12,34 +12,32 @@ interface PressableWithHover extends PressableProps {
|
||||||
hoverStyle: StyleProp<ViewStyle>
|
hoverStyle: StyleProp<ViewStyle>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PressableWithHover = forwardRef(
|
export const PressableWithHover = forwardRef(function PressableWithHoverImpl(
|
||||||
(
|
{
|
||||||
{
|
children,
|
||||||
children,
|
style,
|
||||||
style,
|
hoverStyle,
|
||||||
hoverStyle,
|
...props
|
||||||
...props
|
}: PropsWithChildren<PressableWithHover>,
|
||||||
}: PropsWithChildren<PressableWithHover>,
|
ref: Ref<any>,
|
||||||
ref: Ref<any>,
|
) {
|
||||||
) => {
|
const [isHovering, setIsHovering] = useState(false)
|
||||||
const [isHovering, setIsHovering] = useState(false)
|
|
||||||
|
|
||||||
const onHoverIn = useCallback(() => setIsHovering(true), [setIsHovering])
|
const onHoverIn = useCallback(() => setIsHovering(true), [setIsHovering])
|
||||||
const onHoverOut = useCallback(() => setIsHovering(false), [setIsHovering])
|
const onHoverOut = useCallback(() => setIsHovering(false), [setIsHovering])
|
||||||
style =
|
style =
|
||||||
typeof style !== 'function' && isHovering
|
typeof style !== 'function' && isHovering
|
||||||
? addStyle(style, hoverStyle)
|
? addStyle(style, hoverStyle)
|
||||||
: style
|
: style
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
{...props}
|
{...props}
|
||||||
style={style}
|
style={style}
|
||||||
onHoverIn={onHoverIn}
|
onHoverIn={onHoverIn}
|
||||||
onHoverOut={onHoverOut}
|
onHoverOut={onHoverOut}
|
||||||
ref={ref}>
|
ref={ref}>
|
||||||
{children}
|
{children}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
|
|
@ -42,100 +42,98 @@ export const ViewSelector = React.forwardRef<
|
||||||
onRefresh?: () => void
|
onRefresh?: () => void
|
||||||
onEndReached?: (info: {distanceFromEnd: number}) => void
|
onEndReached?: (info: {distanceFromEnd: number}) => void
|
||||||
}
|
}
|
||||||
>(
|
>(function ViewSelectorImpl(
|
||||||
(
|
{
|
||||||
{
|
sections,
|
||||||
sections,
|
items,
|
||||||
items,
|
refreshing,
|
||||||
refreshing,
|
renderHeader,
|
||||||
renderHeader,
|
renderItem,
|
||||||
renderItem,
|
ListFooterComponent,
|
||||||
ListFooterComponent,
|
onSelectView,
|
||||||
onSelectView,
|
onScroll,
|
||||||
onScroll,
|
onRefresh,
|
||||||
onRefresh,
|
onEndReached,
|
||||||
onEndReached,
|
|
||||||
},
|
|
||||||
ref,
|
|
||||||
) => {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState<number>(0)
|
|
||||||
const flatListRef = React.useRef<FlatList>(null)
|
|
||||||
|
|
||||||
// events
|
|
||||||
// =
|
|
||||||
|
|
||||||
const keyExtractor = React.useCallback((item: any) => item._reactKey, [])
|
|
||||||
|
|
||||||
const onPressSelection = React.useCallback(
|
|
||||||
(index: number) => setSelectedIndex(clamp(index, 0, sections.length)),
|
|
||||||
[setSelectedIndex, sections],
|
|
||||||
)
|
|
||||||
useEffect(() => {
|
|
||||||
onSelectView?.(selectedIndex)
|
|
||||||
}, [selectedIndex, onSelectView])
|
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
|
||||||
scrollToTop: () => {
|
|
||||||
flatListRef.current?.scrollToOffset({offset: 0})
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
// rendering
|
|
||||||
// =
|
|
||||||
|
|
||||||
const renderItemInternal = React.useCallback(
|
|
||||||
({item}: {item: any}) => {
|
|
||||||
if (item === HEADER_ITEM) {
|
|
||||||
if (renderHeader) {
|
|
||||||
return renderHeader()
|
|
||||||
}
|
|
||||||
return <View />
|
|
||||||
} else if (item === SELECTOR_ITEM) {
|
|
||||||
return (
|
|
||||||
<Selector
|
|
||||||
items={sections}
|
|
||||||
selectedIndex={selectedIndex}
|
|
||||||
onSelect={onPressSelection}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return renderItem(item)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[sections, selectedIndex, onPressSelection, renderHeader, renderItem],
|
|
||||||
)
|
|
||||||
|
|
||||||
const data = React.useMemo(
|
|
||||||
() => [HEADER_ITEM, SELECTOR_ITEM, ...items],
|
|
||||||
[items],
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<FlatList
|
|
||||||
ref={flatListRef}
|
|
||||||
data={data}
|
|
||||||
keyExtractor={keyExtractor}
|
|
||||||
renderItem={renderItemInternal}
|
|
||||||
ListFooterComponent={ListFooterComponent}
|
|
||||||
// NOTE sticky header disabled on android due to major performance issues -prf
|
|
||||||
stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES}
|
|
||||||
onScroll={onScroll}
|
|
||||||
onEndReached={onEndReached}
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={refreshing!}
|
|
||||||
onRefresh={onRefresh}
|
|
||||||
tintColor={pal.colors.text}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onEndReachedThreshold={0.6}
|
|
||||||
contentContainerStyle={s.contentContainer}
|
|
||||||
removeClippedSubviews={true}
|
|
||||||
scrollIndicatorInsets={{right: 1}} // fixes a bug where the scroll indicator is on the middle of the screen https://github.com/bluesky-social/social-app/pull/464
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
ref,
|
||||||
|
) {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState<number>(0)
|
||||||
|
const flatListRef = React.useRef<FlatList>(null)
|
||||||
|
|
||||||
|
// events
|
||||||
|
// =
|
||||||
|
|
||||||
|
const keyExtractor = React.useCallback((item: any) => item._reactKey, [])
|
||||||
|
|
||||||
|
const onPressSelection = React.useCallback(
|
||||||
|
(index: number) => setSelectedIndex(clamp(index, 0, sections.length)),
|
||||||
|
[setSelectedIndex, sections],
|
||||||
|
)
|
||||||
|
useEffect(() => {
|
||||||
|
onSelectView?.(selectedIndex)
|
||||||
|
}, [selectedIndex, onSelectView])
|
||||||
|
|
||||||
|
React.useImperativeHandle(ref, () => ({
|
||||||
|
scrollToTop: () => {
|
||||||
|
flatListRef.current?.scrollToOffset({offset: 0})
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// rendering
|
||||||
|
// =
|
||||||
|
|
||||||
|
const renderItemInternal = React.useCallback(
|
||||||
|
({item}: {item: any}) => {
|
||||||
|
if (item === HEADER_ITEM) {
|
||||||
|
if (renderHeader) {
|
||||||
|
return renderHeader()
|
||||||
|
}
|
||||||
|
return <View />
|
||||||
|
} else if (item === SELECTOR_ITEM) {
|
||||||
|
return (
|
||||||
|
<Selector
|
||||||
|
items={sections}
|
||||||
|
selectedIndex={selectedIndex}
|
||||||
|
onSelect={onPressSelection}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return renderItem(item)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[sections, selectedIndex, onPressSelection, renderHeader, renderItem],
|
||||||
|
)
|
||||||
|
|
||||||
|
const data = React.useMemo(
|
||||||
|
() => [HEADER_ITEM, SELECTOR_ITEM, ...items],
|
||||||
|
[items],
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
ref={flatListRef}
|
||||||
|
data={data}
|
||||||
|
keyExtractor={keyExtractor}
|
||||||
|
renderItem={renderItemInternal}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
// NOTE sticky header disabled on android due to major performance issues -prf
|
||||||
|
stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES}
|
||||||
|
onScroll={onScroll}
|
||||||
|
onEndReached={onEndReached}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing!}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
tintColor={pal.colors.text}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onEndReachedThreshold={0.6}
|
||||||
|
contentContainerStyle={s.contentContainer}
|
||||||
|
removeClippedSubviews={true}
|
||||||
|
scrollIndicatorInsets={{right: 1}} // fixes a bug where the scroll indicator is on the middle of the screen https://github.com/bluesky-social/social-app/pull/464
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
export function Selector({
|
export function Selector({
|
||||||
selectedIndex,
|
selectedIndex,
|
||||||
|
|
|
@ -38,7 +38,7 @@ export function CenteredView({
|
||||||
return <View style={style} {...props} />
|
return <View style={style} {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FlatList = React.forwardRef(function <ItemT>(
|
export const FlatList = React.forwardRef(function FlatListImpl<ItemT>(
|
||||||
{
|
{
|
||||||
contentContainerStyle,
|
contentContainerStyle,
|
||||||
style,
|
style,
|
||||||
|
@ -99,7 +99,7 @@ export const FlatList = React.forwardRef(function <ItemT>(
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ScrollView = React.forwardRef(function (
|
export const ScrollView = React.forwardRef(function ScrollViewImpl(
|
||||||
{contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>,
|
{contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>,
|
||||||
ref: React.Ref<RNScrollView>,
|
ref: React.Ref<RNScrollView>,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -26,7 +26,7 @@ type PropsInner = TriggerableAnimatedProps & {
|
||||||
export const TriggerableAnimated = React.forwardRef<
|
export const TriggerableAnimated = React.forwardRef<
|
||||||
TriggerableAnimatedRef,
|
TriggerableAnimatedRef,
|
||||||
TriggerableAnimatedProps
|
TriggerableAnimatedProps
|
||||||
>(({children, ...props}, ref) => {
|
>(function TriggerableAnimatedImpl({children, ...props}, ref) {
|
||||||
const [anim, setAnim] = React.useState<TriggeredAnimation | undefined>(
|
const [anim, setAnim] = React.useState<TriggeredAnimation | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,13 +2,12 @@ import React from 'react'
|
||||||
import {isNative} from 'platform/detection'
|
import {isNative} from 'platform/detection'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
|
|
||||||
export const withBreakpoints =
|
export const withBreakpoints = <P extends object>(
|
||||||
<P extends object>(
|
Mobile: React.ComponentType<P>,
|
||||||
Mobile: React.ComponentType<P>,
|
Tablet: React.ComponentType<P>,
|
||||||
Tablet: React.ComponentType<P>,
|
Desktop: React.ComponentType<P>,
|
||||||
Desktop: React.ComponentType<P>,
|
): React.FC<P> =>
|
||||||
): React.FC<P> =>
|
function WithBreakpoints(props: P) {
|
||||||
(props: P) => {
|
|
||||||
const {isMobile, isTabletOrMobile} = useWebMediaQueries()
|
const {isMobile, isTabletOrMobile} = useWebMediaQueries()
|
||||||
|
|
||||||
if (isMobile || isNative) {
|
if (isMobile || isNative) {
|
||||||
|
|
|
@ -9906,7 +9906,7 @@ eslint-plugin-react-native@^4.0.0:
|
||||||
"@babel/traverse" "^7.7.4"
|
"@babel/traverse" "^7.7.4"
|
||||||
eslint-plugin-react-native-globals "^0.1.1"
|
eslint-plugin-react-native-globals "^0.1.1"
|
||||||
|
|
||||||
eslint-plugin-react@^7.27.1, eslint-plugin-react@^7.30.1:
|
eslint-plugin-react@^7.27.1, eslint-plugin-react@^7.30.1, eslint-plugin-react@^7.33.2:
|
||||||
version "7.33.2"
|
version "7.33.2"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz#69ee09443ffc583927eafe86ffebb470ee737608"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz#69ee09443ffc583927eafe86ffebb470ee737608"
|
||||||
integrity sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==
|
integrity sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==
|
||||||
|
|
Loading…
Reference in New Issue