Compare commits

...

8 Commits
main ... misc

Author SHA1 Message Date
Alejandro Gomez
73162efe63
fix: get profile zaps in live feed 2023-07-09 17:08:41 +02:00
Alejandro Gomez
e13e3bccf6
fix: only trigger confetti when watching live 2023-07-09 01:11:45 +02:00
Alejandro Gomez
aaa2e26bd9
fix: tags wrapping 2023-07-09 01:05:40 +02:00
Alejandro Gomez
d98440b47b
fix: show all zappers 2023-07-09 01:04:08 +02:00
Alejandro Gomez
12366bdf2c
fix: chat height 2023-07-09 01:04:01 +02:00
Alejandro Gomez
6dd6c6324f
fix: show channel emoji first 2023-07-09 00:57:04 +02:00
Alejandro Gomez
c30ebbb973
fix: hide top zappers header on mobile 2023-07-09 00:00:23 +02:00
Alejandro Gomez
040d2252ab
fix: adjust chat height on mobile 2023-07-08 23:59:53 +02:00
12 changed files with 218 additions and 156 deletions

View File

@ -20,6 +20,7 @@
"emoji-mart": "^5.5.2",
"hls.js": "^1.4.6",
"lodash": "^4.17.21",
"lodash.uniqby": "^4.7.0",
"moment": "^2.29.4",
"qr-code-styling": "^1.6.0-rc.1",
"react": "^18.2.0",
@ -65,6 +66,7 @@
"@formatjs/cli": "^6.0.1",
"@formatjs/ts-transformer": "^3.13.1",
"@types/lodash": "^4.14.195",
"@types/lodash.uniqby": "^4.7.7",
"@webbtc/webln-types": "^1.0.12",
"babel-loader": "^9.1.2",
"babel-plugin-formatjs": "^10.5.1",

View File

@ -6,6 +6,7 @@ import { NostrLink, ParsedZap, NostrEvent } from "@snort/system";
import { Icon } from "./icon";
import { findTag } from "utils";
import { formatSats } from "number";
import usePreviousValue from "hooks/usePreviousValue";
export function Goal({
link,
@ -33,6 +34,7 @@ export function Goal({
const progress = (soFar / goalAmount) * 100;
const isFinished = progress >= 100;
const previousValue = usePreviousValue(isFinished);
return (
<div className="goal">
@ -56,7 +58,9 @@ export function Goal({
/>
</div>
</div>
{isFinished && <Confetti numberOfPieces={2100} recycle={false} />}
{isFinished && previousValue === false && (
<Confetti numberOfPieces={2100} recycle={false} />
)}
</div>
);
}

View File

@ -4,7 +4,7 @@
flex-direction: column;
padding: 8px 16px;
border: none;
height: calc(100vh - 56px - 64px - 56px - 230px);
height: calc(100vh - 56px - 64px - 36px - 230px);
}
@media (min-width: 768px) {
@ -47,8 +47,14 @@
}
.live-chat > .header {
display: flex;
justify-content: space-between
display: none;
}
@media (min-width: 768px){
.live-chat > .header {
display: flex;
justify-content: space-between
}
}
.live-chat .header .title {
@ -159,6 +165,12 @@
font-weight: 600;
}
@media (max-width: 768px) {
.top-zappers h3 {
display: none;
}
}
.top-zappers-container {
display: flex;
overflow-y: scroll;

View File

@ -31,7 +31,7 @@ export interface LiveChatOptions {
}
function TopZappers({ zaps }: { zaps: ParsedZap[] }) {
const zappers = useTopZappers(zaps).slice(0, 3);
const zappers = useTopZappers(zaps);
return (
<>
@ -65,7 +65,8 @@ export function LiveChat({
options?: LiveChatOptions;
height?: number;
}) {
const feed = useLiveChatFeed(link);
const host = getHost(ev);
const feed = useLiveChatFeed(link, host);
const login = useLogin();
useEffect(() => {
const pubkeys = [

View File

@ -1,8 +1,9 @@
import {NostrLink, EventPublisher, EventKind} from "@snort/system";
import { useRef, useState, ChangeEvent } from "react";
import { NostrLink, EventPublisher, EventKind } from "@snort/system";
import { useRef, useState, useMemo, ChangeEvent } from "react";
import uniqBy from "lodash.uniqby";
import { LIVE_STREAM_CHAT } from "../const";
import useEmoji from "../hooks/emoji";
import useEmoji, { packId } from "../hooks/emoji";
import { useLogin } from "../hooks/login";
import { System } from "../index";
import AsyncButton from "./async-button";
@ -11,115 +12,116 @@ import { Textarea } from "./textarea";
import { EmojiPicker } from "./emoji-picker";
interface Emoji {
id: string;
native?: string;
id: string;
native?: string;
}
export function WriteMessage({ link }: { link: NostrLink }) {
const ref = useRef(null);
const emojiRef = useRef(null);
const [chat, setChat] = useState("");
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const login = useLogin();
const userEmojiPacks = useEmoji(login!.pubkey);
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 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() {
const pub = await EventPublisher.nip7();
if (chat.length > 1) {
let emojiNames = new Set();
for (const name of names) {
if (chat.includes(`:${name}:`)) {
emojiNames.add(name);
const ref = useRef(null);
const emojiRef = useRef(null);
const [chat, setChat] = useState("");
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const login = useLogin();
const userEmojiPacks = useEmoji(login!.pubkey);
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 names = emojis.map((t) => t.at(1));
const allEmojiPacks = useMemo(() => {
return uniqBy(channelEmojiPacks.concat(userEmojiPacks), packId);
}, [userEmojiPacks, channelEmojiPacks]);
// @ts-expect-error
const topOffset = ref.current?.getBoundingClientRect().top;
// @ts-expect-error
const leftOffset = ref.current?.getBoundingClientRect().left;
async function sendChatMessage() {
const pub = await EventPublisher.nip7();
if (chat.length > 1) {
let emojiNames = new Set();
for (const name of names) {
if (chat.includes(`:${name}:`)) {
emojiNames.add(name);
}
}
const reply = await pub?.generic((eb) => {
const emoji = [...emojiNames].map((name) =>
emojis.find((e) => e.at(1) === name)
);
eb.kind(LIVE_STREAM_CHAT as EventKind)
.content(chat)
.tag(["a", `${link.kind}:${link.author}:${link.id}`, "", "root"])
.processContent();
for (const e of emoji) {
if (e) {
eb.tag(e);
}
}
const reply = await pub?.generic((eb) => {
const emoji = [...emojiNames].map((name) =>
emojis.find((e) => e.at(1) === name)
);
eb.kind(LIVE_STREAM_CHAT as EventKind)
.content(chat)
.tag(["a", `${link.kind}:${link.author}:${link.id}`, "", "root"])
.processContent();
for (const e of emoji) {
if (e) {
eb.tag(e);
}
}
return eb;
});
if (reply) {
console.debug(reply);
System.BroadcastEvent(reply);
}
setChat("");
return eb;
});
if (reply) {
console.debug(reply);
System.BroadcastEvent(reply);
}
setChat("");
}
function onEmojiSelect(emoji: Emoji) {
if (emoji.native) {
setChat(`${chat}${emoji.native}`);
} else {
setChat(`${chat}:${emoji.id}:`);
}
setShowEmojiPicker(false);
}
async function onKeyDown(e: React.KeyboardEvent) {
if (e.code === "Enter") {
e.preventDefault();
await sendChatMessage();
}
}
async function onChange(e: ChangeEvent) {
// @ts-expect-error
setChat(e.target.value);
}
function pickEmoji(ev: any) {
ev.stopPropagation();
setShowEmojiPicker(!showEmojiPicker);
}
return (
<>
<div className="paper" ref={ref}>
<Textarea
emojis={emojis}
value={chat}
onKeyDown={onKeyDown}
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>
<AsyncButton onClick={sendChatMessage} className="btn btn-border">
Send
</AsyncButton>
</>
);
}
function onEmojiSelect(emoji: Emoji) {
if (emoji.native) {
setChat(`${chat}${emoji.native}`);
} else {
setChat(`${chat}:${emoji.id}:`);
}
setShowEmojiPicker(false);
}
async function onKeyDown(e: React.KeyboardEvent) {
if (e.code === "Enter") {
e.preventDefault();
await sendChatMessage();
}
}
async function onChange(e: ChangeEvent) {
// @ts-expect-error
setChat(e.target.value);
}
function pickEmoji(ev: any) {
ev.stopPropagation();
setShowEmojiPicker(!showEmojiPicker);
}
return (
<>
<div className="paper" ref={ref}>
<Textarea
emojis={emojis}
value={chat}
onKeyDown={onKeyDown}
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>
<AsyncButton onClick={sendChatMessage} className="btn btn-border">
Send
</AsyncButton>
</>
);
}

View File

@ -3,12 +3,14 @@ import {
EventKind,
ReplaceableNoteStore,
NoteCollection,
NostrEvent,
} from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { System } from "index";
import { useMemo } from "react";
import { findTag } from "utils";
import type { EmojiTag } from "../element/emoji";
import uniqBy from "lodash.uniqby";
export interface EmojiPack {
address: string;
@ -17,6 +19,20 @@ export interface EmojiPack {
emojis: EmojiTag[];
}
function toEmojiPack(ev: NostrEvent): EmojiPack {
const d = findTag(ev, "d") || "";
return {
address: `${ev.kind}:${ev.pubkey}:${d}`,
name: d,
author: ev.pubkey,
emojis: ev.tags.filter((t) => t.at(0) === "emoji") as EmojiTag[],
};
}
export function packId(pack: EmojiPack): string {
return `${pack.author}:${pack.name}`;
}
export default function useEmoji(pubkey: string) {
const sub = useMemo(() => {
const rb = new RequestBuilder(`emoji:${pubkey}`);
@ -61,31 +77,26 @@ export default function useEmoji(pubkey: string) {
.authors(authors)
.tag("d", identifiers);
rb.withFilter()
.kinds([30030 as EventKind])
.authors([pubkey]);
return rb;
}, [pubkey, related]);
const { data: relatedData } =
useRequestBuilder<NoteCollection>(
System,
NoteCollection,
subRelated
);
const { data: relatedData } = useRequestBuilder<NoteCollection>(
System,
NoteCollection,
subRelated
);
const emojiPacks = useMemo(() => {
return relatedData ?? [];
}, [relatedData]);
const emojis = useMemo(() => {
return emojiPacks.map((ev) => {
const d = findTag(ev, "d");
return {
address: `${ev.kind}:${ev.pubkey}:${d}`,
name: d,
author: ev.pubkey,
emojis: ev.tags.filter((t) => t.at(0) === "emoji") as EmojiTag[],
} as EmojiPack;
});
}, [userEmoji, emojiPacks]);
return uniqBy(emojiPacks.map(toEmojiPack), packId);
}, [emojiPacks]);
return emojis;
}

View File

@ -9,22 +9,20 @@ import { System } from "index";
import { useMemo } from "react";
import { LIVE_STREAM_CHAT } from "const";
export function useLiveChatFeed(link: NostrLink) {
export function useLiveChatFeed(link: NostrLink, host?: string) {
const sub = useMemo(() => {
const rb = new RequestBuilder(`live:${link.id}:${link.author}`);
rb.withOptions({
leaveOpen: true,
});
const aTag = `${link.kind}:${link.author}:${link.id}`;
rb.withFilter()
.kinds([LIVE_STREAM_CHAT])
.tag("a", [aTag])
.limit(100);
rb.withFilter()
.kinds([EventKind.ZapReceipt])
.tag("a", [aTag]);
rb.withFilter().kinds([LIVE_STREAM_CHAT]).tag("a", [aTag]).limit(100);
rb.withFilter().kinds([EventKind.ZapReceipt]).tag("a", [aTag]);
if (host) {
rb.withFilter().kinds([EventKind.ZapReceipt]).tag("p", [host]);
}
return rb;
}, [link]);
}, [link, host]);
const feed = useRequestBuilder<FlatNoteStore>(System, FlatNoteStore, sub);

View File

@ -0,0 +1,20 @@
import { useEffect, useRef } from "react";
/**
* On each render returns the previous value of the given variable/constant.
*/
const usePreviousValue = <TValue>(value?: TValue): TValue | undefined => {
const prevValue = useRef<TValue>();
useEffect(() => {
prevValue.current = value;
return () => {
prevValue.current = undefined;
};
});
return prevValue.current;
};
export default usePreviousValue;

View File

@ -205,6 +205,7 @@ button span.hide-on-mobile {
.tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
@ -242,4 +243,4 @@ button span.hide-on-mobile {
.age-check .btn {
padding: 12px 16px;
}
}

View File

@ -150,4 +150,4 @@
.offline>video {
z-index: -1;
position: relative;
}
}

View File

@ -47,21 +47,20 @@ function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedRawEvent }) {
<div className="f-grow stream-info">
<h1>{findTag(ev, "title")}</h1>
<p>{findTag(ev, "summary")}</p>
{ev && (
<Tags ev={ev}>
<StatePill state={status as StreamState} />
{viewers > 0 && (
<span className="pill viewers">
{formatSats(viewers)} viewers
</span>
)}
{status === StreamState.Live && (
<span className="pill">
<StreamTimer ev={ev} />
</span>
)}
</Tags>
)}
<div className="tags">
<StatePill state={status as StreamState} />
{viewers > 0 && (
<span className="pill viewers">
{formatSats(viewers)} viewers
</span>
)}
{status === StreamState.Live && (
<span className="pill">
<StreamTimer ev={ev} />
</span>
)}
{ev && <Tags ev={ev} />}
</div>
{isMine && (
<div className="actions">
{ev && <NewStreamDialog text="Edit" ev={ev} btnClassName="btn" />}

View File

@ -1827,7 +1827,14 @@
resolved "https://registry.yarnpkg.com/@types/json-stable-stringify/-/json-stable-stringify-1.0.34.tgz#c0fb25e4d957e0ee2e497c1f553d7f8bb668fd75"
integrity sha512-s2cfwagOQAS8o06TcwKfr9Wx11dNGbH2E9vJz1cqV+a/LOyhWNLUNd6JSRYNzvB4d29UuJX2M0Dj9vE1T8fRXw==
"@types/lodash@^4.14.195":
"@types/lodash.uniqby@^4.7.7":
version "4.7.7"
resolved "https://registry.yarnpkg.com/@types/lodash.uniqby/-/lodash.uniqby-4.7.7.tgz#48dbb652c41cc8fb30aa61a44174368081835ab5"
integrity sha512-sv2g6vkCIvEUsK5/Vq17haoZaisfj2EWW8mP7QWlnKi6dByoNmeuHDDXHR7sabuDqwO4gvU7ModIL22MmnOocg==
dependencies:
"@types/lodash" "*"
"@types/lodash@*", "@types/lodash@^4.14.195":
version "4.14.195"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632"
integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==
@ -4558,6 +4565,11 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
lodash.uniqby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302"
integrity sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==
lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"