mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13:41 -05:00
web/emojipicker: make rendering lazier
This commit is contained in:
parent
2de87fa645
commit
11dad8541f
7 changed files with 250 additions and 160 deletions
114
web/src/ui/emojipicker/EmojiGroup.tsx
Normal file
114
web/src/ui/emojipicker/EmojiGroup.tsx
Normal file
|
@ -0,0 +1,114 @@
|
|||
// gomuks - A Matrix client written in Go.
|
||||
// Copyright (C) 2024 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import React, { use, useCallback } from "react"
|
||||
import { stringToRoomStateGUID } from "@/api/types"
|
||||
import useContentVisibility from "@/util/contentvisibility.ts"
|
||||
import { CATEGORY_FREQUENTLY_USED, CustomEmojiPack, Emoji, PartialEmoji, categories } from "@/util/emoji"
|
||||
import useEvent from "@/util/useEvent.ts"
|
||||
import { ClientContext } from "../ClientContext.ts"
|
||||
import renderEmoji from "./renderEmoji.tsx"
|
||||
|
||||
interface EmojiGroupProps {
|
||||
emojis: Emoji[]
|
||||
categoryID: number | string
|
||||
selected?: string[]
|
||||
pack?: CustomEmojiPack
|
||||
isWatched?: boolean
|
||||
onSelect: (emoji?: PartialEmoji) => void
|
||||
setPreviewEmoji: (emoji?: Emoji) => void
|
||||
}
|
||||
|
||||
export const EmojiGroup = ({
|
||||
emojis,
|
||||
categoryID,
|
||||
selected,
|
||||
pack,
|
||||
isWatched,
|
||||
onSelect,
|
||||
setPreviewEmoji,
|
||||
}: EmojiGroupProps) => {
|
||||
const client = use(ClientContext)!
|
||||
const [isVisible, divRef] = useContentVisibility<HTMLDivElement>(true)
|
||||
|
||||
const getEmojiFromAttrs = (elem: HTMLButtonElement) => {
|
||||
const idx = elem.getAttribute("data-emoji-index")
|
||||
if (!idx) {
|
||||
return
|
||||
}
|
||||
const emoji = emojis[+idx]
|
||||
if (!emoji) {
|
||||
return
|
||||
}
|
||||
return emoji
|
||||
}
|
||||
const onClickEmoji = useEvent((evt: React.MouseEvent<HTMLButtonElement>) =>
|
||||
onSelect(getEmojiFromAttrs(evt.currentTarget)))
|
||||
const onMouseOverEmoji = useEvent((evt: React.MouseEvent<HTMLButtonElement>) =>
|
||||
setPreviewEmoji(getEmojiFromAttrs(evt.currentTarget)))
|
||||
const onMouseOutEmoji = useCallback(() => setPreviewEmoji(undefined), [setPreviewEmoji])
|
||||
const onClickSubscribePack = useEvent((evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const guid = stringToRoomStateGUID(evt.currentTarget.getAttribute("data-pack-id"))
|
||||
if (!guid) {
|
||||
return
|
||||
}
|
||||
client.subscribeToEmojiPack(guid, true)
|
||||
.catch(err => window.alert(`Failed to subscribe to emoji pack: ${err}`))
|
||||
})
|
||||
const onClickUnsubscribePack = useEvent((evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const guid = stringToRoomStateGUID(evt.currentTarget.getAttribute("data-pack-id"))
|
||||
if (!guid) {
|
||||
return
|
||||
}
|
||||
client.subscribeToEmojiPack(guid, false)
|
||||
.catch(err => window.alert(`Failed to unsubscribe from emoji pack: ${err}`))
|
||||
})
|
||||
|
||||
let categoryName: string
|
||||
if (typeof categoryID === "number") {
|
||||
categoryName = categories[categoryID]
|
||||
} else if (categoryID === CATEGORY_FREQUENTLY_USED) {
|
||||
categoryName = CATEGORY_FREQUENTLY_USED
|
||||
} else if (pack) {
|
||||
categoryName = pack.name
|
||||
} else {
|
||||
categoryName = "Unknown category"
|
||||
}
|
||||
return <div
|
||||
ref={divRef}
|
||||
className="emoji-category"
|
||||
id={`emoji-category-${categoryID}`}
|
||||
style={{ containIntrinsicHeight: `${1.5 + Math.ceil(emojis.length / 8) * 2.5}rem` }}
|
||||
>
|
||||
<h4 className="emoji-category-name">
|
||||
{categoryName}
|
||||
{pack && <button
|
||||
className="emoji-category-add"
|
||||
onClick={isWatched ? onClickUnsubscribePack : onClickSubscribePack}
|
||||
data-pack-id={categoryID}
|
||||
>{isWatched ? "Unsubscribe" : "Subscribe"}</button>}
|
||||
</h4>
|
||||
<div className="emoji-category-list">
|
||||
{isVisible ? emojis.map((emoji, idx) => <button
|
||||
key={emoji.u}
|
||||
className={`emoji ${selected?.includes(emoji.u) ? "selected" : ""}`}
|
||||
data-emoji-index={idx}
|
||||
onMouseOver={onMouseOverEmoji}
|
||||
onMouseOut={onMouseOutEmoji}
|
||||
onClick={onClickEmoji}
|
||||
>{renderEmoji(emoji)}</button>) : null}
|
||||
</div>
|
||||
</div>
|
||||
}
|
|
@ -104,6 +104,7 @@ div.emoji-picker {
|
|||
div.emoji-category {
|
||||
width: 100%;
|
||||
content-visibility: auto;
|
||||
contain: size;
|
||||
}
|
||||
|
||||
div.emoji-category-list {
|
||||
|
|
|
@ -13,14 +13,16 @@
|
|||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { CSSProperties, JSX, use, useCallback, useState } from "react"
|
||||
import React, { CSSProperties, JSX, use, useCallback, useState } from "react"
|
||||
import { getMediaURL } from "@/api/media.ts"
|
||||
import { RoomStateStore, useCustomEmojis } from "@/api/statestore"
|
||||
import { roomStateGUIDToString, stringToRoomStateGUID } from "@/api/types"
|
||||
import { roomStateGUIDToString } from "@/api/types"
|
||||
import { CATEGORY_FREQUENTLY_USED, Emoji, PartialEmoji, categories, useFilteredEmojis } from "@/util/emoji"
|
||||
import useEvent from "@/util/useEvent.ts"
|
||||
import { ClientContext } from "../ClientContext.ts"
|
||||
import { ModalCloseContext } from "../modal/Modal.tsx"
|
||||
import { EmojiGroup } from "./EmojiGroup.tsx"
|
||||
import renderEmoji from "./renderEmoji.tsx"
|
||||
import FallbackPackIcon from "@/icons/category.svg?react"
|
||||
import CloseIcon from "@/icons/close.svg?react"
|
||||
import ActivitiesIcon from "@/icons/emoji-categories/activities.svg?react"
|
||||
|
@ -36,13 +38,7 @@ import RecentIcon from "@/icons/schedule.svg?react"
|
|||
import SearchIcon from "@/icons/search.svg?react"
|
||||
import "./EmojiPicker.css"
|
||||
|
||||
interface EmojiCategory {
|
||||
index: number
|
||||
name?: string
|
||||
icon: JSX.Element
|
||||
}
|
||||
|
||||
const sortedEmojiCategories: EmojiCategory[] = [
|
||||
const sortedEmojiCategories: {index: number, icon: JSX.Element}[] = [
|
||||
{ index: 7, icon: <SmileysEmotionIcon/> },
|
||||
{ index: 6, icon: <PeopleBodyIcon/> },
|
||||
{ index: 1, icon: <AnimalsNatureIcon/> },
|
||||
|
@ -54,13 +50,6 @@ const sortedEmojiCategories: EmojiCategory[] = [
|
|||
{ index: 3, icon: <FlagsIcon/> },
|
||||
]
|
||||
|
||||
function renderEmoji(emoji: Emoji): JSX.Element | string {
|
||||
if (emoji.u.startsWith("mxc://")) {
|
||||
return <img loading="lazy" src={getMediaURL(emoji.u)} alt={`:${emoji.n}:`}/>
|
||||
}
|
||||
return emoji.u
|
||||
}
|
||||
|
||||
interface EmojiPickerProps {
|
||||
style: CSSProperties
|
||||
onSelect: (emoji: PartialEmoji, isSelected?: boolean) => void
|
||||
|
@ -73,16 +62,16 @@ interface EmojiPickerProps {
|
|||
export const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, closeOnSelect }: EmojiPickerProps) => {
|
||||
const client = use(ClientContext)!
|
||||
const [query, setQuery] = useState("")
|
||||
const [previewEmoji, setPreviewEmoji] = useState<Emoji>()
|
||||
const watchedEmojiPackKeys = client.store.getEmojiPackKeys().map(roomStateGUIDToString)
|
||||
const customEmojiPacks = useCustomEmojis(client.store, room)
|
||||
const emojis = useFilteredEmojis(query, {
|
||||
frequentlyUsed: client.store.frequentlyUsedEmoji,
|
||||
customEmojiPacks,
|
||||
})
|
||||
const [previewEmoji, setPreviewEmoji] = useState<Emoji>()
|
||||
const clearQuery = useCallback(() => setQuery(""), [])
|
||||
const close = use(ModalCloseContext)
|
||||
const onSelectWrapped = (emoji?: PartialEmoji) => {
|
||||
const close = closeOnSelect ? use(ModalCloseContext) : null
|
||||
const onSelectWrapped = useCallback((emoji?: PartialEmoji) => {
|
||||
if (!emoji) {
|
||||
return
|
||||
}
|
||||
|
@ -91,121 +80,13 @@ export const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, cl
|
|||
client.incrementFrequentlyUsedEmoji(emoji.u)
|
||||
.catch(err => console.error("Failed to increment frequently used emoji", err))
|
||||
}
|
||||
if (closeOnSelect) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
const getEmojiFromAttrs = (elem: HTMLButtonElement) => {
|
||||
const groupIdx = elem.getAttribute("data-emoji-group-index")
|
||||
if (!groupIdx) {
|
||||
return
|
||||
}
|
||||
const idx = elem.getAttribute("data-emoji-index")
|
||||
if (!idx) {
|
||||
return
|
||||
}
|
||||
const emoji = emojis[+groupIdx]?.[+idx]
|
||||
if (!emoji) {
|
||||
return
|
||||
}
|
||||
return emoji
|
||||
}
|
||||
const onClickEmoji = useEvent((evt: React.MouseEvent<HTMLButtonElement>) =>
|
||||
onSelectWrapped(getEmojiFromAttrs(evt.currentTarget)))
|
||||
const onMouseOverEmoji = useEvent((evt: React.MouseEvent<HTMLButtonElement>) =>
|
||||
setPreviewEmoji(getEmojiFromAttrs(evt.currentTarget)))
|
||||
const onMouseOutEmoji = useCallback(() => setPreviewEmoji(undefined), [])
|
||||
close?.()
|
||||
}, [onSelect, selected, client, close])
|
||||
const onClickFreeformReact = useEvent(() => onSelectWrapped({ u: query }))
|
||||
const onClickSubscribePack = useEvent((evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const guid = stringToRoomStateGUID(evt.currentTarget.getAttribute("data-pack-id"))
|
||||
if (!guid) {
|
||||
return
|
||||
}
|
||||
client.subscribeToEmojiPack(guid, true)
|
||||
.catch(err => window.alert(`Failed to subscribe to emoji pack: ${err}`))
|
||||
})
|
||||
const onClickUnsubscribePack = useEvent((evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const guid = stringToRoomStateGUID(evt.currentTarget.getAttribute("data-pack-id"))
|
||||
if (!guid) {
|
||||
return
|
||||
}
|
||||
client.subscribeToEmojiPack(guid, false)
|
||||
.catch(err => window.alert(`Failed to unsubscribe from emoji pack: ${err}`))
|
||||
})
|
||||
|
||||
const renderedCats: JSX.Element[] = []
|
||||
let currentCatRender: JSX.Element[] = []
|
||||
let currentCatNum: number | string = -1
|
||||
const renderCurrentCategory = () => {
|
||||
if (!currentCatRender.length) {
|
||||
return
|
||||
}
|
||||
let categoryName: string
|
||||
let headerExtra: JSX.Element | null = null
|
||||
if (typeof currentCatNum === "number") {
|
||||
categoryName = categories[currentCatNum]
|
||||
} else if (currentCatNum === CATEGORY_FREQUENTLY_USED) {
|
||||
categoryName = CATEGORY_FREQUENTLY_USED
|
||||
} else {
|
||||
const customPack = customEmojiPacks.find(pack => pack.id === currentCatNum)
|
||||
categoryName = customPack?.name ?? "Unknown name"
|
||||
if (customPack && customPack.id !== "personal") {
|
||||
if (watchedEmojiPackKeys.includes(customPack.id)) {
|
||||
headerExtra = <button
|
||||
className="emoji-category-add"
|
||||
onClick={onClickUnsubscribePack}
|
||||
data-pack-id={customPack.id}
|
||||
>Unsubscribe</button>
|
||||
} else {
|
||||
headerExtra = <button
|
||||
className="emoji-category-add"
|
||||
onClick={onClickSubscribePack}
|
||||
data-pack-id={customPack.id}
|
||||
>Subscribe</button>
|
||||
}
|
||||
}
|
||||
}
|
||||
renderedCats.push(<div
|
||||
className="emoji-category"
|
||||
key={currentCatNum}
|
||||
id={`emoji-category-${currentCatNum}`}
|
||||
style={{ containIntrinsicHeight: `${1.5 + Math.ceil(currentCatRender.length / 8) * 2.5}rem` }}
|
||||
>
|
||||
<h4 className="emoji-category-name">{categoryName}{headerExtra}</h4>
|
||||
<div className="emoji-category-list">
|
||||
{currentCatRender}
|
||||
</div>
|
||||
</div>)
|
||||
currentCatRender = []
|
||||
currentCatNum = -1
|
||||
}
|
||||
for (let catIdx = 0; catIdx < emojis.length; catIdx++) {
|
||||
const cat = emojis[catIdx]
|
||||
for (let emojiIdx = 0; emojiIdx < cat.length; emojiIdx++) {
|
||||
const emoji = cat[emojiIdx]
|
||||
if (emoji.c === 2) {
|
||||
continue
|
||||
}
|
||||
if (emoji.c !== currentCatNum) {
|
||||
renderCurrentCategory()
|
||||
currentCatNum = emoji.c
|
||||
}
|
||||
currentCatRender.push(<button
|
||||
key={`${emoji.c}-${emoji.u}`}
|
||||
className={`emoji ${selected?.includes(emoji.u) ? "selected" : ""}`}
|
||||
data-emoji-group-index={catIdx}
|
||||
data-emoji-index={emojiIdx}
|
||||
onMouseOver={onMouseOverEmoji}
|
||||
onMouseOut={onMouseOutEmoji}
|
||||
onClick={onClickEmoji}
|
||||
>{renderEmoji(emoji)}</button>)
|
||||
}
|
||||
renderCurrentCategory()
|
||||
}
|
||||
const onChangeQuery = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => setQuery(evt.target.value), [])
|
||||
const onClickCategoryButton = useCallback((evt: React.MouseEvent) => {
|
||||
const categoryID = evt.currentTarget.getAttribute("data-category-id")!
|
||||
document.getElementById(`emoji-category-${categoryID}`)?.scrollIntoView({ behavior: "smooth" })
|
||||
document.getElementById(`emoji-category-${categoryID}`)?.scrollIntoView()
|
||||
}, [])
|
||||
return <div className="emoji-picker" style={style}>
|
||||
<div className="emoji-category-bar">
|
||||
|
@ -214,13 +95,13 @@ export const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, cl
|
|||
data-category-id={CATEGORY_FREQUENTLY_USED}
|
||||
title={CATEGORY_FREQUENTLY_USED}
|
||||
onClick={onClickCategoryButton}
|
||||
>{<RecentIcon/>}</button>
|
||||
><RecentIcon/></button>
|
||||
{sortedEmojiCategories.map(cat =>
|
||||
<button
|
||||
key={cat.index}
|
||||
className="emoji-category-icon"
|
||||
data-category-id={cat.index}
|
||||
title={cat.name ?? categories[cat.index]}
|
||||
title={categories[cat.index]}
|
||||
onClick={onClickCategoryButton}
|
||||
>{cat.icon}</button>,
|
||||
)}
|
||||
|
@ -243,7 +124,23 @@ export const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, cl
|
|||
</button>
|
||||
</div>
|
||||
<div className="emoji-list">
|
||||
{renderedCats}
|
||||
{emojis.map(group => {
|
||||
if (!group?.length) {
|
||||
return null
|
||||
}
|
||||
const categoryID = group[0].c
|
||||
const customPack = customEmojiPacks.find(pack => pack.id === categoryID)
|
||||
return <EmojiGroup
|
||||
key={categoryID}
|
||||
emojis={group}
|
||||
categoryID={categoryID}
|
||||
selected={selected}
|
||||
pack={customPack}
|
||||
isWatched={typeof categoryID === "string" && watchedEmojiPackKeys.includes(categoryID)}
|
||||
onSelect={onSelectWrapped}
|
||||
setPreviewEmoji={setPreviewEmoji}
|
||||
/>
|
||||
})}
|
||||
{allowFreeform && query && <button
|
||||
className="freeform-react"
|
||||
onClick={onClickFreeformReact}
|
||||
|
|
25
web/src/ui/emojipicker/renderEmoji.tsx
Normal file
25
web/src/ui/emojipicker/renderEmoji.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
// gomuks - A Matrix client written in Go.
|
||||
// Copyright (C) 2024 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { JSX } from "react"
|
||||
import { getMediaURL } from "@/api/media.ts"
|
||||
import { Emoji } from "@/util/emoji"
|
||||
|
||||
export default function renderEmoji(emoji: Emoji): JSX.Element | string {
|
||||
if (emoji.u.startsWith("mxc://")) {
|
||||
return <img loading="lazy" src={getMediaURL(emoji.u)} alt={`:${emoji.n}:`}/>
|
||||
}
|
||||
return emoji.u
|
||||
}
|
|
@ -13,10 +13,11 @@
|
|||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { memo, use, useLayoutEffect, useRef, useState } from "react"
|
||||
import { memo, use } from "react"
|
||||
import { getAvatarURL } from "@/api/media.ts"
|
||||
import type { RoomListEntry } from "@/api/statestore"
|
||||
import type { MemDBEvent, MemberEventContent } from "@/api/types"
|
||||
import useContentVisibility from "@/util/contentvisibility.ts"
|
||||
import { ClientContext } from "../ClientContext.ts"
|
||||
|
||||
export interface RoomListEntryProps {
|
||||
|
@ -77,21 +78,7 @@ const EntryInner = ({ room }: InnerProps) => {
|
|||
}
|
||||
|
||||
const Entry = ({ room, setActiveRoom, isActive, hidden }: RoomListEntryProps) => {
|
||||
const [isVisible, setVisible] = useState(false)
|
||||
const divRef = useRef<HTMLDivElement>(null)
|
||||
useLayoutEffect(() => {
|
||||
const div = divRef.current
|
||||
if (!div) {
|
||||
return
|
||||
}
|
||||
const listener = (evt: unknown) => {
|
||||
if (!(evt as ContentVisibilityAutoStateChangeEvent).skipped) {
|
||||
setVisible(true)
|
||||
}
|
||||
}
|
||||
div.addEventListener("contentvisibilityautostatechange", listener)
|
||||
return () => div.removeEventListener("contentvisibilityautostatechange", listener)
|
||||
}, [])
|
||||
const [isVisible, divRef] = useContentVisibility<HTMLDivElement>()
|
||||
return <div
|
||||
ref={divRef}
|
||||
className={`room-entry ${isActive ? "active" : ""} ${hidden ? "hidden" : ""}`}
|
||||
|
|
39
web/src/util/contentvisibility.ts
Normal file
39
web/src/util/contentvisibility.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
// gomuks - A Matrix client written in Go.
|
||||
// Copyright (C) 2024 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { RefObject, useLayoutEffect, useRef, useState } from "react"
|
||||
|
||||
export default function useContentVisibilit<T extends HTMLElement>(
|
||||
allowRevert = false,
|
||||
): [boolean, RefObject<T | null>] {
|
||||
const ref = useRef<T>(null)
|
||||
const [isVisible, setVisible] = useState(false)
|
||||
useLayoutEffect(() => {
|
||||
const element = ref.current
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
const listener = (evt: unknown) => {
|
||||
if (!(evt as ContentVisibilityAutoStateChangeEvent).skipped) {
|
||||
setVisible(true)
|
||||
} else if (allowRevert) {
|
||||
setVisible(false)
|
||||
}
|
||||
}
|
||||
element.addEventListener("contentvisibilityautostatechange", listener)
|
||||
return () => element.removeEventListener("contentvisibilityautostatechange", listener)
|
||||
}, [allowRevert])
|
||||
return [isVisible, ref]
|
||||
}
|
|
@ -33,14 +33,35 @@ export type Emoji = EmojiText & EmojiMetadata
|
|||
|
||||
export const emojis: Emoji[] = data.e
|
||||
export const emojiMap = new Map<string, Emoji>()
|
||||
export const emojisByCategory: Emoji[][] = []
|
||||
export const categories = data.c
|
||||
|
||||
export const CATEGORY_FREQUENTLY_USED = "Frequently Used"
|
||||
|
||||
for (const emoji of emojis) {
|
||||
emojiMap.set(emoji.u, emoji)
|
||||
function initEmojiMaps() {
|
||||
let building: Emoji[] = []
|
||||
let buildingCat: number = -1
|
||||
for (const emoji of emojis) {
|
||||
emojiMap.set(emoji.u, emoji)
|
||||
if (emoji.c === 2) {
|
||||
continue
|
||||
}
|
||||
if (emoji.c !== buildingCat) {
|
||||
if (building.length) {
|
||||
emojisByCategory.push(building)
|
||||
}
|
||||
buildingCat = emoji.c as number
|
||||
building = []
|
||||
}
|
||||
building.push(emoji)
|
||||
}
|
||||
if (building.length) {
|
||||
emojisByCategory.push(building)
|
||||
}
|
||||
}
|
||||
|
||||
initEmojiMaps()
|
||||
|
||||
function filter(emojis: Emoji[], query: string): Emoji[] {
|
||||
return emojis.filter(emoji => emoji.s.some(shortcode => shortcode.includes(query)))
|
||||
}
|
||||
|
@ -187,16 +208,22 @@ export function useFilteredEmojis(query: string, params: useEmojisParams = {}):
|
|||
.filter(emoji => emoji !== undefined))
|
||||
.filter((_emoji, index) => index < 24)
|
||||
}, [params.frequentlyUsed])
|
||||
const allPacks = [frequentlyUsedCategory, emojis, ...(params.customEmojiPacks?.map(pack => pack.emojis) ?? [])]
|
||||
const prev = useRef<filteredEmojiCache>({ query: "", result: allPacks })
|
||||
if (!query) {
|
||||
prev.current.query = ""
|
||||
prev.current.result = allPacks
|
||||
} else if (prev.current.query !== query) {
|
||||
if (query.startsWith(prev.current.query) && allPacks.length === prev.current.result.length) {
|
||||
const prev = useRef<filteredEmojiCache>({
|
||||
query: "",
|
||||
result: [],
|
||||
})
|
||||
const categoriesChanged = prev.current.result.length !==
|
||||
(1 + emojisByCategory.length + (params.customEmojiPacks?.length ?? 0))
|
||||
if (prev.current.query !== query || categoriesChanged) {
|
||||
if (!query.startsWith(prev.current.query) || categoriesChanged) {
|
||||
prev.current.result = [
|
||||
frequentlyUsedCategory,
|
||||
...emojisByCategory,
|
||||
...(params.customEmojiPacks?.map(pack => pack.emojis) ?? []),
|
||||
]
|
||||
}
|
||||
if (query !== "") {
|
||||
prev.current.result = prev.current.result.map(pack => filter(pack, query))
|
||||
} else {
|
||||
prev.current.result = allPacks.map(pack => filter(pack, query))
|
||||
}
|
||||
prev.current.query = query
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue