// 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 . import { CSSProperties, JSX, use, useCallback, useState } from "react" import { getMediaURL } from "@/api/media.ts" import { RoomStateStore, useCustomEmojis } from "@/api/statestore" 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 FallbackPackIcon from "@/icons/category.svg?react" import CloseIcon from "@/icons/close.svg?react" import ActivitiesIcon from "@/icons/emoji-categories/activities.svg?react" import AnimalsNatureIcon from "@/icons/emoji-categories/animals-nature.svg?react" import FlagsIcon from "@/icons/emoji-categories/flags.svg?react" import FoodBeverageIcon from "@/icons/emoji-categories/food-beverage.svg?react" import ObjectsIcon from "@/icons/emoji-categories/objects.svg?react" import PeopleBodyIcon from "@/icons/emoji-categories/people-body.svg?react" import SmileysEmotionIcon from "@/icons/emoji-categories/smileys-emotion.svg?react" import SymbolsIcon from "@/icons/emoji-categories/symbols.svg?react" import TravelPlacesIcon from "@/icons/emoji-categories/travel-places.svg?react" 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[] = [ { index: 7, icon: }, { index: 6, icon: }, { index: 1, icon: }, { index: 4, icon: }, { index: 9, icon: }, { index: 0, icon: }, { index: 5, icon: }, { index: 8, icon: }, { index: 3, icon: }, ] function renderEmoji(emoji: Emoji): JSX.Element | string { if (emoji.u.startsWith("mxc://")) { return {`:${emoji.n}:`}/ } return emoji.u } interface EmojiPickerProps { style: CSSProperties onSelect: (emoji: PartialEmoji, isSelected?: boolean) => void room: RoomStateStore allowFreeform?: boolean closeOnSelect?: boolean selected?: string[] } export const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, closeOnSelect }: EmojiPickerProps) => { const client = use(ClientContext)! const [query, setQuery] = useState("") const customEmojiPacks = useCustomEmojis(client.store, room) const emojis = useFilteredEmojis(query, { frequentlyUsed: client.store.frequentlyUsedEmoji, customEmojiPacks, }) const [previewEmoji, setPreviewEmoji] = useState() const clearQuery = useCallback(() => setQuery(""), []) const close = use(ModalCloseContext) const onSelectWrapped = (emoji?: PartialEmoji) => { if (!emoji) { return } onSelect(emoji, selected?.includes(emoji.u)) if (emoji.c) { 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) => onSelectWrapped(getEmojiFromAttrs(evt.currentTarget))) const onMouseOverEmoji = useEvent((evt: React.MouseEvent) => setPreviewEmoji(getEmojiFromAttrs(evt.currentTarget))) const onMouseOutEmoji = useCallback(() => setPreviewEmoji(undefined), []) const onClickFreeformReact = useEvent(() => onSelectWrapped({ u: query })) const renderedCats: JSX.Element[] = [] let currentCatRender: JSX.Element[] = [] let currentCatNum: number | string = -1 const renderCurrentCategory = () => { if (!currentCatRender.length) { return } const categoryName = typeof currentCatNum === "number" ? categories[currentCatNum] : currentCatNum renderedCats.push(

{categoryName}

{currentCatRender}
) 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() } renderCurrentCategory() } const onChangeQuery = useCallback((evt: React.ChangeEvent) => setQuery(evt.target.value), []) const onClickCategoryButton = useCallback((evt: React.MouseEvent) => { const categoryName = evt.currentTarget.getAttribute("title")! document.getElementById(`emoji-category-${categoryName}`)?.scrollIntoView({ behavior: "smooth" }) }, []) return
{sortedEmojiCategories.map(cat => , )} {customEmojiPacks.map(customPack => , )}
{renderedCats} {allowFreeform && query && }
{previewEmoji ?
{renderEmoji(previewEmoji)}
{previewEmoji.t}
:{previewEmoji.n}:
:
}
} export default EmojiPicker