From e726a674139b43efcd4123467664a5fb9e56d7cd Mon Sep 17 00:00:00 2001 From: verbiricha Date: Sun, 30 Jul 2023 00:26:16 +0200 Subject: [PATCH 1/7] feat: mute list --- src/login.ts | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/login.ts b/src/login.ts index 1b20352..0b6e35e 100644 --- a/src/login.ts +++ b/src/login.ts @@ -72,6 +72,11 @@ export class LoginStore extends ExternalStore { this.#save(); } + updateSession(s: LoginSession) { + this.#session = s; + this.#save(); + } + takeSnapshot() { return this.#session ? { ...this.#session } : undefined; } @@ -133,3 +138,47 @@ export function getPublisher(session: LoginSession) { } } } + +export function setFollows( + state: LoginSession, + follows: Array, + ts: number, +) { + if (state.follows.timestamp >= ts) { + return; + } + state.follows.tags = follows; + state.follows.timestamp = ts; +} + +export function setEmojis(state: LoginSession, emojis: Array) { + state.emojis = emojis; +} + +export function setMuted( + state: LoginSession, + muted: Array, + ts: number, +) { + if (state.muted.timestamp >= ts) { + return; + } + state.muted.tags = muted; + state.muted.timestamp = ts; +} + +export function setRelays( + state: LoginSession, + relays: Array, + ts: number, +) { + if (state.relays.timestamp >= ts) { + return; + } + state.relays = relays.reduce((acc, r) => { + const [, relay] = r; + const write = r.length === 2 || r.includes("write"); + const read = r.length === 2 || r.includes("read"); + return { ...acc, [relay]: { read, write } }; + }, {}); +} From 75ff1dc37638a37c77aa548fd842d9263e6ceb92 Mon Sep 17 00:00:00 2001 From: verbiricha Date: Mon, 31 Jul 2023 12:19:51 +0200 Subject: [PATCH 2/7] homepage improvements - show followed streams first - show stream hashtags in video tile - allow to visit hashtag page by clicking tag - allow to follow/unfollow hashtags --- src/element/follow-button.tsx | 25 +++++-- src/element/tags.tsx | 22 +++--- src/element/video-tile.css | 70 ++++++++++-------- src/element/video-tile.tsx | 26 +++++-- src/hooks/live-streams.ts | 63 ++++++++++++++++ src/index.tsx | 5 ++ src/login.ts | 49 ------------- src/pages/root.tsx | 131 ++++++++++++++++++---------------- src/pages/tag.css | 12 ++++ src/pages/tag.tsx | 24 +++++++ src/utils.ts | 19 +++++ 11 files changed, 288 insertions(+), 158 deletions(-) create mode 100644 src/hooks/live-streams.ts create mode 100644 src/pages/tag.css create mode 100644 src/pages/tag.tsx diff --git a/src/element/follow-button.tsx b/src/element/follow-button.tsx index 42167f5..0e7750d 100644 --- a/src/element/follow-button.tsx +++ b/src/element/follow-button.tsx @@ -4,16 +4,22 @@ import { useLogin } from "hooks/login"; import AsyncButton from "element/async-button"; import { Login, System } from "index"; -export function LoggedInFollowButton({ pubkey }: { pubkey: string }) { +export function LoggedInFollowButton({ + tag, + value, +}: { + tag: "p" | "t"; + value: string; +}) { const login = useLogin(); const tags = login.follows.tags; - const follows = tags.filter((t) => t.at(0) === "p"); - const isFollowing = follows.find((t) => t.at(1) === pubkey); + const follows = tags.filter((t) => t.at(0) === tag); + const isFollowing = follows.find((t) => t.at(1) === value); async function unfollow() { const pub = login?.publisher(); if (pub) { - const newFollows = tags.filter((t) => t.at(1) !== pubkey); + const newFollows = tags.filter((t) => t.at(1) !== value); const ev = await pub.generic((eb) => { eb.kind(EventKind.ContactList).content(login.follows.content); for (const t of newFollows) { @@ -30,7 +36,7 @@ export function LoggedInFollowButton({ pubkey }: { pubkey: string }) { async function follow() { const pub = login?.publisher(); if (pub) { - const newFollows = [...tags, ["p", pubkey]]; + const newFollows = [...tags, [tag, value]]; const ev = await pub.generic((eb) => { eb.kind(EventKind.ContactList).content(login.follows.content); for (const tag of newFollows) { @@ -56,9 +62,16 @@ export function LoggedInFollowButton({ pubkey }: { pubkey: string }) { ); } +export function FollowTagButton({ tag }: { tag: string }) { + const login = useLogin(); + return login?.pubkey ? ( + + ) : null; +} + export function FollowButton({ pubkey }: { pubkey: string }) { const login = useLogin(); return login?.pubkey ? ( - + ) : null; } diff --git a/src/element/tags.tsx b/src/element/tags.tsx index fce9d8f..c4f446c 100644 --- a/src/element/tags.tsx +++ b/src/element/tags.tsx @@ -1,18 +1,25 @@ import type { ReactNode } from "react"; import moment from "moment"; + import { NostrEvent } from "@snort/system"; + import { StreamState } from "index"; -import { findTag } from "utils"; +import { findTag, getTagValues } from "utils"; export function Tags({ children, + max, ev, }: { children?: ReactNode; + max?: number; ev: NostrEvent; }) { const status = findTag(ev, "status"); const start = findTag(ev, "starts"); + const hashtags = getTagValues(ev.tags, "t"); + const tags = max ? hashtags.slice(0, max) : hashtags; + return ( <> {children} @@ -22,14 +29,11 @@ export function Tags({ {moment(Number(start) * 1000).fromNow()} )} - {ev.tags - .filter((a) => a[0] === "t") - .map((a) => a[1]) - .map((a) => ( - - {a} - - ))} + {tags.map((a) => ( + + {a} + + ))} ); } diff --git a/src/element/video-tile.css b/src/element/video-tile.css index 6c8674b..a47df95 100644 --- a/src/element/video-tile.css +++ b/src/element/video-tile.css @@ -1,45 +1,59 @@ .video-tile { - position: relative; + position: relative; } -.video-tile.nsfw>div:nth-child(1) { - filter: blur(3px); +.video-tile.nsfw > div:nth-child(1) { + filter: blur(3px); } -.video-tile>div:nth-child(1) { - border-radius: 16px; - width: 100%; - aspect-ratio: 16 / 10; - background-size: cover; - background-position: center; - background-repeat: no-repeat; +.video-tile > div:nth-child(1) { + border-radius: 16px; + width: 100%; + aspect-ratio: 16 / 10; + background-size: cover; + background-position: center; + background-repeat: no-repeat; } .video-tile h3 { - font-size: 20px; - line-height: 25px; - margin: 16px 0; - word-break: break-all; - word-wrap: break-word; + font-size: 20px; + line-height: 25px; + margin: 16px 0; + word-break: break-all; + word-wrap: break-word; } .video-tile .pill-box { - margin: 16px 20px; - text-transform: uppercase; - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: flex-end; - position: absolute; - top: 0; - right: 0; - gap: 8px; + margin: 16px 20px; + text-transform: uppercase; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-end; + position: absolute; + top: 0; + right: 0; + gap: 8px; } .video-tile .pill-box .pill { - width: fit-content; + width: fit-content; } .video-tile .pill-box .pill.viewers { - text-transform: lowercase; -} \ No newline at end of file + text-transform: lowercase; +} + +.video-tile .video-tags { + display: flex; + align-items: flex-start; + flex-wrap: wrap; + gap: 4px; + position: absolute; + bottom: 4px; + left: 4px; +} + +.video-tile .video-tags .pill { + font-size: 12px; +} diff --git a/src/element/video-tile.tsx b/src/element/video-tile.tsx index b2c7184..6feaf1b 100644 --- a/src/element/video-tile.tsx +++ b/src/element/video-tile.tsx @@ -9,6 +9,7 @@ import { findTag, getHost } from "utils"; import { formatSats } from "number"; import ZapStream from "../../public/zap-stream.svg"; import { isContentWarningAccepted } from "./content-warning"; +import { Tags } from "element/tags"; export function VideoTile({ ev, @@ -25,7 +26,8 @@ export function VideoTile({ const image = findTag(ev, "image"); const status = findTag(ev, "status"); const viewers = findTag(ev, "current_participants"); - const contentWarning = findTag(ev, "content-warning") && !isContentWarningAccepted(); + const contentWarning = + findTag(ev, "content-warning") && !isContentWarningAccepted(); const host = getHost(ev); const link = encodeTLV( @@ -33,19 +35,33 @@ export function VideoTile({ id, undefined, ev.kind, - ev.pubkey + ev.pubkey, ); return ( - +
0 ? image : ZapStream) : ""})`, + position: "relative", + backgroundImage: `url(${ + inView ? ((image?.length ?? 0) > 0 ? image : ZapStream) : "" + })`, }} > +
+ +
{showStatus && } - {viewers && {formatSats(Number(viewers))} viewers} + {viewers && ( + + {formatSats(Number(viewers))} viewers + + )}

{title}

{showAuthor &&
{inView && }
} diff --git a/src/hooks/live-streams.ts b/src/hooks/live-streams.ts new file mode 100644 index 0000000..aee8303 --- /dev/null +++ b/src/hooks/live-streams.ts @@ -0,0 +1,63 @@ +import { useMemo } from "react"; + +import { NoteCollection, RequestBuilder } from "@snort/system"; +import { useRequestBuilder } from "@snort/system-react"; + +import { unixNow } from "@snort/shared"; +import { LIVE_STREAM } from "const"; +import { System, StreamState } from "index"; +import { findTag, dedupeByHost } from "utils"; + +export function useStreamsFeed(tag?: string) { + const rb = useMemo(() => { + const rb = new RequestBuilder(tag ? `streams:${tag}` : "streams"); + rb.withOptions({ + leaveOpen: true, + }); + if (tag) { + rb.withFilter() + .kinds([LIVE_STREAM]) + .tag("t", [tag]) + .since(unixNow() - 86400); + } else { + rb.withFilter() + .kinds([LIVE_STREAM]) + .since(unixNow() - 86400); + } + return rb; + }, [tag]); + + const feed = useRequestBuilder(System, NoteCollection, rb); + const feedSorted = useMemo(() => { + if (feed.data) { + return [...feed.data].sort((a, b) => { + const aStatus = findTag(a, "status")!; + const bStatus = findTag(b, "status")!; + if (aStatus === bStatus) { + const aStart = Number(findTag(a, "starts") ?? "0"); + const bStart = Number(findTag(b, "starts") ?? "0"); + return bStart > aStart ? 1 : -1; + } else { + return aStatus === "live" ? -1 : 1; + } + }); + } + return []; + }, [feed.data]); + + const live = dedupeByHost( + feedSorted.filter((a) => findTag(a, "status") === StreamState.Live), + ); + const planned = dedupeByHost( + feedSorted.filter((a) => findTag(a, "status") === StreamState.Planned), + ); + const ended = dedupeByHost( + feedSorted.filter((a) => { + const hasEnded = findTag(a, "status") === StreamState.Ended; + const recording = findTag(a, "recording"); + return hasEnded && recording?.length > 0; + }), + ); + + return { live, planned, ended }; +} diff --git a/src/index.tsx b/src/index.tsx index 09161db..bd5437e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,6 +7,7 @@ import { NostrSystem } from "@snort/system"; import { RouterProvider, createBrowserRouter } from "react-router-dom"; import { RootPage } from "pages/root"; +import { TagPage } from "pages/tag"; import { LayoutPage } from "pages/layout"; import { ProfilePage } from "pages/profile-page"; import { StreamPage } from "pages/stream-page"; @@ -41,6 +42,10 @@ const router = createBrowserRouter([ path: "/", element: , }, + { + path: "/t/:tag", + element: , + }, { path: "/p/:npub", element: , diff --git a/src/login.ts b/src/login.ts index 0b6e35e..1b20352 100644 --- a/src/login.ts +++ b/src/login.ts @@ -72,11 +72,6 @@ export class LoginStore extends ExternalStore { this.#save(); } - updateSession(s: LoginSession) { - this.#session = s; - this.#save(); - } - takeSnapshot() { return this.#session ? { ...this.#session } : undefined; } @@ -138,47 +133,3 @@ export function getPublisher(session: LoginSession) { } } } - -export function setFollows( - state: LoginSession, - follows: Array, - ts: number, -) { - if (state.follows.timestamp >= ts) { - return; - } - state.follows.tags = follows; - state.follows.timestamp = ts; -} - -export function setEmojis(state: LoginSession, emojis: Array) { - state.emojis = emojis; -} - -export function setMuted( - state: LoginSession, - muted: Array, - ts: number, -) { - if (state.muted.timestamp >= ts) { - return; - } - state.muted.tags = muted; - state.muted.timestamp = ts; -} - -export function setRelays( - state: LoginSession, - relays: Array, - ts: number, -) { - if (state.relays.timestamp >= ts) { - return; - } - state.relays = relays.reduce((acc, r) => { - const [, relay] = r; - const write = r.length === 2 || r.includes("write"); - const read = r.length === 2 || r.includes("read"); - return { ...acc, [relay]: { read, write } }; - }, {}); -} diff --git a/src/pages/root.tsx b/src/pages/root.tsx index 1d3bd34..260e8fb 100644 --- a/src/pages/root.tsx +++ b/src/pages/root.tsx @@ -1,72 +1,79 @@ import "./root.css"; +import type { NostrEvent } from "@snort/system"; -import { useMemo } from "react"; -import { unixNow } from "@snort/shared"; -import { - NoteCollection, - RequestBuilder, -} from "@snort/system"; -import { useRequestBuilder } from "@snort/system-react"; -import { StreamState, System } from ".."; -import { VideoTile } from "../element/video-tile"; -import { findTag } from "../utils"; -import { LIVE_STREAM } from "../const"; +import { VideoTile } from "element/video-tile"; +import { useLogin } from "hooks/login"; +import { getHost, getTagValues, dedupeByHost } from "utils"; +import { useStreamsFeed } from "hooks/live-streams"; export function RootPage() { - const rb = useMemo(() => { - const rb = new RequestBuilder("root"); - rb.withOptions({ - leaveOpen: true, - }) - .withFilter() - .kinds([LIVE_STREAM]) - .since(unixNow() - 86400); - return rb; - }, []); + const login = useLogin(); - const feed = useRequestBuilder( - System, - NoteCollection, - rb - ); - const feedSorted = useMemo(() => { - if (feed.data) { - return [...feed.data].sort((a, b) => { - const aStatus = findTag(a, "status")!; - const bStatus = findTag(b, "status")!; - if (aStatus === bStatus) { - const aStart = Number(findTag(a, "starts") ?? "0"); - const bStart = Number(findTag(b, "starts") ?? "0"); - return bStart > aStart ? 1 : -1; - } else { - return aStatus === "live" ? -1 : 1; - } - }); - } - return []; - }, [feed.data]); + const { live, planned, ended } = useStreamsFeed(); + const mutedHosts = getTagValues(login?.muted.tags ?? [], "p"); + const followsHost = (ev: NostrEvent) => { + return login?.follows.tags?.find((t) => t.at(1) === getHost(ev)); + }; + const hashtags = getTagValues(login?.follows.tags ?? [], "t"); + const following = dedupeByHost(live.filter(followsHost)); + const liveNow = dedupeByHost(live.filter((e) => !following.includes(e))); + const hasFollowingLive = following.length > 0; + + const plannedEvents = planned + .filter((e) => !mutedHosts.includes(getHost(e))) + .filter(followsHost); - const live = feedSorted.filter( - (a) => findTag(a, "status") === StreamState.Live - ); - const planned = feedSorted.filter( - (a) => findTag(a, "status") === StreamState.Planned - ); - const ended = feedSorted.filter( - (a) => findTag(a, "status") === StreamState.Ended - ); return (
-
- {live.map((e) => ( - - ))} -
- {planned.length > 0 && ( + {hasFollowingLive && ( +
+ {following.map((e) => ( + + ))} +
+ )} + {!hasFollowingLive && ( +
+ {live + .filter((e) => !mutedHosts.includes(getHost(e))) + .map((e) => ( + + ))} +
+ )} + {hashtags.map((t) => ( + <> +

#{t}

+
+ {live + .filter((e) => !mutedHosts.includes(getHost(e))) + .filter((e) => { + const evTags = getTagValues(e.tags, "t"); + return evTags.includes(t); + }) + .map((e) => ( + + ))} +
+ + ))} + {hasFollowingLive && liveNow.length > 0 && ( + <> +

Live

+
+ {liveNow + .filter((e) => !mutedHosts.includes(getHost(e))) + .map((e) => ( + + ))} +
+ + )} + {plannedEvents.length > 0 && ( <>

Planned

- {planned.map((e) => ( + {plannedEvents.map((e) => ( ))}
@@ -76,9 +83,11 @@ export function RootPage() { <>

Ended

- {ended.map((e) => ( - - ))} + {ended + .filter((e) => !mutedHosts.includes(getHost(e))) + .map((e) => ( + + ))}
)} diff --git a/src/pages/tag.css b/src/pages/tag.css new file mode 100644 index 0000000..b366551 --- /dev/null +++ b/src/pages/tag.css @@ -0,0 +1,12 @@ +@import "./root.css"; + +.tag-page-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 0 12px; +} + +.tag-page h1 { + margin: 0; +} diff --git a/src/pages/tag.tsx b/src/pages/tag.tsx new file mode 100644 index 0000000..f6809d1 --- /dev/null +++ b/src/pages/tag.tsx @@ -0,0 +1,24 @@ +import "./tag.css"; +import { useParams } from "react-router-dom"; + +import { VideoTile } from "element/video-tile"; +import { FollowTagButton } from "element/follow-button"; +import { useStreamsFeed } from "hooks/live-streams"; + +export function TagPage() { + const { tag } = useParams(); + const { live } = useStreamsFeed(tag); + return ( +
+
+

#{tag}

+ +
+
+ {live.map((e) => ( + + ))} +
+
+ ); +} diff --git a/src/utils.ts b/src/utils.ts index 6d915b5..6546fcc 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -94,3 +94,22 @@ export async function openFile(): Promise { export function getTagValues(tags: Array, tag: string) { return tags.filter((t) => t.at(0) === tag).map((t) => t.at(1)); } + +export function dedupeByHost(events: Array) { + const { result } = events.reduce( + ({ result, seen }, ev) => { + const host = getHost(ev); + if (seen.has(host)) { + return { result, seen }; + } + result.push(ev); + seen.add(host); + return { result, seen }; + }, + { + result: [], + seen: new Set(), + }, + ); + return result; +} From bd3d566c3f52e191bb2417c65773419be499360a Mon Sep 17 00:00:00 2001 From: verbiricha Date: Mon, 31 Jul 2023 22:55:27 +0200 Subject: [PATCH 3/7] fix: typo --- src/element/text.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/element/text.tsx b/src/element/text.tsx index e419156..9b75421 100644 --- a/src/element/text.tsx +++ b/src/element/text.tsx @@ -2,7 +2,7 @@ import { useMemo, type ReactNode } from "react"; import { parseNostrLink, validateNostrLink } from "@snort/system"; -import { Address } from "element/Address"; +import { Address } from "element/address"; import { Event } from "element/Event"; import { Mention } from "element/mention"; import { Emoji } from "element/emoji"; From 5026f6a00847453606fa010f4223a93674d686ed Mon Sep 17 00:00:00 2001 From: verbiricha Date: Tue, 1 Aug 2023 07:48:51 +0200 Subject: [PATCH 4/7] fix: show tags outside video tile --- src/element/video-tile.css | 20 +++++++++++----- src/element/video-tile.tsx | 48 ++++++++++++++++++++------------------ 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/element/video-tile.css b/src/element/video-tile.css index a47df95..6ac3108 100644 --- a/src/element/video-tile.css +++ b/src/element/video-tile.css @@ -2,6 +2,17 @@ position: relative; } +.video-tile-container { + display: flex; + flex-direction: column; +} + +.video-tile-info { + display: flex; + flex-direction: column; + gap: 6px; +} + .video-tile.nsfw > div:nth-child(1) { filter: blur(3px); } @@ -18,7 +29,7 @@ .video-tile h3 { font-size: 20px; line-height: 25px; - margin: 16px 0; + margin: 16px 0 6px 0; word-break: break-all; word-wrap: break-word; } @@ -44,16 +55,13 @@ text-transform: lowercase; } -.video-tile .video-tags { +.video-tags { display: flex; align-items: flex-start; flex-wrap: wrap; gap: 4px; - position: absolute; - bottom: 4px; - left: 4px; } -.video-tile .video-tags .pill { +.video-tags .pill { font-size: 12px; } diff --git a/src/element/video-tile.tsx b/src/element/video-tile.tsx index 6feaf1b..2f49bc3 100644 --- a/src/element/video-tile.tsx +++ b/src/element/video-tile.tsx @@ -38,33 +38,35 @@ export function VideoTile({ ev.pubkey, ); return ( - -
0 ? image : ZapStream) : "" - })`, - }} +
+ +
0 ? image : ZapStream) : "" + })`, + }} + >
+ + {showStatus && } + {viewers && ( + + {formatSats(Number(viewers))} viewers + + )} + +

{title}

+ +
+ {showAuthor &&
{inView && }
}
- - {showStatus && } - {viewers && ( - - {formatSats(Number(viewers))} viewers - - )} - -

{title}

- {showAuthor &&
{inView && }
} - +
); } From 58c55f17b6357f10acacc8b90484b94e48204b0b Mon Sep 17 00:00:00 2001 From: verbiricha Date: Tue, 1 Aug 2023 07:58:12 +0200 Subject: [PATCH 5/7] refactor: cache since, dont dedupe by host --- src/hooks/live-chat.tsx | 9 +++++---- src/hooks/live-streams.ts | 34 ++++++++++++++-------------------- src/pages/root.tsx | 27 +++++++++++++-------------- src/utils.ts | 19 ------------------- 4 files changed, 32 insertions(+), 57 deletions(-) diff --git a/src/hooks/live-chat.tsx b/src/hooks/live-chat.tsx index f18a64c..8723648 100644 --- a/src/hooks/live-chat.tsx +++ b/src/hooks/live-chat.tsx @@ -11,9 +11,10 @@ import { useMemo } from "react"; import { LIVE_STREAM_CHAT } from "const"; export function useLiveChatFeed(link: NostrLink, eZaps?: Array) { - const since = useMemo(() => - unixNow() - (60 * 60 * 24 * 7), // 7-days of zaps - [link.id]); + const since = useMemo( + () => unixNow() - 60 * 60 * 24 * 7, // 7-days of zaps + [link.id], + ); const sub = useMemo(() => { const rb = new RequestBuilder(`live:${link.id}:${link.author}`); rb.withOptions({ @@ -57,7 +58,7 @@ export function useLiveChatFeed(link: NostrLink, eZaps?: Array) { const reactionsSub = useRequestBuilder( System, FlatNoteStore, - esub + esub, ); const reactions = reactionsSub.data ?? []; diff --git a/src/hooks/live-streams.ts b/src/hooks/live-streams.ts index aee8303..712cd06 100644 --- a/src/hooks/live-streams.ts +++ b/src/hooks/live-streams.ts @@ -6,26 +6,22 @@ import { useRequestBuilder } from "@snort/system-react"; import { unixNow } from "@snort/shared"; import { LIVE_STREAM } from "const"; import { System, StreamState } from "index"; -import { findTag, dedupeByHost } from "utils"; +import { findTag } from "utils"; export function useStreamsFeed(tag?: string) { + const since = useMemo(() => unixNow() - 86400, [tag]); const rb = useMemo(() => { const rb = new RequestBuilder(tag ? `streams:${tag}` : "streams"); rb.withOptions({ leaveOpen: true, }); if (tag) { - rb.withFilter() - .kinds([LIVE_STREAM]) - .tag("t", [tag]) - .since(unixNow() - 86400); + rb.withFilter().kinds([LIVE_STREAM]).tag("t", [tag]).since(since); } else { - rb.withFilter() - .kinds([LIVE_STREAM]) - .since(unixNow() - 86400); + rb.withFilter().kinds([LIVE_STREAM]).since(since); } return rb; - }, [tag]); + }, [tag, since]); const feed = useRequestBuilder(System, NoteCollection, rb); const feedSorted = useMemo(() => { @@ -45,19 +41,17 @@ export function useStreamsFeed(tag?: string) { return []; }, [feed.data]); - const live = dedupeByHost( - feedSorted.filter((a) => findTag(a, "status") === StreamState.Live), + const live = feedSorted.filter( + (a) => findTag(a, "status") === StreamState.Live, ); - const planned = dedupeByHost( - feedSorted.filter((a) => findTag(a, "status") === StreamState.Planned), - ); - const ended = dedupeByHost( - feedSorted.filter((a) => { - const hasEnded = findTag(a, "status") === StreamState.Ended; - const recording = findTag(a, "recording"); - return hasEnded && recording?.length > 0; - }), + const planned = feedSorted.filter( + (a) => findTag(a, "status") === StreamState.Planned, ); + const ended = feedSorted.filter((a) => { + const hasEnded = findTag(a, "status") === StreamState.Ended; + const recording = findTag(a, "recording"); + return hasEnded && recording?.length > 0; + }); return { live, planned, ended }; } diff --git a/src/pages/root.tsx b/src/pages/root.tsx index 260e8fb..9ed2a91 100644 --- a/src/pages/root.tsx +++ b/src/pages/root.tsx @@ -3,25 +3,26 @@ import type { NostrEvent } from "@snort/system"; import { VideoTile } from "element/video-tile"; import { useLogin } from "hooks/login"; -import { getHost, getTagValues, dedupeByHost } from "utils"; +import { getHost, getTagValues } from "utils"; import { useStreamsFeed } from "hooks/live-streams"; export function RootPage() { const login = useLogin(); const { live, planned, ended } = useStreamsFeed(); - const mutedHosts = getTagValues(login?.muted.tags ?? [], "p"); + const mutedHosts = new Set(getTagValues(login?.muted.tags ?? [], "p")); const followsHost = (ev: NostrEvent) => { return login?.follows.tags?.find((t) => t.at(1) === getHost(ev)); }; const hashtags = getTagValues(login?.follows.tags ?? [], "t"); - const following = dedupeByHost(live.filter(followsHost)); - const liveNow = dedupeByHost(live.filter((e) => !following.includes(e))); + const following = live.filter(followsHost); + const liveNow = live.filter((e) => !following.includes(e)); const hasFollowingLive = following.length > 0; const plannedEvents = planned - .filter((e) => !mutedHosts.includes(getHost(e))) + .filter((e) => !mutedHosts.has(getHost(e))) .filter(followsHost); + const endedEvents = ended.filter((e) => !mutedHosts.has(getHost(e))); return (
@@ -35,7 +36,7 @@ export function RootPage() { {!hasFollowingLive && (
{live - .filter((e) => !mutedHosts.includes(getHost(e))) + .filter((e) => !mutedHosts.has(getHost(e))) .map((e) => ( ))} @@ -46,7 +47,7 @@ export function RootPage() {

#{t}

{live - .filter((e) => !mutedHosts.includes(getHost(e))) + .filter((e) => !mutedHosts.has(getHost(e))) .filter((e) => { const evTags = getTagValues(e.tags, "t"); return evTags.includes(t); @@ -62,7 +63,7 @@ export function RootPage() {

Live

{liveNow - .filter((e) => !mutedHosts.includes(getHost(e))) + .filter((e) => !mutedHosts.has(getHost(e))) .map((e) => ( ))} @@ -79,15 +80,13 @@ export function RootPage() {
)} - {ended.length > 0 && ( + {endedEvents.length > 0 && ( <>

Ended

- {ended - .filter((e) => !mutedHosts.includes(getHost(e))) - .map((e) => ( - - ))} + {endedEvents.map((e) => ( + + ))}
)} diff --git a/src/utils.ts b/src/utils.ts index 6546fcc..6d915b5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -94,22 +94,3 @@ export async function openFile(): Promise { export function getTagValues(tags: Array, tag: string) { return tags.filter((t) => t.at(0) === tag).map((t) => t.at(1)); } - -export function dedupeByHost(events: Array) { - const { result } = events.reduce( - ({ result, seen }, ev) => { - const host = getHost(ev); - if (seen.has(host)) { - return { result, seen }; - } - result.push(ev); - seen.add(host); - return { result, seen }; - }, - { - result: [], - seen: new Set(), - }, - ); - return result; -} From 1baecf41f219d8c1ba9bbccc0d7ccaa4c99f75ac Mon Sep 17 00:00:00 2001 From: verbiricha Date: Tue, 1 Aug 2023 08:10:58 +0200 Subject: [PATCH 6/7] refactor: useCallback --- src/pages/root.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pages/root.tsx b/src/pages/root.tsx index 9ed2a91..84c0e69 100644 --- a/src/pages/root.tsx +++ b/src/pages/root.tsx @@ -1,4 +1,5 @@ import "./root.css"; +import { useCallback } from "react"; import type { NostrEvent } from "@snort/system"; import { VideoTile } from "element/video-tile"; @@ -11,9 +12,12 @@ export function RootPage() { const { live, planned, ended } = useStreamsFeed(); const mutedHosts = new Set(getTagValues(login?.muted.tags ?? [], "p")); - const followsHost = (ev: NostrEvent) => { - return login?.follows.tags?.find((t) => t.at(1) === getHost(ev)); - }; + const followsHost = useCallback( + (ev: NostrEvent) => { + return login?.follows.tags.find((t) => t.at(1) === getHost(ev)); + }, + [login?.follows], + ); const hashtags = getTagValues(login?.follows.tags ?? [], "t"); const following = live.filter(followsHost); const liveNow = live.filter((e) => !following.includes(e)); From 0fcb712759853c80bae3f6df45c089715d29de6b Mon Sep 17 00:00:00 2001 From: verbiricha Date: Tue, 1 Aug 2023 10:22:56 +0200 Subject: [PATCH 7/7] fix: logout emoji packs --- src/element/emoji-pack.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/element/emoji-pack.tsx b/src/element/emoji-pack.tsx index 1e4cce2..532156e 100644 --- a/src/element/emoji-pack.tsx +++ b/src/element/emoji-pack.tsx @@ -12,7 +12,7 @@ import { Login, System } from "index"; export function EmojiPack({ ev }: { ev: NostrEvent }) { const login = useLogin(); const name = findTag(ev, "d"); - const isUsed = login.emojis.find( + const isUsed = login?.emojis.find( (e) => e.author === ev.pubkey && e.name === name, ); const emoji = ev.tags.filter((e) => e.at(0) === "emoji");