feat: custom emoji autocomplete

This commit is contained in:
Alejandro Gomez
2023-07-03 18:27:44 +02:00
parent a023ba93bf
commit 9f5875f175
3 changed files with 181 additions and 52 deletions

View File

@ -262,7 +262,7 @@
border: none; border: none;
cursor: pointer; cursor: pointer;
height: 24px; height: 24px;
padding: 0px 4px; padding: 4px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 2px; gap: 2px;
@ -315,3 +315,26 @@
font-weight: 500; font-weight: 500;
line-height: 18px; line-height: 18px;
} }
.message-composer {
display: flex;
flex-direction: column;
}
.write-message-container {
display: flex;
align-items: center;
gap: 8px;
}
.write-message-container .paper {
flex: 1;
}
.write-emoji-button {
color: #FFFFFF80;
cursor: pointer;
}
.write-emoji-button:hover {
color: white;
}

View File

@ -14,13 +14,13 @@ import {
useRef, useRef,
type KeyboardEvent, type KeyboardEvent,
type ChangeEvent, type ChangeEvent,
type LegacyRef, type RefObject,
} from "react"; } from "react";
import { useHover, useOnClickOutside, useMediaQuery } from "usehooks-ts"; import { useHover, useOnClickOutside, useMediaQuery } from "usehooks-ts";
import data from "@emoji-mart/data"; import data from "@emoji-mart/data";
import Picker from "@emoji-mart/react"; import Picker from "@emoji-mart/react";
import useEmoji from "hooks/emoji"; import useEmoji, { type EmojiPack } from "hooks/emoji";
import { System } from "index"; import { System } from "index";
import { useLiveChatFeed } from "hooks/live-chat"; import { useLiveChatFeed } from "hooks/live-chat";
import AsyncButton from "./async-button"; import AsyncButton from "./async-button";
@ -37,6 +37,71 @@ import useTopZappers from "hooks/top-zappers";
import { LIVE_STREAM_CHAT } from "const"; import { LIVE_STREAM_CHAT } from "const";
import { findTag } from "utils"; import { findTag } from "utils";
interface EmojiPickerProps {
topOffset: number;
leftOffset: number;
emojiPacks?: EmojiPack[];
onEmojiSelect: (e: Emoji) => void;
onClickOutside: () => void;
height?: number;
ref: RefObject<HTMLDivElement>;
}
function EmojiPicker({
topOffset,
leftOffset,
onEmojiSelect,
onClickOutside,
emojiPacks = [],
height = 300,
ref,
}: EmojiPickerProps) {
const customEmojiList = emojiPacks.map((pack) => {
return {
id: pack.address,
name: pack.name,
emojis: pack.emojis.map((e) => {
const [, name, url] = e;
return {
id: name,
name,
skins: [{ src: url }],
};
}),
};
});
return (
<>
<div
style={{
position: "fixed",
top: topOffset - height - 10,
left: leftOffset,
zIndex: 1,
}}
ref={ref}
>
<style>
{`
em-emoji-picker { max-height: ${height}px; }
`}
</style>
<Picker
data={data}
custom={customEmojiList}
perLine={7}
previewPosition="none"
skinTonePosition="search"
theme="dark"
onEmojiSelect={onEmojiSelect}
onClickOutside={onClickOutside}
maxFrequentRows={0}
/>
</div>
</>
);
}
export interface LiveChatOptions { export interface LiveChatOptions {
canWrite?: boolean; canWrite?: boolean;
showHeader?: boolean; showHeader?: boolean;
@ -144,7 +209,8 @@ function emojifyReaction(reaction: string) {
} }
interface Emoji { interface Emoji {
native: string; id: string;
native?: string;
} }
function ChatMessage({ function ChatMessage({
@ -198,7 +264,7 @@ function ChatMessage({
const pub = await EventPublisher.nip7(); const pub = await EventPublisher.nip7();
const reply = await pub?.generic((eb) => { const reply = await pub?.generic((eb) => {
eb.kind(EventKind.Reaction) eb.kind(EventKind.Reaction)
.content(emoji.native) .content(emoji.native || "+1")
.tag(["e", ev.id]) .tag(["e", ev.id])
.tag(["p", ev.pubkey]); .tag(["p", ev.pubkey]);
return eb; return eb;
@ -215,6 +281,11 @@ function ChatMessage({
// @ts-expect-error // @ts-expect-error
const leftOffset = ref.current?.getBoundingClientRect().left; const leftOffset = ref.current?.getBoundingClientRect().left;
function pickEmoji(ev: any) {
ev.stopPropagation();
setShowEmojiPicker(!showEmojiPicker);
}
return ( return (
<> <>
<div <div
@ -274,39 +345,20 @@ function ChatMessage({
targetName={profile?.name || ev.pubkey} targetName={profile?.name || ev.pubkey}
/> />
)} )}
<button <button className="message-zap-button" onClick={pickEmoji}>
className="message-zap-button"
onClick={() => setShowEmojiPicker(true)}
>
<Icon name="face" className="message-zap-button-icon" /> <Icon name="face" className="message-zap-button-icon" />
</button> </button>
</div> </div>
)} )}
</div> </div>
{showEmojiPicker && ( {showEmojiPicker && (
<div <EmojiPicker
style={{ topOffset={topOffset}
position: "fixed", leftOffset={leftOffset}
top: topOffset - 310, onEmojiSelect={onEmojiSelect}
left: leftOffset, onClickOutside={() => setShowEmojiPicker(false)}
zIndex: 1,
}}
ref={emojiRef} ref={emojiRef}
> />
<style>
{`
em-emoji-picker { max-height: 300px; }
`}
</style>
<Picker
data={data}
perLine={7}
previewPosition="none"
skinTonePosition="search"
theme="dark"
onEmojiSelect={onEmojiSelect}
/>
</div>
)} )}
</> </>
); );
@ -353,12 +405,22 @@ function ChatZap({ streamer, ev }: { streamer: string; ev: TaggedRawEvent }) {
} }
function WriteMessage({ link }: { link: NostrLink }) { function WriteMessage({ link }: { link: NostrLink }) {
const ref = useRef(null);
const emojiRef = useRef(null);
const [chat, setChat] = useState(""); const [chat, setChat] = useState("");
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const login = useLogin(); const login = useLogin();
const userEmojis = useEmoji(login!.pubkey); const userEmojiPacks = useEmoji(login!.pubkey);
const channelEmojis = useEmoji(link.author!); const userEmojis = userEmojiPacks.map((pack) => pack.emojis).flat();
const channelEmojiPacks = useEmoji(link.author!);
const channelEmojis = channelEmojiPacks.map((pack) => pack.emojis).flat();
const emojis = userEmojis.concat(channelEmojis); const emojis = userEmojis.concat(channelEmojis);
const names = emojis.map((t) => t.at(1)); const names = emojis.map((t) => t.at(1));
const allEmojiPacks = userEmojiPacks.concat(channelEmojiPacks);
// @ts-expect-error
const topOffset = ref.current?.getBoundingClientRect().top;
// @ts-expect-error
const leftOffset = ref.current?.getBoundingClientRect().left;
async function sendChatMessage() { async function sendChatMessage() {
const pub = await EventPublisher.nip7(); const pub = await EventPublisher.nip7();
@ -394,6 +456,15 @@ function WriteMessage({ link }: { link: NostrLink }) {
} }
} }
function onEmojiSelect(emoji: Emoji) {
if (emoji.native) {
setChat(`${chat}${emoji.native}`);
} else {
setChat(`${chat}:${emoji.id}:`);
}
setShowEmojiPicker(false);
}
async function onKeyDown(e: KeyboardEvent) { async function onKeyDown(e: KeyboardEvent) {
if (e.code === "Enter") { if (e.code === "Enter") {
e.preventDefault(); e.preventDefault();
@ -406,15 +477,33 @@ function WriteMessage({ link }: { link: NostrLink }) {
setChat(e.target.value); setChat(e.target.value);
} }
function pickEmoji(ev: any) {
ev.stopPropagation();
setShowEmojiPicker(!showEmojiPicker);
}
return ( return (
<> <>
<div className="paper"> <div className="paper" ref={ref}>
<Textarea <Textarea
emojis={emojis} emojis={emojis}
value={chat} value={chat}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
onChange={onChange} onChange={onChange}
/> />
<div onClick={pickEmoji}>
<Icon name="face" className="write-emoji-button" />
</div>
{showEmojiPicker && (
<EmojiPicker
topOffset={topOffset}
leftOffset={leftOffset}
emojiPacks={allEmojiPacks}
onEmojiSelect={onEmojiSelect}
onClickOutside={() => setShowEmojiPicker(false)}
ref={emojiRef}
/>
)}
</div> </div>
<AsyncButton onClick={sendChatMessage} className="btn btn-border"> <AsyncButton onClick={sendChatMessage} className="btn btn-border">
Send Send

View File

@ -1,9 +1,22 @@
import { RequestBuilder, EventKind, FlatNoteStore } from "@snort/system"; import {
RequestBuilder,
EventKind,
ReplaceableNoteStore,
ParameterizedReplaceableNoteStore,
} from "@snort/system";
import { useRequestBuilder } from "@snort/system-react"; import { useRequestBuilder } from "@snort/system-react";
import { System } from "index"; import { System } from "index";
import { useMemo } from "react"; import { useMemo } from "react";
import { findTag } from "utils";
import type { EmojiTag } from "../element/emoji"; import type { EmojiTag } from "../element/emoji";
export interface EmojiPack {
address: string;
name: string;
author: string;
emojis: EmojiTag[];
}
export default function useEmoji(pubkey: string) { export default function useEmoji(pubkey: string) {
const sub = useMemo(() => { const sub = useMemo(() => {
const rb = new RequestBuilder(`emoji:${pubkey}`); const rb = new RequestBuilder(`emoji:${pubkey}`);
@ -15,15 +28,15 @@ export default function useEmoji(pubkey: string) {
return rb; return rb;
}, [pubkey]); }, [pubkey]);
const { data } = useRequestBuilder<FlatNoteStore>(System, FlatNoteStore, sub); const { data: userEmoji } = useRequestBuilder<ReplaceableNoteStore>(
const userEmoji = useMemo(() => { System,
return data ?? []; ReplaceableNoteStore,
}, [data]); sub
);
const related = useMemo(() => { const related = useMemo(() => {
if (userEmoji) { if (userEmoji) {
const tags = userEmoji.at(0)?.tags ?? []; return userEmoji.tags.filter(
return tags.filter(
(t) => t.at(0) === "a" && t.at(1)?.startsWith(`30030:`) (t) => t.at(0) === "a" && t.at(1)?.startsWith(`30030:`)
); );
} }
@ -52,22 +65,26 @@ export default function useEmoji(pubkey: string) {
return rb; return rb;
}, [pubkey, related]); }, [pubkey, related]);
const { data: relatedData } = useRequestBuilder<FlatNoteStore>( const { data: relatedData } =
System, useRequestBuilder<ParameterizedReplaceableNoteStore>(
FlatNoteStore, System,
subRelated ParameterizedReplaceableNoteStore,
); subRelated
);
const emojiPacks = useMemo(() => { const emojiPacks = useMemo(() => {
return relatedData ?? []; return relatedData ?? [];
}, [relatedData]); }, [relatedData]);
const emojis = useMemo(() => { const emojis = useMemo(() => {
return userEmoji return emojiPacks.map((ev) => {
.concat(emojiPacks) const d = findTag(ev, "d");
.map((ev) => { return {
return ev.tags.filter((t) => t.at(0) === "emoji"); address: `${ev.kind}:${ev.pubkey}:${d}`,
}) name: d,
.flat() as EmojiTag[]; author: ev.pubkey,
emojis: ev.tags.filter((t) => t.at(0) === "emoji") as EmojiTag[],
} as EmojiPack;
});
}, [userEmoji, emojiPacks]); }, [userEmoji, emojiPacks]);
return emojis; return emojis;