Private
Public Access
Add emoji picker and message reactions (Gitea issue #11)
Composer gains an emoji picker (static curated unicode list, insert at cursor position) and messages gain Slack/Mattermost-style reactions: react with any emoji, toggle off by reacting again, see who reacted via a tooltip on each pill. Backend: MessageReaction model (unique on message_id+user_id+emoji backs toggle semantics), WS "reaction" envelope broadcasts the full recomputed reaction list per message (same approach as message edits), REST message list embeds reactions so a reload doesn't lose state that only arrived over WS. Frontend: shared EmojiPicker component (anchored popover, Escape/outside- click dismiss via new useEscapeKey hook) used by both the composer and a new hover-revealed reaction trigger on each message row.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useRef, useState, type ChangeEvent, type KeyboardEvent } from 'react'
|
||||
import { useOnlineStatus } from '../hooks/useOnlineStatus'
|
||||
import { uploadRoomImage } from '../api/rooms'
|
||||
import { EmojiPicker } from './EmojiPicker'
|
||||
import './Composer.css'
|
||||
|
||||
interface ComposerProps {
|
||||
@@ -15,6 +16,7 @@ export function Composer({ roomId, roomName, disabled, onSend }: ComposerProps)
|
||||
const [pendingImage, setPendingImage] = useState<{ id: string; previewUrl: string } | null>(null)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [uploadError, setUploadError] = useState<string | null>(null)
|
||||
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const online = useOnlineStatus()
|
||||
@@ -69,6 +71,24 @@ export function Composer({ roomId, roomName, disabled, onSend }: ComposerProps)
|
||||
})
|
||||
}
|
||||
|
||||
function insertEmoji(emoji: string) {
|
||||
const el = textareaRef.current
|
||||
setEmojiPickerOpen(false)
|
||||
if (!el) {
|
||||
setValue((v) => v + emoji)
|
||||
return
|
||||
}
|
||||
const start = el.selectionStart ?? value.length
|
||||
const end = el.selectionEnd ?? value.length
|
||||
setValue(value.slice(0, start) + emoji + value.slice(end))
|
||||
requestAnimationFrame(() => {
|
||||
el.focus()
|
||||
const cursor = start + emoji.length
|
||||
el.setSelectionRange(cursor, cursor)
|
||||
autoGrow()
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="composer">
|
||||
{pendingImage && (
|
||||
@@ -116,6 +136,25 @@ export function Composer({ roomId, roomName, disabled, onSend }: ComposerProps)
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
<div className="composer-emoji-wrap">
|
||||
<button
|
||||
type="button"
|
||||
className="composer-emoji-trigger"
|
||||
onClick={() => setEmojiPickerOpen((v) => !v)}
|
||||
disabled={disabled}
|
||||
aria-label="Insert an emoji"
|
||||
>
|
||||
🙂
|
||||
</button>
|
||||
{emojiPickerOpen && (
|
||||
<EmojiPicker
|
||||
onPick={insertEmoji}
|
||||
onClose={() => setEmojiPickerOpen(false)}
|
||||
placement="above"
|
||||
align="left"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
|
||||
Reference in New Issue
Block a user