Skip to content

Commit

Permalink
perf: Optimize Global Search Performance (#1255)
Browse files Browse the repository at this point in the history
* Faster and less laggy global search

- Memoise each row in the search results page
- Limit number of concurrent searches (has setting, defaults to 5 sources at once)
- Load sources in the same order as they show in menu so from the user's pov it loads in faster
- Cancel search when u close global search
- Made the order of sources that are still loading follow the sorted order so theres less popin
- Added setting to remove animations from the loading placeholders
- Pause global search if u open a sub window (eg opening a novel)
- Change no results color to look less active

* Cancel previous global search when starting another

* Dont clear global search on unfocus
  • Loading branch information
Soopyboo32 authored Nov 4, 2024
1 parent 52ec11a commit 445187c
Show file tree
Hide file tree
Showing 20 changed files with 410 additions and 161 deletions.
1 change: 1 addition & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ module.exports = {
trailingComma: 'all',
arrowParens: 'avoid',
quoteProps: 'preserve',
endOfLine: 'auto', // stop prettier from getting mad on windows
};
8 changes: 5 additions & 3 deletions src/hooks/common/useSearch.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { useIsFocused } from '@react-navigation/native';
import { useCallback, useEffect, useState } from 'react';

const useSearch = (defaultSearchText?: string) => {
const useSearch = (defaultSearchText?: string, clearSearchOnUnfocus = true) => {
const isFocused = useIsFocused();

const [searchText, setSearchText] = useState<string>(defaultSearchText || '');

const clearSearchbar = useCallback(() => setSearchText(''), []);

useEffect(() => {
if (!isFocused) {
clearSearchbar();
if (clearSearchOnUnfocus) {
if (!isFocused) {
clearSearchbar();
}
}
}, [isFocused, clearSearchbar]);

Expand Down
4 changes: 4 additions & 0 deletions src/hooks/persisted/useSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface AppSettings {
showUpdatesTab: boolean;
showLabelsInNav: boolean;
useFabForContinueReading: boolean;
disableLoadingAnimations: boolean;

/**
* Library settings
Expand Down Expand Up @@ -56,6 +57,7 @@ export interface AppSettings {
export interface BrowseSettings {
showMyAnimeList: boolean;
showAniList: boolean;
globalSearchConcurrency?: number;
}

export interface LibrarySettings {
Expand Down Expand Up @@ -132,6 +134,7 @@ const initialAppSettings: AppSettings = {
showUpdatesTab: true,
showLabelsInNav: true,
useFabForContinueReading: false,
disableLoadingAnimations: false,

/**
* Library settings
Expand Down Expand Up @@ -160,6 +163,7 @@ const initialAppSettings: AppSettings = {
const initialBrowseSettings: BrowseSettings = {
showMyAnimeList: true,
showAniList: true,
globalSearchConcurrency: 1,
};

export const initialChapterGeneralSettings: ChapterGeneralSettings = {
Expand Down
2 changes: 1 addition & 1 deletion src/navigators/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import MigrateNovel from '../screens/browse/migration/MigrationNovels';
import MalTopNovels from '../screens/browse/discover/MalTopNovels';
import AniListTopNovels from '../screens/browse/discover/AniListTopNovels';
import NewUpdateDialog from '../components/NewUpdateDialog';
import BrowseSettings from '../screens/browse/BrowseSettings';
import BrowseSettings from '../screens/browse/settings/BrowseSettings';
import WebviewScreen from '@screens/WebviewScreen/WebviewScreen';
import { RootStackParamList } from './types';
import Color from 'color';
Expand Down
3 changes: 3 additions & 0 deletions src/screens/Categories/components/CategorySkeletonLoading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder';
import { LinearGradient } from 'expo-linear-gradient';
import { ThemeColors } from '@theme/types';
import getLoadingColors from '@utils/getLoadingColors';
import { useAppSettings } from '@hooks/persisted/index';

interface Props {
width: number;
Expand All @@ -12,6 +13,7 @@ interface Props {
}

const CategorySkeletonLoading: React.FC<Props> = ({ height, width, theme }) => {
const { disableLoadingAnimations } = useAppSettings();
const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient);

const [highlightColor, backgroundColor] = getLoadingColors(theme);
Expand All @@ -24,6 +26,7 @@ const CategorySkeletonLoading: React.FC<Props> = ({ height, width, theme }) => {
shimmerColors={[backgroundColor, highlightColor, backgroundColor]}
height={height}
width={width}
stopAutoRun={disableLoadingAnimations}
/>
</View>
);
Expand Down
1 change: 1 addition & 0 deletions src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const GlobalSearchScreen = (props: Props) => {
const theme = useTheme();
const { searchText, setSearchText, clearSearchbar } = useSearch(
props?.route?.params?.searchText,
false,
);
const onChangeText = (text: string) => setSearchText(text);
const onSubmitEditing = () => globalSearch(searchText);
Expand Down
219 changes: 114 additions & 105 deletions src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import { GlobalSearchResult } from '../hooks/useGlobalSearch';
import GlobalSearchNovelItem from './GlobalSearchNovelItem';
import { useLibraryNovels } from '@screens/library/hooks/useLibrary';
import { LibraryNovelInfo } from '@database/types';
import GlobalSearchSkeletonLoading from '@screens/browse/loadingAnimation/GlobalSearchSkeletonLoading';
import { switchNovelToLibrary } from '@database/queries/NovelQueries';
import GlobalSearchSkeletonLoading from '@screens/browse/loadingAnimation/GlobalSearchSkeletonLoading';
import { interpolateColor } from 'react-native-reanimated';

interface GlobalSearchResultsListProps {
searchResults: GlobalSearchResult[];
Expand All @@ -25,130 +26,138 @@ const GlobalSearchResultsList: React.FC<GlobalSearchResultsListProps> = ({
searchResults,
ListEmptyComponent,
}) => {
const theme = useTheme();
const navigation = useNavigation<StackNavigationProp<any>>();
const keyExtractor = useCallback(
(item: GlobalSearchResult) => item.plugin.id,
[],
);

return (
<FlatList<GlobalSearchResult>
keyExtractor={keyExtractor}
data={searchResults}
contentContainerStyle={styles.resultList}
renderItem={({ item }) => <GlobalSearchSourceResults item={item} />}
ListEmptyComponent={ListEmptyComponent}
/>
);
};

const GlobalSearchSourceResults: React.FC<{ item: GlobalSearchResult }> = ({
item,
}) => {
const theme = useTheme();
const navigation = useNavigation<StackNavigationProp<any>>();
const { library, setLibrary } = useLibraryNovels();

const novelInLibrary = (pluginId: string, novelPath: string) =>
library?.some(
novel => novel.pluginId === pluginId && novel.path === novelPath,
);

const errorColor = useMemo(() => (theme.isDark ? '#B3261E' : '#F2B8B5'), []);
const errorColor = theme.isDark ? '#B3261E' : '#F2B8B5';
const noResultsColor = interpolateColor(
0.8,
[0, 1],
['transparent', theme.onSurfaceVariant],
);

const navigateToNovel = useCallback(
(item: { name: string; path: string; pluginId: string }) =>
navigation.push('Novel', item),
[],
);

return (
<FlatList<GlobalSearchResult>
keyExtractor={keyExtractor}
data={searchResults}
contentContainerStyle={styles.resultList}
renderItem={({ item }) => (
<>
<View>
<Pressable
android_ripple={{
color: color(theme.primary).alpha(0.12).string(),
}}
style={styles.sourceHeader}
onPress={() =>
navigation.navigate('SourceScreen', {
pluginId: item.plugin.id,
pluginName: item.plugin.name,
site: item.plugin.site,
})
}
>
<View>
<Text style={[styles.sourceName, { color: theme.onSurface }]}>
{item.plugin.name}
</Text>
<Text
style={[styles.language, { color: theme.onSurfaceVariant }]}
>
{item.plugin.lang}
</Text>
</View>
<MaterialCommunityIcons
name="arrow-right"
size={24}
color={theme.onSurface}
/>
</Pressable>
{item.isLoading ? (
<GlobalSearchSkeletonLoading theme={theme} />
) : item.error ? (
<Text style={[styles.error, { color: errorColor }]}>
{item.error}
return useMemo(
() => (
<>
<View>
<Pressable
android_ripple={{
color: color(theme.primary).alpha(0.12).string(),
}}
style={styles.sourceHeader}
onPress={() =>
navigation.navigate('SourceScreen', {
pluginId: item.plugin.id,
pluginName: item.plugin.name,
site: item.plugin.site,
})
}
>
<View>
<Text style={[styles.sourceName, { color: theme.onSurface }]}>
{item.plugin.name}
</Text>
) : (
<FlatList
horizontal
contentContainerStyle={styles.novelsContainer}
keyExtractor={novelItem =>
item.plugin.id + '_' + novelItem.path
}
data={item.novels}
ListEmptyComponent={
<Text
style={[
styles.listEmpty,
{ color: theme.onSurfaceVariant },
]}
>
{getString('sourceScreen.noResultsFound')}
</Text>
}
renderItem={({ item: novelItem }) => {
const inLibrary = novelInLibrary(
item.plugin.id,
novelItem.path,
);
<Text
style={[styles.language, { color: theme.onSurfaceVariant }]}
>
{item.plugin.lang}
</Text>
</View>
<MaterialCommunityIcons
name="arrow-right"
size={24}
color={theme.onSurface}
/>
</Pressable>
{item.isLoading ? (
<GlobalSearchSkeletonLoading theme={theme} />
) : item.error ? (
<Text style={[styles.error, { color: errorColor }]}>
{item.error}
</Text>
) : (
<FlatList
horizontal
contentContainerStyle={styles.novelsContainer}
keyExtractor={novelItem => item.plugin.id + '_' + novelItem.path}
data={item.novels}
ListEmptyComponent={
<Text style={[styles.listEmpty, { color: noResultsColor }]}>
{getString('sourceScreen.noResultsFound')}
</Text>
}
renderItem={({ item: novelItem }) => {
const inLibrary = novelInLibrary(
item.plugin.id,
novelItem.path,
);

return (
<GlobalSearchNovelItem
novel={novelItem}
pluginId={item.plugin.id}
inLibrary={inLibrary}
navigateToNovel={navigateToNovel}
theme={theme}
onLongPress={() => {
setLibrary(prevValues => {
if (inLibrary) {
return [
...prevValues.filter(
novel => novel.path !== novelItem.path,
),
];
} else {
return [
...prevValues,
{
path: novelItem.path,
} as LibraryNovelInfo,
];
}
});
switchNovelToLibrary(novelItem.path, item.plugin.id);
}}
/>
);
}}
/>
)}
</View>
</>
)}
ListEmptyComponent={ListEmptyComponent}
/>
return (
<GlobalSearchNovelItem
novel={novelItem}
pluginId={item.plugin.id}
inLibrary={inLibrary}
navigateToNovel={navigateToNovel}
theme={theme}
onLongPress={() => {
setLibrary(prevValues => {
if (inLibrary) {
return [
...prevValues.filter(
novel => novel.path !== novelItem.path,
),
];
} else {
return [
...prevValues,
{
path: novelItem.path,
} as LibraryNovelInfo,
];
}
});
switchNovelToLibrary(novelItem.path, item.plugin.id);
}}
/>
);
}}
/>
)}
</View>
</>
),
[item.isLoading],
);
};

Expand Down
Loading

0 comments on commit 445187c

Please sign in to comment.