mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 10:33:41 -05:00
web/emojipicker: small improvements
This commit is contained in:
parent
30222d2c6e
commit
55a9866eac
8 changed files with 84 additions and 34 deletions
|
@ -75,7 +75,7 @@ export default class Client {
|
|||
await this.rpc.setState(room.roomID, "m.room.pinned_events", "", { pinned: pinnedEvents })
|
||||
}
|
||||
|
||||
async sendEvent(roomID: RoomID, type: EventType, content: Record<string, unknown>): Promise<void> {
|
||||
async sendEvent(roomID: RoomID, type: EventType, content: unknown): Promise<void> {
|
||||
const room = this.store.rooms.get(roomID)
|
||||
if (!room) {
|
||||
throw new Error("Room not found")
|
||||
|
|
|
@ -128,7 +128,7 @@ export default abstract class RPCClient {
|
|||
return this.request("send_message", params)
|
||||
}
|
||||
|
||||
sendEvent(room_id: RoomID, type: EventType, content: Record<string, unknown>): Promise<RawDBEvent> {
|
||||
sendEvent(room_id: RoomID, type: EventType, content: unknown): Promise<RawDBEvent> {
|
||||
return this.request("send_event", { room_id, type, content })
|
||||
}
|
||||
|
||||
|
|
|
@ -125,6 +125,15 @@ export interface MediaMessageEventContent extends BaseMessageEventContent {
|
|||
info?: MediaInfo
|
||||
}
|
||||
|
||||
export interface ReactionEventContent {
|
||||
"m.relates_to": {
|
||||
rel_type: "m.annotation"
|
||||
event_id: EventID
|
||||
key: string
|
||||
}
|
||||
"com.beeper.emoji.shortcode"?: string
|
||||
}
|
||||
|
||||
export interface EncryptedFile {
|
||||
url: ContentURI
|
||||
k: string
|
||||
|
|
|
@ -25,6 +25,7 @@ import type {
|
|||
RelatesTo,
|
||||
RoomID,
|
||||
} from "@/api/types"
|
||||
import { emojiToMarkdown } from "@/util/emoji"
|
||||
import useEvent from "@/util/useEvent.ts"
|
||||
import { ClientContext } from "../ClientContext.ts"
|
||||
import EmojiPicker from "../emojipicker/EmojiPicker.tsx"
|
||||
|
@ -320,7 +321,7 @@ const MessageComposer = () => {
|
|||
onSelect={emoji => {
|
||||
setState({
|
||||
text: state.text.slice(0, textInput.current?.selectionStart ?? 0)
|
||||
+ emoji.u
|
||||
+ emojiToMarkdown(emoji)
|
||||
+ state.text.slice(textInput.current?.selectionEnd ?? 0),
|
||||
})
|
||||
}}
|
||||
|
|
|
@ -119,6 +119,11 @@ div.emoji-picker {
|
|||
&:hover {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border: 1px solid #cec;
|
||||
opacity: .8;
|
||||
}
|
||||
}
|
||||
|
||||
button.freeform-react {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { CSSProperties, JSX, use, useCallback, useState } from "react"
|
||||
import { getMediaURL } from "@/api/media.ts"
|
||||
import { Emoji, categories, useFilteredEmojis } from "@/util/emoji"
|
||||
import { Emoji, PartialEmoji, categories, useFilteredEmojis } from "@/util/emoji"
|
||||
import { ModalCloseContext } from "../modal/Modal.tsx"
|
||||
import CloseIcon from "@/icons/close.svg?react"
|
||||
import ActivitiesIcon from "@/icons/emoji-categories/activities.svg?react"
|
||||
|
@ -58,37 +58,42 @@ function renderEmoji(emoji: Emoji): JSX.Element | string {
|
|||
|
||||
interface EmojiPickerProps {
|
||||
style: CSSProperties
|
||||
onSelect: (emoji: Partial<Emoji>) => void
|
||||
onSelect: (emoji: PartialEmoji, isSelected?: boolean) => void
|
||||
allowFreeform?: boolean
|
||||
closeOnSelect?: boolean
|
||||
selected?: string[]
|
||||
}
|
||||
|
||||
export const EmojiPicker = ({ style, onSelect, allowFreeform, closeOnSelect }: EmojiPickerProps) => {
|
||||
export const EmojiPicker = ({ style, selected, onSelect, allowFreeform, closeOnSelect }: EmojiPickerProps) => {
|
||||
const [query, setQuery] = useState("")
|
||||
const emojis = useFilteredEmojis(query)
|
||||
const [previewEmoji, setPreviewEmoji] = useState<Emoji>()
|
||||
const clearQuery = useCallback(() => setQuery(""), [])
|
||||
const onChangeQuery = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => setQuery(evt.target.value), [])
|
||||
const cats: JSX.Element[] = []
|
||||
let currentCat: JSX.Element[] | undefined
|
||||
let currentCat: JSX.Element[] = []
|
||||
let currentCatNum: number | string = -1
|
||||
const close = use(ModalCloseContext)
|
||||
const onSelectWrapped = (emoji: Partial<Emoji>) => {
|
||||
onSelect(emoji)
|
||||
const onSelectWrapped = (emoji: PartialEmoji) => {
|
||||
onSelect(emoji, selected?.includes(emoji.u))
|
||||
if (closeOnSelect) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
cats.push(<div className="emoji-category" data-emoji-category="Frequently Used">
|
||||
<h4 className="emoji-category-name">Frequently Used</h4>
|
||||
<div className="emoji-category-list">
|
||||
{/* TODO */}
|
||||
</div>
|
||||
</div>)
|
||||
for (const emoji of emojis) {
|
||||
if (emoji.c === 2) {
|
||||
continue
|
||||
}
|
||||
if (emoji.c !== currentCatNum || !currentCat) {
|
||||
if (currentCat) {
|
||||
cats.push(<div className="emoji-category" key={currentCatNum} data-emoji-category={currentCatNum}>
|
||||
<h4 className="emoji-category-name">{
|
||||
typeof currentCatNum === "number" ? categories[currentCatNum] : currentCatNum
|
||||
}</h4>
|
||||
if (emoji.c !== currentCatNum) {
|
||||
if (currentCat.length) {
|
||||
const categoryName = typeof currentCatNum === "number" ? categories[currentCatNum] : currentCatNum
|
||||
cats.push(<div className="emoji-category" key={currentCatNum} id={`emoji-category-${categoryName}`}>
|
||||
<h4 className="emoji-category-name">{categoryName}</h4>
|
||||
<div className="emoji-category-list">
|
||||
{currentCat}
|
||||
</div>
|
||||
|
@ -99,23 +104,38 @@ export const EmojiPicker = ({ style, onSelect, allowFreeform, closeOnSelect }: E
|
|||
}
|
||||
currentCat.push(<button
|
||||
key={emoji.u}
|
||||
className="emoji"
|
||||
className={`emoji ${selected?.includes(emoji.u) ? "selected" : ""}`}
|
||||
onMouseOver={() => setPreviewEmoji(emoji)}
|
||||
onMouseOut={() => setPreviewEmoji(undefined)}
|
||||
onClick={() => onSelectWrapped(emoji)}
|
||||
>{renderEmoji(emoji)}</button>)
|
||||
}
|
||||
if (currentCat.length) {
|
||||
const categoryName = typeof currentCatNum === "number" ? categories[currentCatNum] : currentCatNum
|
||||
cats.push(<div className="emoji-category" key={currentCatNum} id={`emoji-category-${categoryName}`}>
|
||||
<h4 className="emoji-category-name">{categoryName}</h4>
|
||||
<div className="emoji-category-list">
|
||||
{currentCat}
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
const onChangeQuery = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => 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 <div className="emoji-picker" style={style}>
|
||||
<div className="emoji-category-bar">
|
||||
<button
|
||||
className="emoji-category-icon"
|
||||
title={"Recently used"}
|
||||
title="Frequently Used"
|
||||
>{<RecentIcon/>}</button>
|
||||
{sortedEmojiCategories.map(cat =>
|
||||
<button
|
||||
key={cat.index}
|
||||
className="emoji-category-icon"
|
||||
title={cat.name ?? categories[cat.index]}
|
||||
onClick={onClickCategoryButton}
|
||||
>{cat.icon}</button>,
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
import { CSSProperties, use, useCallback, useRef } from "react"
|
||||
import { useRoomState } from "@/api/statestore"
|
||||
import { MemDBEvent, PowerLevelEventContent } from "@/api/types"
|
||||
import { emojiToReactionContent } from "@/util/emoji"
|
||||
import { useNonNullEventAsState } from "@/util/eventdispatcher.ts"
|
||||
import { ClientContext } from "../ClientContext.ts"
|
||||
import EmojiPicker from "../emojipicker/EmojiPicker.tsx"
|
||||
|
@ -67,17 +68,7 @@ const EventMenu = ({ evt, setForceOpen }: EventHoverMenuProps) => {
|
|||
content: <EmojiPicker
|
||||
style={style}
|
||||
onSelect={emoji => {
|
||||
const content: Record<string, unknown> = {
|
||||
"m.relates_to": {
|
||||
rel_type: "m.annotation",
|
||||
event_id: evt.event_id,
|
||||
key: emoji.u,
|
||||
},
|
||||
}
|
||||
if (emoji.u?.startsWith("mxc://") && emoji.n) {
|
||||
content["com.beeper.emoji.shortcode"] = emoji.n
|
||||
}
|
||||
client.sendEvent(evt.room_id, "m.reaction", content)
|
||||
client.sendEvent(evt.room_id, "m.reaction", emojiToReactionContent(emoji, evt.event_id))
|
||||
.catch(err => window.alert(`Failed to send reaction: ${err}`))
|
||||
}}
|
||||
closeOnSelect={true}
|
||||
|
|
|
@ -14,16 +14,19 @@
|
|||
// 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 { useRef } from "react"
|
||||
import { EventID, ReactionEventContent } from "@/api/types"
|
||||
import data from "./data.json"
|
||||
|
||||
export interface Emoji {
|
||||
export interface PartialEmoji {
|
||||
u: string // Unicode codepoint or custom emoji mxc:// URI
|
||||
c: number | string // Category number or custom emoji pack name
|
||||
t: string // Emoji title
|
||||
n: string // Primary shortcode
|
||||
s: string[] // Shortcodes without underscores
|
||||
c?: number | string // Category number or custom emoji pack name
|
||||
t?: string // Emoji title
|
||||
n?: string // Primary shortcode
|
||||
s?: string[] // Shortcodes without underscores
|
||||
}
|
||||
|
||||
export type Emoji = Required<PartialEmoji>
|
||||
|
||||
export const emojis: Emoji[] = data.e
|
||||
export const categories = data.c
|
||||
|
||||
|
@ -51,6 +54,27 @@ export function search(query: string, sorted = false, prev?: Emoji[]): Emoji[] {
|
|||
return (sorted ? filterAndSort : filter)(prev ?? emojis, query)
|
||||
}
|
||||
|
||||
export function emojiToMarkdown(emoji: PartialEmoji): string {
|
||||
if (emoji.u.startsWith("mxc://")) {
|
||||
return `<img data-mx-emoticon src="${emoji.u}" alt=":${emoji.n}:" title=":${emoji.n}:"/>`
|
||||
}
|
||||
return emoji.u
|
||||
}
|
||||
|
||||
export function emojiToReactionContent(emoji: PartialEmoji, evtID: EventID): ReactionEventContent {
|
||||
const content: ReactionEventContent = {
|
||||
"m.relates_to": {
|
||||
rel_type: "m.annotation",
|
||||
event_id: evtID,
|
||||
key: emoji.u,
|
||||
},
|
||||
}
|
||||
if (emoji.u?.startsWith("mxc://") && emoji.n) {
|
||||
content["com.beeper.emoji.shortcode"] = emoji.n
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
interface filteredEmojiCache {
|
||||
query: string
|
||||
result: Emoji[]
|
||||
|
|
Loading…
Add table
Reference in a new issue