1
0
Fork 0
forked from Mirrors/gomuks
nyxmuks/web/src/ui/composer/Autocompleter.tsx
2024-10-26 15:49:04 +03:00

144 lines
4.9 KiB
TypeScript

// 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, use, useEffect } from "react"
import { getAvatarURL, getMediaURL } from "@/api/media.ts"
import { RoomStateStore, useCustomEmojis } from "@/api/statestore"
import { Emoji, emojiToMarkdown, useSortedAndFilteredEmojis } from "@/util/emoji"
import useEvent from "@/util/useEvent.ts"
import { ClientContext } from "../ClientContext.ts"
import type { ComposerState } from "./MessageComposer.tsx"
import { AutocompleteUser, useFilteredMembers } from "./userautocomplete.ts"
import "./Autocompleter.css"
export interface AutocompleteQuery {
type: "user" | "room" | "emoji"
query: string
startPos: number
endPos: number
frozenQuery?: string
selected?: number
}
export interface AutocompleterProps {
setState: (state: Partial<ComposerState>) => void
setAutocomplete: (params: AutocompleteQuery | null) => void
state: ComposerState
params: AutocompleteQuery
room: RoomStateStore
}
const positiveMod = (val: number, div: number) => (val % div + div) % div
interface InnerAutocompleterProps<T> extends AutocompleterProps {
items: T[]
getText: (item: T) => string
getKey: (item: T) => string
render: (item: T) => JSX.Element
}
function useAutocompleter<T>({
params, state, setState, setAutocomplete,
items, getText, getKey, render,
}: InnerAutocompleterProps<T>) {
const onSelect = useEvent((index: number) => {
if (items.length === 0) {
return
}
index = positiveMod(index, items.length)
const replacementText = getText(items[index])
setState({
text: state.text.slice(0, params.startPos) + replacementText + state.text.slice(params.endPos),
})
setAutocomplete({
...params,
endPos: params.startPos + replacementText.length,
frozenQuery: params.frozenQuery ?? params.query,
})
document.querySelector(`div.autocompletion-item[data-index='${index}']`)?.scrollIntoView({ block: "nearest" })
})
const onClick = useEvent((evt: React.MouseEvent<HTMLDivElement>) => {
const idx = evt.currentTarget.getAttribute("data-index")
if (idx) {
onSelect(+idx)
setAutocomplete(null)
}
})
useEffect(() => {
if (params.selected !== undefined) {
onSelect(params.selected)
}
}, [onSelect, params.selected])
const selected = params.selected !== undefined ? positiveMod(params.selected, items.length) : -1
return <div className="autocompletions">
{items.map((item, i) => <div
onClick={onClick}
data-index={i}
className={`autocompletion-item ${selected === i ? "selected" : ""}`}
key={getKey(item)}
>{render(item)}</div>)}
</div>
}
const emojiFuncs = {
getText: (emoji: Emoji) => emojiToMarkdown(emoji),
getKey: (emoji: Emoji) => `${emoji.c}-${emoji.u}`,
render: (emoji: Emoji) => <>{emoji.u.startsWith("mxc://")
? <img loading="lazy" src={getMediaURL(emoji.u)} alt={`:${emoji.n}:`}/>
: emoji.u
} :{emoji.n}:</>,
}
export const EmojiAutocompleter = ({ params, room, ...rest }: AutocompleterProps) => {
const client = use(ClientContext)!
const customEmojiPacks = useCustomEmojis(client.store, room)
const items = useSortedAndFilteredEmojis((params.frozenQuery ?? params.query).slice(1), {
frequentlyUsed: client.store.frequentlyUsedEmoji,
customEmojiPacks,
})
return useAutocompleter({ params, room, ...rest, items, ...emojiFuncs })
}
const escapeDisplayname = (input: string) => input
.replace("\n", " ")
.replace(/([\\`*_[\]])/g, "\\$1")
.replace("<", "&lt;")
.replace(">", "&gt;")
const userFuncs = {
getText: (user: AutocompleteUser) =>
`[${escapeDisplayname(user.displayName)}](https://matrix.to/#/${encodeURIComponent(user.userID)}) `,
getKey: (user: AutocompleteUser) => user.userID,
render: (user: AutocompleteUser) => <>
<img
className="small avatar"
loading="lazy"
src={getAvatarURL(user.userID, { displayname: user.displayName, avatar_url: user.avatarURL })}
alt=""
/>
{user.displayName}
</>,
}
export const UserAutocompleter = ({ params, room, ...rest }: AutocompleterProps) => {
const items = useFilteredMembers(room, (params.frozenQuery ?? params.query).slice(1))
return useAutocompleter({ params, room, ...rest, items, ...userFuncs })
}
export const RoomAutocompleter = ({ params }: AutocompleterProps) => {
return <div className="autocompletions">
Autocomplete {params.type} {params.query}
</div>
}