Show host emojis when writing stream chat message

This commit is contained in:
hzrd149 2023-08-31 13:55:32 -05:00
parent c10a17eee7
commit 27abb20fd7
5 changed files with 146 additions and 135 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Show host emojis when writing stream chat message

View File

@ -23,9 +23,9 @@ export function DefaultEmojiProvider({ children }: PropsWithChildren) {
return <EmojiProvider emojis={defaultEmojis}>{children}</EmojiProvider>;
}
export function UserEmojiProvider({ children }: PropsWithChildren) {
export function UserEmojiProvider({ children, pubkey }: PropsWithChildren & { pubkey?: string }) {
const account = useCurrentAccount();
const userPacks = useUserEmojiPacks(account?.pubkey);
const userPacks = useUserEmojiPacks(pubkey || account?.pubkey);
const events = useReplaceableEvents(userPacks?.packs);
const emojis = events
@ -34,8 +34,6 @@ export function UserEmojiProvider({ children }: PropsWithChildren) {
)
.flat();
console.log(userPacks, emojis);
return <EmojiProvider emojis={emojis}>{children}</EmojiProvider>;
}

View File

@ -35,6 +35,7 @@ import RelaySelectionProvider from "../../../providers/relay-selection-provider"
import StreamerCards from "../components/streamer-cards";
import { useAppTitle } from "../../../hooks/use-app-title";
import StreamSatsPerMinute from "../components/stream-sats-per-minute";
import { UserEmojiProvider } from "../../../providers/emoji-provider";
function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode?: ChatDisplayMode }) {
useAppTitle(stream.title);
@ -194,7 +195,9 @@ export default function StreamView() {
return (
// add snort and damus relays so zap.stream will always see zaps
<RelaySelectionProvider additionalDefaults={streamRelays}>
<StreamPage stream={stream} displayMode={(params.get("displayMode") as ChatDisplayMode) ?? undefined} />
<UserEmojiProvider pubkey={stream.host}>
<StreamPage stream={stream} displayMode={(params.get("displayMode") as ChatDisplayMode) ?? undefined} />
</UserEmojiProvider>
</RelaySelectionProvider>
);
}

View File

@ -0,0 +1,95 @@
import { useMemo } from "react";
import { Box, Button, IconButton, useDisclosure, useToast } from "@chakra-ui/react";
import { useForm } from "react-hook-form";
import { ParsedStream, buildChatMessage } from "../../../../helpers/nostr/stream";
import { useRelaySelectionRelays } from "../../../../providers/relay-selection-provider";
import { useUserRelays } from "../../../../hooks/use-user-relays";
import { RelayMode } from "../../../../classes/relay";
import { unique } from "../../../../helpers/array";
import { LightningIcon } from "../../../../components/icons";
import useUserLNURLMetadata from "../../../../hooks/use-user-lnurl-metadata";
import ZapModal from "../../../../components/zap-modal";
import { useInvoiceModalContext } from "../../../../providers/invoice-modal";
import { useSigningContext } from "../../../../providers/signing-provider";
import NostrPublishAction from "../../../../classes/nostr-publish-action";
import { createEmojiTags, ensureNotifyContentMentions } from "../../../../helpers/nostr/post";
import { useContextEmojis } from "../../../../providers/emoji-provider";
import MagicTextArea from "../../../../components/magic-textarea";
export default function ChatMessageForm({ stream }: { stream: ParsedStream }) {
const toast = useToast();
const emojis = useContextEmojis();
const streamRelays = useRelaySelectionRelays();
const hostReadRelays = useUserRelays(stream.host)
.filter((r) => r.mode & RelayMode.READ)
.map((r) => r.url);
const relays = useMemo(() => unique([...streamRelays, ...hostReadRelays]), [hostReadRelays, streamRelays]);
const { requestSignature } = useSigningContext();
const { setValue, handleSubmit, formState, reset, getValues, watch } = useForm({
defaultValues: { content: "" },
});
const sendMessage = handleSubmit(async (values) => {
try {
let draft = buildChatMessage(stream, values.content);
draft = ensureNotifyContentMentions(draft);
draft = createEmojiTags(draft, emojis);
const signed = await requestSignature(draft);
new NostrPublishAction("Send Chat", relays, signed);
reset();
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
});
const { requestPay } = useInvoiceModalContext();
const zapModal = useDisclosure();
const zapMetadata = useUserLNURLMetadata(stream.host);
watch("content");
return (
<>
<Box as="form" borderRadius="md" flexShrink={0} display="flex" gap="2" px="2" pb="2" onSubmit={sendMessage}>
<MagicTextArea
placeholder="Message"
autoComplete="off"
isRequired
value={getValues().content}
onChange={(e) => setValue("content", e.target.value)}
rows={1}
/>
<Button colorScheme="brand" type="submit" isLoading={formState.isSubmitting}>
Send
</Button>
{zapMetadata.metadata?.allowsNostr && (
<IconButton
icon={<LightningIcon color="yellow.400" />}
aria-label="Zap stream"
borderColor="yellow.400"
variant="outline"
onClick={zapModal.onOpen}
/>
)}
</Box>
{zapModal.isOpen && (
<ZapModal
isOpen
stream={stream}
pubkey={stream.host}
onInvoice={async (invoice) => {
reset();
zapModal.onClose();
await requestPay(invoice);
}}
onClose={zapModal.onClose}
initialComment={getValues().content}
additionalRelays={relays}
/>
)}
</>
);
}

View File

@ -1,47 +1,24 @@
import { useCallback, useMemo, useRef } from "react";
import {
Box,
Button,
Card,
CardBody,
CardHeader,
CardProps,
Flex,
Heading,
IconButton,
Input,
useDisclosure,
useToast,
} from "@chakra-ui/react";
import { Card, CardBody, CardHeader, CardProps, Flex, Heading } from "@chakra-ui/react";
import { css } from "@emotion/react";
import { Kind } from "nostr-tools";
import { ParsedStream, STREAM_CHAT_MESSAGE_KIND, buildChatMessage, getATag } from "../../../../helpers/nostr/stream";
import { useUserRelays } from "../../../../hooks/use-user-relays";
import { RelayMode } from "../../../../classes/relay";
import ZapModal from "../../../../components/zap-modal";
import { LightningIcon } from "../../../../components/icons";
import { ParsedStream, STREAM_CHAT_MESSAGE_KIND, getATag } from "../../../../helpers/nostr/stream";
import ChatMessage from "./chat-message";
import ZapMessage from "./zap-message";
import { LightboxProvider } from "../../../../components/lightbox-provider";
import IntersectionObserverProvider from "../../../../providers/intersection-observer";
import useUserLNURLMetadata from "../../../../hooks/use-user-lnurl-metadata";
import { useInvoiceModalContext } from "../../../../providers/invoice-modal";
import { unique } from "../../../../helpers/array";
import { useForm } from "react-hook-form";
import { useSigningContext } from "../../../../providers/signing-provider";
import { useTimelineCurserIntersectionCallback } from "../../../../hooks/use-timeline-cursor-intersection-callback";
import useSubject from "../../../../hooks/use-subject";
import useTimelineLoader from "../../../../hooks/use-timeline-loader";
import { truncatedId } from "../../../../helpers/nostr/events";
import { css } from "@emotion/react";
import TopZappers from "./top-zappers";
import { parseZapEvent } from "../../../../helpers/zaps";
import { Kind } from "nostr-tools";
import { useRelaySelectionRelays } from "../../../../providers/relay-selection-provider";
import useUserMuteList from "../../../../hooks/use-user-mute-list";
import { NostrEvent, isPTag } from "../../../../types/nostr-event";
import { useCurrentAccount } from "../../../../hooks/use-current-account";
import NostrPublishAction from "../../../../classes/nostr-publish-action";
import { ensureNotifyContentMentions } from "../../../../helpers/nostr/post";
import ChatMessageForm from "./chat-message-form";
const hideScrollbar = css`
scrollbar-width: 0;
@ -59,14 +36,8 @@ export default function StreamChat({
displayMode,
...props
}: CardProps & { stream: ParsedStream; actions?: React.ReactNode; displayMode?: ChatDisplayMode }) {
const toast = useToast();
const account = useCurrentAccount();
const streamRelays = useRelaySelectionRelays();
const hostReadRelays = useUserRelays(stream.host)
.filter((r) => r.mode & RelayMode.READ)
.map((r) => r.url);
const relays = useMemo(() => unique([...streamRelays, ...hostReadRelays]), [hostReadRelays, streamRelays]);
const hostMuteList = useUserMuteList(stream.host);
const muteList = useUserMuteList(account?.pubkey);
@ -101,106 +72,45 @@ export default function StreamChat({
const scrollBox = useRef<HTMLDivElement | null>(null);
const callback = useTimelineCurserIntersectionCallback(timeline);
const { requestSignature } = useSigningContext();
const { register, handleSubmit, formState, reset, getValues } = useForm({
defaultValues: { content: "" },
});
const sendMessage = handleSubmit(async (values) => {
try {
const draft = buildChatMessage(stream, values.content);
const signed = await requestSignature(draft);
new NostrPublishAction("Send Chat", relays, signed);
reset();
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
});
const zapModal = useDisclosure();
const { requestPay } = useInvoiceModalContext();
const zapMetadata = useUserLNURLMetadata(stream.host);
const isPopup = !!displayMode;
const isChatLog = displayMode === "log";
return (
<>
<IntersectionObserverProvider callback={callback} root={scrollBox}>
<LightboxProvider>
<Card {...props} overflow="hidden" background={isChatLog ? "transparent" : undefined}>
{!isPopup && (
<CardHeader py="3" display="flex" justifyContent="space-between" alignItems="center">
<Heading size="md">Stream Chat</Heading>
{actions}
</CardHeader>
)}
<CardBody display="flex" flexDirection="column" overflow="hidden" p={0}>
<TopZappers zaps={zaps} pt={!isPopup ? 0 : undefined} />
<Flex
overflowY="scroll"
overflowX="hidden"
ref={scrollBox}
direction="column-reverse"
flex={1}
px="4"
py="2"
mb="2"
gap="2"
css={isChatLog && hideScrollbar}
>
{events.map((event) =>
event.kind === STREAM_CHAT_MESSAGE_KIND ? (
<ChatMessage key={event.id} event={event} stream={stream} />
) : (
<ZapMessage key={event.id} zap={event} stream={stream} />
),
)}
</Flex>
{!isChatLog && (
<Box
as="form"
borderRadius="md"
flexShrink={0}
display="flex"
gap="2"
px="2"
pb="2"
onSubmit={sendMessage}
>
<Input placeholder="Message" {...register("content", { required: true })} autoComplete="off" />
<Button colorScheme="brand" type="submit" isLoading={formState.isSubmitting}>
Send
</Button>
{zapMetadata.metadata?.allowsNostr && (
<IconButton
icon={<LightningIcon color="yellow.400" />}
aria-label="Zap stream"
borderColor="yellow.400"
variant="outline"
onClick={zapModal.onOpen}
/>
)}
</Box>
<IntersectionObserverProvider callback={callback} root={scrollBox}>
<LightboxProvider>
<Card {...props} overflow="hidden" background={isChatLog ? "transparent" : undefined}>
{!isPopup && (
<CardHeader py="3" display="flex" justifyContent="space-between" alignItems="center">
<Heading size="md">Stream Chat</Heading>
{actions}
</CardHeader>
)}
<CardBody display="flex" flexDirection="column" overflow="hidden" p={0}>
<TopZappers zaps={zaps} pt={!isPopup ? 0 : undefined} />
<Flex
overflowY="scroll"
overflowX="hidden"
ref={scrollBox}
direction="column-reverse"
flex={1}
px="4"
py="2"
mb="2"
gap="2"
css={isChatLog && hideScrollbar}
>
{events.map((event) =>
event.kind === STREAM_CHAT_MESSAGE_KIND ? (
<ChatMessage key={event.id} event={event} stream={stream} />
) : (
<ZapMessage key={event.id} zap={event} stream={stream} />
),
)}
</CardBody>
</Card>
</LightboxProvider>
</IntersectionObserverProvider>
{zapModal.isOpen && (
<ZapModal
isOpen
stream={stream}
pubkey={stream.host}
onInvoice={async (invoice) => {
reset();
zapModal.onClose();
await requestPay(invoice);
}}
onClose={zapModal.onClose}
initialComment={getValues().content}
additionalRelays={relays}
/>
)}
</>
</Flex>
{!isChatLog && <ChatMessageForm stream={stream} />}
</CardBody>
</Card>
</LightboxProvider>
</IntersectionObserverProvider>
);
}