Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: performance issues #163

Merged
merged 9 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ jobs:
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- name: Enable Corepack before setting up Node
run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 18
cache: yarn
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/test-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ jobs:
name: Test deployment
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- name: Enable Corepack before setting up Node
run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 18
cache: yarn
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/tsc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ jobs:
tsc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- name: Enable Corepack before setting up Node
run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 18
cache: yarn
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"prettier": "^2.0.5",
"react": "18.2.0",
"react-native": "0.71.8",
"react-native-builder-bob": "^0.20.0",
"react-native-builder-bob": "^0.23.2",
"react-test-renderer": "18.2.0",
"release-it": "^15.0.0",
"typescript": "^5.0.2",
Expand All @@ -88,7 +88,7 @@
"engines": {
"node": ">= 16.0.0"
},
"packageManager": "^[email protected].15",
"packageManager": "[email protected].21",
"jest": {
"preset": "react-native",
"modulePathIgnorePatterns": [
Expand Down
88 changes: 62 additions & 26 deletions src/components/EmojiCategory.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import * as React from 'react'

import { StyleSheet, View, Text, FlatList, type ListRenderItemInfo } from 'react-native'
import type { EmojisByCategory, EmojiSizes, JsonEmoji } from '../types'
import {
CATEGORIES,
type CategoryPosition,
type EmojisByCategory,
type EmojiSizes,
type JsonEmoji,
} from '../types'
import { SingleEmoji } from './SingleEmoji'
import { KeyboardContext } from '../contexts/KeyboardContext'
import { useKeyboardStore } from '../store/useKeyboardStore'
import { parseEmoji } from '../utils/parseEmoji'
import { removeSkinToneModifier } from '../utils/skinToneSelectorUtils'
import { useKeyboard } from '../hooks/useKeyboard'
import { InteractionManager } from 'react-native'

const emptyEmoji: JsonEmoji = {
emoji: '',
Expand All @@ -16,13 +23,19 @@ const emptyEmoji: JsonEmoji = {
toneEnabled: false,
}

const ListFooterComponent = ({ categoryPosition }: { categoryPosition: CategoryPosition }) => (
<View style={categoryPosition === 'floating' ? styles.footerFloating : styles.footer} />
)

export const EmojiCategory = React.memo(
({
item: { title, data },
setKeyboardScrollOffsetY,
activeCategoryIndex,
}: {
item: EmojisByCategory
setKeyboardScrollOffsetY: React.Dispatch<React.SetStateAction<number>>
activeCategoryIndex: number
}) => {
const {
onEmojiSelected,
Expand All @@ -37,6 +50,7 @@ export const EmojiCategory = React.memo(
theme,
styles: themeStyles,
selectedEmojis,
minimalEmojisAmountToDisplay,
} = React.useContext(KeyboardContext)

const { keyboardHeight } = useKeyboard(true)
Expand All @@ -56,15 +70,6 @@ export const EmojiCategory = React.memo(
}
}, [numberOfColumns, data])

const getItemLayout = React.useCallback(
(_: JsonEmoji[] | null | undefined, index: number) => ({
length: emojiSize ? emojiSize : 0,
offset: emojiSize * Math.ceil(index / numberOfColumns),
index,
}),
[emojiSize, numberOfColumns],
)

const handleEmojiPress = React.useCallback(
(emoji: JsonEmoji) => {
if (emoji.name === 'blank emoji') return
Expand Down Expand Up @@ -132,41 +137,72 @@ export const EmojiCategory = React.memo(
themeStyles.emoji.selected,
],
)

const handleOnScroll = (ev: { nativeEvent: { contentOffset: { y: number } } }) => {
setKeyboardScrollOffsetY(ev.nativeEvent.contentOffset.y)
clearEmojiTonesData()
}

const keyExtractor = React.useCallback((item: JsonEmoji) => item.name, [])

const [maxIndex, setMaxIndex] = React.useState(0)

// with InteractionManager we can show emojis after interaction is finished
// It helps with delay during category change animation
InteractionManager.runAfterInteractions(() => {
if (maxIndex === 0 && data.length) {
setMaxIndex(minimalEmojisAmountToDisplay)
}
})

const onEndReached = () => {
if (maxIndex <= data.length) {
setMaxIndex(data.length)
}
}

React.useEffect(() => {
if (CATEGORIES[activeCategoryIndex] !== title) {
setMaxIndex(0)
}
}, [activeCategoryIndex, title])

const flatListData = data.slice(0, maxIndex)

return (
<View style={[styles.container, { width }]}>
{!hideHeader && (
<Text style={[styles.sectionTitle, themeStyles.header, { color: theme.header }]}>
{translation[title]}
</Text>
)}
<FlatList
data={[...data, ...empty]}
keyExtractor={keyExtractor}
numColumns={numberOfColumns}
renderItem={renderItem}
getItemLayout={getItemLayout}
onScroll={handleOnScroll}
ListFooterComponent={() => (
<View style={categoryPosition === 'floating' ? styles.footerFloating : styles.footer} />
)}
initialNumToRender={10}
windowSize={16}
maxToRenderPerBatch={5}
keyboardShouldPersistTaps="handled"
contentContainerStyle={contentContainerStyle}
/>
{flatListData.length === 0 ? null : (
<FlatList
data={[...flatListData, ...empty]}
onEndReached={onEndReached}
onEndReachedThreshold={0.3}
keyExtractor={keyExtractor}
numColumns={numberOfColumns}
renderItem={renderItem}
onScroll={handleOnScroll}
ListFooterComponent={<ListFooterComponent categoryPosition={categoryPosition} />}
initialNumToRender={10}
windowSize={16}
maxToRenderPerBatch={5}
keyboardShouldPersistTaps="handled"
contentContainerStyle={contentContainerStyle}
scrollEventThrottle={16}
/>
)}
</View>
)
},
(prevProps, nextProps) => {
if (prevProps.activeCategoryIndex !== nextProps.activeCategoryIndex) return false
if (nextProps.item.title !== 'search') return true

if (prevProps.item.data.length !== nextProps.item.data.length) return false

return (
prevProps.item.data.map((d) => d.name).join() ===
nextProps.item.data.map((d) => d.name).join()
Expand Down
31 changes: 28 additions & 3 deletions src/components/EmojiStaticKeyboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
type NativeScrollEvent,
type NativeSyntheticEvent,
} from 'react-native'
import type { EmojisByCategory } from '../types'
import { type EmojisByCategory } from '../types'
import { EmojiCategory } from './EmojiCategory'
import { KeyboardContext } from '../contexts/KeyboardContext'
import { Categories } from './Categories'
Expand Down Expand Up @@ -56,8 +56,33 @@
const [keyboardScrollOffsetY, setKeyboardScrollOffsetY] = React.useState(0)

const renderItem = React.useCallback(
(props) => <EmojiCategory setKeyboardScrollOffsetY={setKeyboardScrollOffsetY} {...props} />,
[],
(props) => {
const item = { ...props.item, data: [] }
const shouldRenderEmojis =
activeCategoryIndex === props.index ||
activeCategoryIndex === props.index - 1 ||
activeCategoryIndex === props.index + 1

if (shouldRenderEmojis) {
return (
<EmojiCategory
setKeyboardScrollOffsetY={setKeyboardScrollOffsetY}
{...props}
activeCategoryIndex={activeCategoryIndex}
/>
)
} else {
return (
<EmojiCategory
setKeyboardScrollOffsetY={setKeyboardScrollOffsetY}
{...props}
item={item}
activeCategoryIndex={activeCategoryIndex}
/>
)
}
},
[activeCategoryIndex],
)

React.useEffect(() => {
Expand Down Expand Up @@ -100,7 +125,7 @@
>
<ConditionalContainer
condition={!disableSafeArea}
container={(children) => (

Check warning on line 128 in src/components/EmojiStaticKeyboard.tsx

View workflow job for this annotation

GitHub Actions / eslint

Do not define components during render. React will see a new component type on every render and destroy the entire subtree’s DOM nodes and state (https://reactjs.org/docs/reconciliation.html#elements-of-different-types). Instead, move this component definition out of the parent component and pass data as props. If you want to allow component creation in props, set allowAsProps option to true
<SafeAreaView
style={[styles.flex, categoryPosition === 'top' && styles.containerReverse]}
>
Expand Down
2 changes: 2 additions & 0 deletions src/contexts/KeyboardContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export type ContextValues = {
emojiTonesData: EmojiTonesData
shouldAnimateScroll: boolean
setShouldAnimateScroll: (value: boolean) => void
minimalEmojisAmountToDisplay: number
}

export const emptyStyles: Styles = {
Expand Down Expand Up @@ -197,6 +198,7 @@ export const defaultKeyboardValues: ContextValues = {
},
shouldAnimateScroll: true,
setShouldAnimateScroll: (_value: boolean) => {},
minimalEmojisAmountToDisplay: 50,
}

export const KeyboardContext = React.createContext<
Expand Down
32 changes: 30 additions & 2 deletions src/contexts/KeyboardProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export const KeyboardProvider: React.FC<ProviderProps> = React.memo((props) => {
const [searchPhrase, setSearchPhrase] = React.useState('')
const { keyboardState } = useKeyboardStore()

const { height } = useWindowDimensions()

const [emojiTonesData, setEmojiTonesData] = React.useState<EmojiTonesData>(null)

const numberOfColumns = React.useRef<number>(
Expand All @@ -41,6 +43,28 @@ export const KeyboardProvider: React.FC<ProviderProps> = React.memo((props) => {
),
)

// On initial render we want to display only emojis that are visible right away after keyboard open
// Rest of emojis are loaded after user interaction with keyboard
const calculateMinimalEmojisAmountToDisplay = () => {
const defaultHeight = props.defaultHeight || defaultKeyboardContext.defaultHeight
const emojiSize = props.emojiSize || defaultKeyboardContext.emojiSize

const keyboardHeightPercentage =
typeof defaultHeight === 'string'
? defaultHeight.substring(0, defaultHeight.length - 1)
: defaultHeight

const keyboardHeight = height * (Number(keyboardHeightPercentage) / 100)

const minimalEmojisAmount = Math.ceil(
(keyboardHeight / (emojiSize * 2)) * numberOfColumns.current,
)

return minimalEmojisAmount + minimalEmojisAmount / numberOfColumns.current
}

const minimalEmojisAmountToDisplay = calculateMinimalEmojisAmountToDisplay()

const generateEmojiTones = React.useCallback(
(emoji: JsonEmoji, emojiIndex: number, emojiSizes: any) => {
if (!emoji || !emoji.toneEnabled) return
Expand Down Expand Up @@ -118,8 +142,10 @@ export const KeyboardProvider: React.FC<ProviderProps> = React.memo((props) => {
let data = emojisByCategory.filter((category) => {
const title = category.title as CategoryTypes
if (props.disabledCategories) return !props.disabledCategories.includes(title)

return true
})

if (keyboardState.recentlyUsed.length && props.enableRecentlyUsed) {
data.push({
title: 'recently_used' as CategoryTypes,
Expand Down Expand Up @@ -161,12 +187,12 @@ export const KeyboardProvider: React.FC<ProviderProps> = React.memo((props) => {
}
return data as EmojisByCategory[]
}, [
keyboardState.recentlyUsed,
props.emojisByCategory,
props.enableRecentlyUsed,
props.enableSearchBar,
props.categoryOrder,
props.disabledCategories,
props.emojisByCategory,
keyboardState.recentlyUsed,
searchPhrase,
])

Expand All @@ -190,6 +216,7 @@ export const KeyboardProvider: React.FC<ProviderProps> = React.memo((props) => {
emojiTonesData,
shouldAnimateScroll,
setShouldAnimateScroll,
minimalEmojisAmountToDisplay,
}),
[
activeCategoryIndex,
Expand All @@ -200,6 +227,7 @@ export const KeyboardProvider: React.FC<ProviderProps> = React.memo((props) => {
searchPhrase,
shouldAnimateScroll,
width,
minimalEmojisAmountToDisplay,
],
)
return <KeyboardContext.Provider value={value}>{props.children}</KeyboardContext.Provider>
Expand Down
15 changes: 4 additions & 11 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6464,11 +6464,6 @@ jest@^28.1.1:
import-local "^3.0.2"
jest-cli "^28.1.3"

jetifier@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/jetifier/-/jetifier-2.0.0.tgz#699391367ca1fe7bc4da5f8bf691eb117758e4cb"
integrity sha512-J4Au9KuT74te+PCCCHKgAjyLlEa+2VyIAEPNCdE5aNkAJ6FAJcAqcdzEkSnzNksIa9NkGmC4tPiClk2e7tCJuQ==

joi@^17.2.1:
version "17.9.2"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.9.2.tgz#8b2e4724188369f55451aebd1d0b1d9482470690"
Expand Down Expand Up @@ -8340,10 +8335,10 @@ react-is@^17.0.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==

react-native-builder-bob@^0.20.0:
version "0.20.4"
resolved "https://registry.yarnpkg.com/react-native-builder-bob/-/react-native-builder-bob-0.20.4.tgz#02df01b8dc02f1bb2d566f820e33c5d42bfb9c99"
integrity sha512-3ZmYP8H7Fg2D8/JAPvxT78I4VWzf5DNMUf69cxWPw7Pukt+hHp1PSQ303af63uv1QXxWMJtrQ11+nuUfVqQf0Q==
react-native-builder-bob@^0.23.2:
version "0.23.2"
resolved "https://registry.yarnpkg.com/react-native-builder-bob/-/react-native-builder-bob-0.23.2.tgz#9f3f6509a9cba8102468169963ca7e1f0aa941a5"
integrity sha512-ehv2XKS9cvPR5JR7FIpSx3qY7tULkljT2Kb82FBAxXsFLjqlRU1WfqHRLh6lytL2XqAxLQODpPfHUH53SsXnag==
dependencies:
"@babel/core" "^7.18.5"
"@babel/plugin-proposal-class-properties" "^7.17.12"
Expand All @@ -8364,8 +8359,6 @@ react-native-builder-bob@^0.20.0:
prompts "^2.4.2"
which "^2.0.2"
yargs "^17.5.1"
optionalDependencies:
jetifier "^2.0.0"

react-native-codegen@^0.71.5:
version "0.71.5"
Expand Down