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>; return <EmojiProvider emojis={defaultEmojis}>{children}</EmojiProvider>;
} }
export function UserEmojiProvider({ children }: PropsWithChildren) { export function UserEmojiProvider({ children, pubkey }: PropsWithChildren & { pubkey?: string }) {
const account = useCurrentAccount(); const account = useCurrentAccount();
const userPacks = useUserEmojiPacks(account?.pubkey); const userPacks = useUserEmojiPacks(pubkey || account?.pubkey);
const events = useReplaceableEvents(userPacks?.packs); const events = useReplaceableEvents(userPacks?.packs);
const emojis = events const emojis = events
@ -34,8 +34,6 @@ export function UserEmojiProvider({ children }: PropsWithChildren) {
) )
.flat(); .flat();
console.log(userPacks, emojis);
return <EmojiProvider emojis={emojis}>{children}</EmojiProvider>; 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 StreamerCards from "../components/streamer-cards";
import { useAppTitle } from "../../../hooks/use-app-title"; import { useAppTitle } from "../../../hooks/use-app-title";
import StreamSatsPerMinute from "../components/stream-sats-per-minute"; import StreamSatsPerMinute from "../components/stream-sats-per-minute";
import { UserEmojiProvider } from "../../../providers/emoji-provider";
function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode?: ChatDisplayMode }) { function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode?: ChatDisplayMode }) {
useAppTitle(stream.title); useAppTitle(stream.title);
@ -194,7 +195,9 @@ export default function StreamView() {
return ( return (
// add snort and damus relays so zap.stream will always see zaps // add snort and damus relays so zap.stream will always see zaps
<RelaySelectionProvider additionalDefaults={streamRelays}> <RelaySelectionProvider additionalDefaults={streamRelays}>
<UserEmojiProvider pubkey={stream.host}>
<StreamPage stream={stream} displayMode={(params.get("displayMode") as ChatDisplayMode) ?? undefined} /> <StreamPage stream={stream} displayMode={(params.get("displayMode") as ChatDisplayMode) ?? undefined} />
</UserEmojiProvider>
</RelaySelectionProvider> </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 { useCallback, useMemo, useRef } from "react";
import { import { Card, CardBody, CardHeader, CardProps, Flex, Heading } from "@chakra-ui/react";
Box, import { css } from "@emotion/react";
Button, import { Kind } from "nostr-tools";
Card,
CardBody,
CardHeader,
CardProps,
Flex,
Heading,
IconButton,
Input,
useDisclosure,
useToast,
} from "@chakra-ui/react";
import { ParsedStream, STREAM_CHAT_MESSAGE_KIND, buildChatMessage, getATag } from "../../../../helpers/nostr/stream"; import { ParsedStream, STREAM_CHAT_MESSAGE_KIND, 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 ChatMessage from "./chat-message"; import ChatMessage from "./chat-message";
import ZapMessage from "./zap-message"; import ZapMessage from "./zap-message";
import { LightboxProvider } from "../../../../components/lightbox-provider"; import { LightboxProvider } from "../../../../components/lightbox-provider";
import IntersectionObserverProvider from "../../../../providers/intersection-observer"; 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 { useTimelineCurserIntersectionCallback } from "../../../../hooks/use-timeline-cursor-intersection-callback";
import useSubject from "../../../../hooks/use-subject"; import useSubject from "../../../../hooks/use-subject";
import useTimelineLoader from "../../../../hooks/use-timeline-loader"; import useTimelineLoader from "../../../../hooks/use-timeline-loader";
import { truncatedId } from "../../../../helpers/nostr/events"; import { truncatedId } from "../../../../helpers/nostr/events";
import { css } from "@emotion/react";
import TopZappers from "./top-zappers"; import TopZappers from "./top-zappers";
import { parseZapEvent } from "../../../../helpers/zaps"; import { parseZapEvent } from "../../../../helpers/zaps";
import { Kind } from "nostr-tools";
import { useRelaySelectionRelays } from "../../../../providers/relay-selection-provider"; import { useRelaySelectionRelays } from "../../../../providers/relay-selection-provider";
import useUserMuteList from "../../../../hooks/use-user-mute-list"; import useUserMuteList from "../../../../hooks/use-user-mute-list";
import { NostrEvent, isPTag } from "../../../../types/nostr-event"; import { NostrEvent, isPTag } from "../../../../types/nostr-event";
import { useCurrentAccount } from "../../../../hooks/use-current-account"; import { useCurrentAccount } from "../../../../hooks/use-current-account";
import NostrPublishAction from "../../../../classes/nostr-publish-action"; import ChatMessageForm from "./chat-message-form";
import { ensureNotifyContentMentions } from "../../../../helpers/nostr/post";
const hideScrollbar = css` const hideScrollbar = css`
scrollbar-width: 0; scrollbar-width: 0;
@ -59,14 +36,8 @@ export default function StreamChat({
displayMode, displayMode,
...props ...props
}: CardProps & { stream: ParsedStream; actions?: React.ReactNode; displayMode?: ChatDisplayMode }) { }: CardProps & { stream: ParsedStream; actions?: React.ReactNode; displayMode?: ChatDisplayMode }) {
const toast = useToast();
const account = useCurrentAccount(); const account = useCurrentAccount();
const streamRelays = useRelaySelectionRelays(); 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 hostMuteList = useUserMuteList(stream.host);
const muteList = useUserMuteList(account?.pubkey); const muteList = useUserMuteList(account?.pubkey);
@ -101,30 +72,10 @@ export default function StreamChat({
const scrollBox = useRef<HTMLDivElement | null>(null); const scrollBox = useRef<HTMLDivElement | null>(null);
const callback = useTimelineCurserIntersectionCallback(timeline); 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 isPopup = !!displayMode;
const isChatLog = displayMode === "log"; const isChatLog = displayMode === "log";
return ( return (
<>
<IntersectionObserverProvider callback={callback} root={scrollBox}> <IntersectionObserverProvider callback={callback} root={scrollBox}>
<LightboxProvider> <LightboxProvider>
<Card {...props} overflow="hidden" background={isChatLog ? "transparent" : undefined}> <Card {...props} overflow="hidden" background={isChatLog ? "transparent" : undefined}>
@ -156,51 +107,10 @@ export default function StreamChat({
), ),
)} )}
</Flex> </Flex>
{!isChatLog && ( {!isChatLog && <ChatMessageForm stream={stream} />}
<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>
)}
</CardBody> </CardBody>
</Card> </Card>
</LightboxProvider> </LightboxProvider>
</IntersectionObserverProvider> </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}
/>
)}
</>
); );
} }