<commit_before>import * as React from 'react'
import {StyleSheet, View, ScrollView} from 'react-native'
import * as c from '@frogpond/colors'
import {
	updateCourseData,
	loadCourseDataIntoMemory,
} from '../../../redux/parts/courses'
import {areAnyTermsCached} from '../../../lib/course-search'
import type {ReduxState} from '../../../redux'
import {useDispatch, useSelector} from 'react-redux'
import {NoticeView, LoadingView} from '@frogpond/notice'
import {AnimatedSearchBar} from '@frogpond/searchbar'
import {RecentItemsList} from '../components/recents-list'
import {Separator} from '@frogpond/separator'
import {buildFilters} from './lib/build-filters'
import type {FilterComboType} from './lib/format-filter-combo'
import fromPairs from 'lodash/fromPairs'
import {useNavigation} from '@react-navigation/native'

const PROMPT_TEXT =
	'We need to download the courses from the server. This will take a few seconds.'
const NETWORK_WARNING =
	'(Please make sure that you are connected to the Internet before downloading the courses).'

type ReduxStateProps = {
	courseDataState: string
	recentFilters: FilterComboType[]
	recentSearches: string[]
}

type ReduxDispatchProps = {
	updateCourseData: () => Promise<any>
	loadCourseDataIntoMemory: () => Promise<any>
}

type Props = ReduxStateProps & ReduxDispatchProps

type Mode = 'loading' | 'pending' | 'ready'

const CourseSearchView = (props: Props): JSX.Element => {
	let [mode, setMode] = React.useState<Mode>('pending')
	let [isSearchbarActive, setIsSearchbarActive] = React.useState(false)
	let [typedQuery, setTypedQuery] = React.useState('')

	let navigation = useNavigation()

	let loadData = React.useCallback(
		async ({userInitiated = true}: {userInitiated?: boolean} = {}) => {
			let hasCache = await areAnyTermsCached()

			if (!hasCache && !userInitiated) {
				// if no terms are cached, and the user didn't push the button,
				// then don't download anything.
				setMode('pending')
				return
			}

			setMode('loading')

			// If the data has not been loaded into Redux State:
			if (props.courseDataState !== 'ready') {
				// 1. load the cached courses
				await props.loadCourseDataIntoMemory()

				// 2. if any courses are cached, hide the spinner
				if (hasCache) {
					setMode('ready')
				}

				// 3. either way, start updating courses in the background
				await props.updateCourseData()
			} else {
				// If the course data is already in Redux State, check for update
				await props.updateCourseData()
			}

			// 4. when everything is done, make sure the spinner is hidden
			setMode('ready')
		},
		[props],
	)

	React.useEffect(() => {
		loadData({userInitiated: false})
	}, [loadData])

	let handleSearchSubmit = () => {
		navigation.navigate('CourseSearchResults', {
			initialQuery: typedQuery,
		})
		setIsSearchbarActive(false)
		setTypedQuery('')
	}

	let handleSearchCancel = () => {
		setIsSearchbarActive(false)
		setTypedQuery('')
	}

	let handleSearchChange = (value: string) => {
		setTypedQuery(value)
	}

	let handleSearchFocus = () => {
		setIsSearchbarActive(true)
	}

	let browseAll = () => {
		navigation.navigate('CourseSearchResults', {initialQuery: ''})
	}

	let onRecentSearchPress = (text: string) => {
		navigation.navigate('CourseSearchResults', {initialQuery: text})
	}

	let onRecentFilterPress = async (text: string) => {
		let {recentFilters} = props
		let selectedFilterCombo = recentFilters.find((f) => f.description === text)

		let freshFilters = await buildFilters()
		let selectedFilters = freshFilters
		if (selectedFilterCombo) {
			let filterLookup = fromPairs(
				selectedFilterCombo.filters.map((f) => [f.key, f]),
			)
			selectedFilters = freshFilters.map((f) => filterLookup[f.key] || f)
		}

		navigation.navigate('CourseSearchResults', {
			initialFilters: selectedFilters,
		})
	}

	if (mode === 'loading') {
		return <LoadingView text="Loading Course Data…" />
	}

	if (props.courseDataState === 'not-loaded') {
		let msg = PROMPT_TEXT + '\n\n' + NETWORK_WARNING

		return (
			<NoticeView
				buttonText="Download"
				header="Almost there…"
				onPress={loadData}
				text={msg}
			/>
		)
	}

	let recentFilterDescriptions = props.recentFilters.map((f) => f.description)

	return (
		<View style={[styles.container, styles.common]}>
			<AnimatedSearchBar
				active={isSearchbarActive}
				onCancel={handleSearchCancel}
				onChange={handleSearchChange}
				onFocus={handleSearchFocus}
				onSubmit={handleSearchSubmit}
				placeholder="Search Class & Lab"
				title="Search Courses"
				value={typedQuery}
			/>

			<Separator />

			<ScrollView
				keyboardDismissMode="interactive"
				style={[styles.common, styles.bottomContainer]}
			>
				<RecentItemsList
					emptyHeader="No recent searches"
					emptyText="Your recent searches will appear here."
					items={props.recentSearches}
					onItemPress={onRecentSearchPress}
					title="Recent"
				/>
				<RecentItemsList
					actionLabel="Browse All"
					emptyHeader="No recent filter combinations"
					emptyText="Your recent filter combinations will appear here."
					items={recentFilterDescriptions}
					onAction={browseAll}
					onItemPress={onRecentFilterPress}
					title="Browse"
				/>
			</ScrollView>
		</View>
	)
}
<commit_msg>replace animated searchbar with native searchbar in recent course search<commit_after>import * as React from 'react'
import {StyleSheet, View, ScrollView} from 'react-native'
import * as c from '@frogpond/colors'
import {
	updateCourseData,
	loadCourseDataIntoMemory,
} from '../../../redux/parts/courses'
import {areAnyTermsCached} from '../../../lib/course-search'
import type {ReduxState} from '../../../redux'
import {useDispatch, useSelector} from 'react-redux'
import {NoticeView, LoadingView} from '@frogpond/notice'
import {RecentItemsList} from '../components/recents-list'
import {buildFilters} from './lib/build-filters'
import type {FilterComboType} from './lib/format-filter-combo'
import fromPairs from 'lodash/fromPairs'
import {useNavigation} from '@react-navigation/native'
import {ChangeTextEvent} from '../../../navigation/types'
import {debounce} from 'lodash'

const PROMPT_TEXT =
	'We need to download the courses from the server. This will take a few seconds.'
const NETWORK_WARNING =
	'(Please make sure that you are connected to the Internet before downloading the courses).'

type ReduxStateProps = {
	courseDataState: string
	recentFilters: FilterComboType[]
	recentSearches: string[]
}

type ReduxDispatchProps = {
	updateCourseData: () => Promise<any>
	loadCourseDataIntoMemory: () => Promise<any>
}

type Props = ReduxStateProps & ReduxDispatchProps

type Mode = 'loading' | 'pending' | 'ready'

let _debounce = debounce((query: string, callback: () => void) => {
	if (query.length >= 2) {
		callback()
	}
}, 1500)

const CourseSearchView = (props: Props): JSX.Element => {
	let [mode, setMode] = React.useState<Mode>('pending')
	let [typedQuery, setTypedQuery] = React.useState('')

	let navigation = useNavigation()

	React.useLayoutEffect(() => {
		/**
		 * SIS top-level recent searches tab has this setup.
		 *
		 * When we are one-level deep in a tab navigator, attaching a search bar to the
		 * view will work fine. See: Recent course search --> Search results
		 *
		 * When we are top-level inside a tab navigator, attaching a search bar to the
		 * view will not work. See: Tab.Navigator > Tab.Screen --> Course search
		 *
		 * The `getParent` workaround is provided in the react navigation docs.
		 * https://reactnavigation.org/docs/navigation-prop#getparent
		 *
		 * getParent returns the navigation prop from the parent navigator that the
		 * current navigator is nested in. If you have a stack navigator and a tab
		 * navigator nested inside the stack, then you can use getParent inside a screen
		 * of the tab navigator to get the navigation prop passed from the stack
		 * navigator.
		 */
		navigation.getParent()?.setOptions({
			headerSearchBarOptions: {
				barTintColor: c.white,
				onChangeText: (event: ChangeTextEvent) => {
					setTypedQuery(event.nativeEvent.text)
				},
			},
		})
	}, [navigation, typedQuery])

	let showSearchResult = React.useCallback(
		(query: string) => {
			navigation.navigate('CourseSearchResults', {initialQuery: query})
		},
		[navigation],
	)

	React.useEffect(() => {
		_debounce(typedQuery, () => {
			showSearchResult(typedQuery)
		})
	}, [showSearchResult, typedQuery])

	let loadData = React.useCallback(
		async ({userInitiated = true}: {userInitiated?: boolean} = {}) => {
			let hasCache = await areAnyTermsCached()

			if (!hasCache && !userInitiated) {
				// if no terms are cached, and the user didn't push the button,
				// then don't download anything.
				setMode('pending')
				return
			}

			setMode('loading')

			// If the data has not been loaded into Redux State:
			if (props.courseDataState !== 'ready') {
				// 1. load the cached courses
				await props.loadCourseDataIntoMemory()

				// 2. if any courses are cached, hide the spinner
				if (hasCache) {
					setMode('ready')
				}

				// 3. either way, start updating courses in the background
				await props.updateCourseData()
			} else {
				// If the course data is already in Redux State, check for update
				await props.updateCourseData()
			}

			// 4. when everything is done, make sure the spinner is hidden
			setMode('ready')
		},
		[props],
	)

	React.useEffect(() => {
		loadData({userInitiated: false})
	}, [loadData])

	let browseAll = () => {
		navigation.navigate('CourseSearchResults', {initialQuery: ''})
	}

	let onRecentFilterPress = async (text: string) => {
		let {recentFilters} = props
		let selectedFilterCombo = recentFilters.find((f) => f.description === text)

		let freshFilters = await buildFilters()
		let selectedFilters = freshFilters
		if (selectedFilterCombo) {
			let filterLookup = fromPairs(
				selectedFilterCombo.filters.map((f) => [f.key, f]),
			)
			selectedFilters = freshFilters.map((f) => filterLookup[f.key] || f)
		}

		navigation.navigate('CourseSearchResults', {
			initialFilters: selectedFilters,
		})
	}

	if (mode === 'loading') {
		return <LoadingView text="Loading Course Data…" />
	}

	if (props.courseDataState === 'not-loaded') {
		let msg = PROMPT_TEXT + '\n\n' + NETWORK_WARNING

		return (
			<NoticeView
				buttonText="Download"
				header="Almost there…"
				onPress={loadData}
				text={msg}
			/>
		)
	}

	let recentFilterDescriptions = props.recentFilters.map((f) => f.description)

	return (
		<View style={[styles.container, styles.common]}>
			<ScrollView
				// needed for handling native searchbar alignment
				contentInsetAdjustmentBehavior="automatic"
				keyboardDismissMode="interactive"
				style={[styles.common, styles.bottomContainer]}
			>
				<RecentItemsList
					emptyHeader="No recent searches"
					emptyText="Your recent searches will appear here."
					items={props.recentSearches}
					onItemPress={showSearchResult}
					title="Recent"
				/>
				<RecentItemsList
					actionLabel="Browse All"
					emptyHeader="No recent filter combinations"
					emptyText="Your recent filter combinations will appear here."
					items={recentFilterDescriptions}
					onAction={browseAll}
					onItemPress={onRecentFilterPress}
					title="Browse"
				/>
			</ScrollView>
		</View>
	)
}

{'fraction_non_alphanumeric': 0.12435738344265201, 'fraction_numerical': 0.0011522779648998405, 'mean_word_length': 3.8844155844155845, 'pattern_counts': {'":': 0, '<': 27, '<?xml version=': 0, '>': 65, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5204855', 'n_tokens_mistral': 3967, 'n_tokens_neox': 3419, 'n_words': 928}