Allow sending emojis in the chat
Add an emoji picker to the dashboard reply composer and let generated replies use emoji. The picker offers a short hand-picked set rather than a full Unicode index, and inserts at the caret so the operator can keep typing without reaching for the mouse again. Insertion respects the Reddit length cap. The chat reply system prompt now permits at most one or two emoji per message, only where someone texting casually would use one. Scoped to ongoing replies — openers build from configurable prompts, so hardcoding guidance there would fight the operator's own settings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2447563c60
commit
b5304e24e3
5 files changed
+118
-1
No files matched your search
@@ -33,6 +33,17 @@ const AT_BOTTOM_THRESHOLD_PX = 48;
|
||||
const UNREAD_DIVIDER_HEADROOM_PX = 12;
|
||||
/** Grace period before an open conversation is considered read. */
|
||||
export const AUTO_MARK_READ_DELAY_MS = 1_000;
|
||||
/**
|
||||
* Emoji offered by the composer picker. Deliberately a short hand-picked set
|
||||
* rather than a full Unicode index — the operator wants the handful that come
|
||||
* up in casual chat, not a searchable catalogue.
|
||||
*/
|
||||
export const COMPOSER_EMOJI = [
|
||||
'😊', '😄', '😉', '😅', '😂', '🙃',
|
||||
'😍', '🥰', '😘', '😏', '😈', '🥵',
|
||||
'🙈', '👀', '🔥', '✨', '❤️', '💜',
|
||||
'👋', '🙌', '🤔', '😴', '😇', '🥺',
|
||||
] as const;
|
||||
|
||||
interface ConversationDetailProps {
|
||||
conversationId: string;
|
||||
@@ -207,8 +218,10 @@ function ConversationComposer({ conversation, conversationId }: { conversation:
|
||||
const [isMediaShareOpen, setIsMediaShareOpen] = useState(false);
|
||||
const [isConfirmingMedia, setIsConfirmingMedia] = useState(false);
|
||||
const [wasMediaShared, setWasMediaShared] = useState(false);
|
||||
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false);
|
||||
const sendButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const mediaShareSummaryRef = useRef<HTMLElement>(null);
|
||||
const replyTextareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const suggestionMutation = useSuggestReplyMutation(conversationId);
|
||||
const mediaAskMutation = useSuggestMediaAskMutation(conversationId);
|
||||
const sendMutation = useSendReplyMutation(conversationId);
|
||||
@@ -247,6 +260,28 @@ function ConversationComposer({ conversation, conversationId }: { conversation:
|
||||
else setRestorableDraft(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splices an emoji in at the caret (replacing any selection) and leaves the
|
||||
* caret after it, so the operator can keep typing without reaching for the
|
||||
* mouse again. Silently no-ops when the draft is already at the length cap,
|
||||
* which matches how the textarea itself refuses further typing.
|
||||
*/
|
||||
function insertEmoji(emoji: string) {
|
||||
const textarea = replyTextareaRef.current;
|
||||
const start = textarea?.selectionStart ?? draft.length;
|
||||
const end = textarea?.selectionEnd ?? draft.length;
|
||||
const next = draft.slice(0, start) + emoji + draft.slice(end);
|
||||
if (next.length > REDDIT_CHAT_MAX_MESSAGE_LENGTH) return;
|
||||
updateDraft(next);
|
||||
setRestorableDraft(null);
|
||||
setWasSent(false);
|
||||
const caret = start + emoji.length;
|
||||
queueMicrotask(() => {
|
||||
textarea?.focus();
|
||||
textarea?.setSelectionRange(caret, caret);
|
||||
});
|
||||
}
|
||||
|
||||
function suggestReply() {
|
||||
setWasSent(false);
|
||||
suggestionMutation.mutate(undefined, { onSuccess: applySuggestion });
|
||||
@@ -301,9 +336,28 @@ function ConversationComposer({ conversation, conversationId }: { conversation:
|
||||
setWasSent(false);
|
||||
}}
|
||||
placeholder="Write or generate a reply"
|
||||
ref={replyTextareaRef}
|
||||
rows={5}
|
||||
value={draft}
|
||||
/>
|
||||
<div className="conversation-emoji">
|
||||
<button
|
||||
aria-expanded={isEmojiPickerOpen}
|
||||
onClick={() => setIsEmojiPickerOpen((current) => !current)}
|
||||
type="button"
|
||||
>
|
||||
{isEmojiPickerOpen ? 'Hide emoji' : 'Add emoji'}
|
||||
</button>
|
||||
{isEmojiPickerOpen ? (
|
||||
<div aria-label="Emoji" className="conversation-emoji-options" role="group">
|
||||
{COMPOSER_EMOJI.map((emoji) => (
|
||||
<button key={emoji} onClick={() => insertEmoji(emoji)} type="button">
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{restorableDraft !== null ? (
|
||||
<div className="conversation-inline-notice" role="status">
|
||||
<span>The suggestion replaced your draft.</span>
|
||||
|
||||
@@ -147,6 +147,10 @@
|
||||
.conversation-section-heading, .conversation-actions { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
|
||||
.conversation-section-heading h3 { margin: 0; }
|
||||
.conversation-actions > span { color: var(--muted); font-size: .75rem; }
|
||||
.conversation-emoji { display: grid; gap: .5rem; justify-items: start; }
|
||||
.conversation-emoji > button { min-height: 2rem !important; padding: .3rem .65rem !important; }
|
||||
.conversation-emoji-options { display: flex; flex-wrap: wrap; gap: .3rem; padding: .5rem; background: #0b1410; border: 1px solid var(--border); border-radius: .65rem; }
|
||||
.conversation-emoji-options button { min-height: 2.1rem !important; padding: .2rem .4rem !important; font-size: 1.1rem; line-height: 1; }
|
||||
.conversation-char-counter.is-warning { color: var(--warning-text) !important; font-weight: 800; }
|
||||
.conversation-inline-error, .conversation-inline-success, .conversation-inline-notice { padding: .65rem .75rem; border-radius: .55rem; }
|
||||
.conversation-inline-notice { display: flex; align-items: center; justify-content: space-between; gap: .75rem; color: var(--warning-text); background: var(--warning-bg); border: 1px solid var(--warning-border); }
|
||||
|
||||
Reference in new issue
Block a user