Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

basic implementation of search history (#2597)

Co-authored-by: Ryan Skinner <ryanskinner@gmail.com>

authored by

Paul Frazee
Ryan Skinner
and committed by
GitHub
cda4fe4a abac959d

+131 -6
+131 -6
src/view/screens/Search/Search.tsx
··· 51 51 import {isNative, isWeb} from '#/platform/detection' 52 52 import {listenSoftReset} from '#/state/events' 53 53 import {s} from '#/lib/styles' 54 + import AsyncStorage from '@react-native-async-storage/async-storage' 54 55 55 56 function Loader() { 56 57 const pal = usePalette('default') ··· 464 465 const [inputIsFocused, setInputIsFocused] = React.useState(false) 465 466 const [showAutocompleteResults, setShowAutocompleteResults] = 466 467 React.useState(false) 468 + const [searchHistory, setSearchHistory] = React.useState<string[]>([]) 469 + 470 + React.useEffect(() => { 471 + const loadSearchHistory = async () => { 472 + try { 473 + const history = await AsyncStorage.getItem('searchHistory') 474 + if (history !== null) { 475 + setSearchHistory(JSON.parse(history)) 476 + } 477 + } catch (e: any) { 478 + logger.error('Failed to load search history', e) 479 + } 480 + } 481 + 482 + loadSearchHistory() 483 + }, []) 467 484 468 485 const onPressMenu = React.useCallback(() => { 469 486 track('ViewHeader:MenuButtonClicked') 470 487 setDrawerOpen(true) 471 488 }, [track, setDrawerOpen]) 489 + 472 490 const onPressCancelSearch = React.useCallback(() => { 473 491 scrollToTopWeb() 474 492 textInput.current?.blur() ··· 477 495 if (searchDebounceTimeout.current) 478 496 clearTimeout(searchDebounceTimeout.current) 479 497 }, [textInput]) 498 + 480 499 const onPressClearQuery = React.useCallback(() => { 481 500 scrollToTopWeb() 482 501 setQuery('') 483 502 setShowAutocompleteResults(false) 484 503 }, [setQuery]) 504 + 485 505 const onChangeText = React.useCallback( 486 506 async (text: string) => { 487 507 scrollToTopWeb() 508 + 488 509 setQuery(text) 489 510 490 511 if (text.length > 0) { 491 512 setIsFetching(true) 492 513 setShowAutocompleteResults(true) 493 514 494 - if (searchDebounceTimeout.current) 515 + if (searchDebounceTimeout.current) { 495 516 clearTimeout(searchDebounceTimeout.current) 517 + } 496 518 497 519 searchDebounceTimeout.current = setTimeout(async () => { 498 520 const results = await search({query: text, limit: 30}) ··· 503 525 } 504 526 }, 300) 505 527 } else { 506 - if (searchDebounceTimeout.current) 528 + if (searchDebounceTimeout.current) { 507 529 clearTimeout(searchDebounceTimeout.current) 530 + } 508 531 setSearchResults([]) 509 532 setIsFetching(false) 510 533 setShowAutocompleteResults(false) ··· 512 535 }, 513 536 [setQuery, search, setSearchResults], 514 537 ) 538 + 539 + const updateSearchHistory = React.useCallback( 540 + async (newQuery: string) => { 541 + newQuery = newQuery.trim() 542 + if (newQuery && !searchHistory.includes(newQuery)) { 543 + let newHistory = [newQuery, ...searchHistory] 544 + 545 + if (newHistory.length > 5) { 546 + newHistory = newHistory.slice(0, 5) 547 + } 548 + 549 + setSearchHistory(newHistory) 550 + try { 551 + await AsyncStorage.setItem( 552 + 'searchHistory', 553 + JSON.stringify(newHistory), 554 + ) 555 + } catch (e: any) { 556 + logger.error('Failed to save search history', e) 557 + } 558 + } 559 + }, 560 + [searchHistory, setSearchHistory], 561 + ) 562 + 515 563 const onSubmit = React.useCallback(() => { 516 564 scrollToTopWeb() 517 565 setShowAutocompleteResults(false) 518 - }, [setShowAutocompleteResults]) 566 + updateSearchHistory(query) 567 + }, [query, setShowAutocompleteResults, updateSearchHistory]) 519 568 520 569 const onSoftReset = React.useCallback(() => { 521 570 scrollToTopWeb() ··· 534 583 }, [onSoftReset, setMinimalShellMode]), 535 584 ) 536 585 586 + const handleHistoryItemClick = (item: React.SetStateAction<string>) => { 587 + setQuery(item) 588 + onSubmit() 589 + } 590 + 591 + const handleRemoveHistoryItem = (itemToRemove: string) => { 592 + const updatedHistory = searchHistory.filter(item => item !== itemToRemove) 593 + setSearchHistory(updatedHistory) 594 + AsyncStorage.setItem('searchHistory', JSON.stringify(updatedHistory)).catch( 595 + e => { 596 + logger.error('Failed to update search history', e) 597 + }, 598 + ) 599 + } 600 + 537 601 return ( 538 602 <View style={isWeb ? null : {flex: 1}}> 539 603 <CenteredView ··· 581 645 style={[pal.text, styles.headerSearchInput]} 582 646 keyboardAppearance={theme.colorScheme} 583 647 onFocus={() => setInputIsFocused(true)} 584 - onBlur={() => setInputIsFocused(false)} 648 + onBlur={() => { 649 + // HACK 650 + // give 100ms to not stop click handlers in the search history 651 + // -prf 652 + setTimeout(() => setInputIsFocused(false), 100) 653 + }} 585 654 onChangeText={onChangeText} 586 655 onSubmitEditing={onSubmit} 587 656 autoFocus={false} ··· 623 692 ) : undefined} 624 693 </CenteredView> 625 694 626 - {showAutocompleteResults && moderationOpts ? ( 695 + {showAutocompleteResults ? ( 627 696 <> 628 - {isFetching ? ( 697 + {isFetching || !moderationOpts ? ( 629 698 <Loader /> 630 699 ) : ( 631 700 <ScrollView ··· 664 733 </ScrollView> 665 734 )} 666 735 </> 736 + ) : !query && inputIsFocused ? ( 737 + <CenteredView 738 + sideBorders={isTabletOrDesktop} 739 + // @ts-ignore web only -prf 740 + style={{ 741 + height: isWeb ? '100vh' : undefined, 742 + }}> 743 + <View style={styles.searchHistoryContainer}> 744 + {searchHistory.length > 0 && ( 745 + <View style={styles.searchHistoryContent}> 746 + <Text style={[pal.text, styles.searchHistoryTitle]}> 747 + Recent Searches 748 + </Text> 749 + {searchHistory.map((historyItem, index) => ( 750 + <View key={index} style={styles.historyItemContainer}> 751 + <Pressable 752 + accessibilityRole="button" 753 + onPress={() => handleHistoryItemClick(historyItem)} 754 + style={styles.historyItem}> 755 + <Text style={pal.text}>{historyItem}</Text> 756 + </Pressable> 757 + <Pressable 758 + accessibilityRole="button" 759 + onPress={() => handleRemoveHistoryItem(historyItem)}> 760 + <FontAwesomeIcon 761 + icon="xmark" 762 + size={16} 763 + style={pal.textLight as FontAwesomeIconStyle} 764 + /> 765 + </Pressable> 766 + </View> 767 + ))} 768 + </View> 769 + )} 770 + </View> 771 + </CenteredView> 667 772 ) : ( 668 773 <SearchScreenInner query={query} /> 669 774 )} ··· 724 829 position: isWeb ? 'sticky' : '', 725 830 top: isWeb ? HEADER_HEIGHT : 0, 726 831 zIndex: 1, 832 + }, 833 + searchHistoryContainer: { 834 + width: '100%', 835 + paddingHorizontal: 12, 836 + }, 837 + searchHistoryContent: { 838 + padding: 10, 839 + borderRadius: 8, 840 + }, 841 + searchHistoryTitle: { 842 + fontWeight: 'bold', 843 + }, 844 + historyItem: { 845 + paddingVertical: 8, 846 + }, 847 + historyItemContainer: { 848 + flexDirection: 'row', 849 + justifyContent: 'space-between', 850 + alignItems: 'center', 851 + paddingVertical: 8, 727 852 }, 728 853 })