Compare commits

..

13 Commits

20 changed files with 219 additions and 70 deletions

View File

@ -0,0 +1,25 @@
{
"applinks": {
"apps": [],
"details": [
{
"appIDs": [
"24VGVR4CHC.io.nostrlabs.zap-stream"
],
"paths": [
"*"
],
"components": [
{
"/": "/*"
}
]
}
]
},
"webcredentials": {
"apps": [
"24VGVR4CHC.io.nostrlabs.zap-stream"
]
}
}

View File

@ -0,0 +1,14 @@
[
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "io.nostrlabs.zap_stream_flutter",
"sha256_cert_fingerprints": [
"6F:87:59:1F:55:29:82:75:F5:C0:D4:22:34:D5:68:DA:79:03:06:31:16:63:A8:28:04:27:D8:34:0A:8F:95:8A"
]
}
}
]

View File

@ -42,7 +42,7 @@ function loadWhitelist() {
const list = import.meta.env.VITE_SINGLE_PUBLISHER as string | undefined; const list = import.meta.env.VITE_SINGLE_PUBLISHER as string | undefined;
if (list) { if (list) {
return list.split(",").map(a => { return list.split(",").map(a => {
if (a.startsWith('npub')) { if (a.startsWith("npub")) {
return parseNostrLink(a).id; return parseNostrLink(a).id;
} else { } else {
return a; return a;
@ -53,4 +53,6 @@ function loadWhitelist() {
return undefined; return undefined;
} }
export const WHITELIST: Array<string> | undefined = loadWhitelist(); export const WHITELIST: Array<string> | undefined = loadWhitelist();
export const NIP5_DOMAIN: string = import.meta.env.VITE_NIP5_DOMAIN || "zap.stream";

View File

@ -35,7 +35,7 @@ export function EventIcon({ kind }: { kind?: EventKind }) {
export function NostrEvent({ ev }: { ev: TaggedNostrEvent }) { export function NostrEvent({ ev }: { ev: TaggedNostrEvent }) {
const link = NostrLink.fromEvent(ev); const link = NostrLink.fromEvent(ev);
function modalPage(inner: ReactNode) { function modalPage(inner: ReactNode) {
return <div className="rounded-2xl px-4 py-3 md:w-[700px] mx-auto w-full">{inner}</div>; return <div className="rounded-2xl px-4 py-3">{inner}</div>;
} }
switch (ev.kind) { switch (ev.kind) {
@ -83,6 +83,10 @@ export function NostrEvent({ ev }: { ev: TaggedNostrEvent }) {
export function EventEmbed({ link, ...props }: EventProps & HTMLProps<HTMLDivElement>) { export function EventEmbed({ link, ...props }: EventProps & HTMLProps<HTMLDivElement>) {
const event = useEventFeed(link); const event = useEventFeed(link);
if (event) { if (event) {
return <NostrEvent ev={event} {...props} />; return (
<div className="md:w-[700px] mx-auto w-full">
<NostrEvent ev={event} {...props} />
</div>
);
} }
} }

View File

@ -1,16 +1,19 @@
import { NostrLink, TaggedNostrEvent } from "@snort/system"; import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { ExternalIconLink } from "./external-link";
import { Profile } from "./profile"; import { Profile } from "./profile";
import EventReactions from "./event-reactions"; import EventReactions from "./event-reactions";
import { Text } from "@/element/text"; import { Text } from "@/element/text";
import { Link } from "react-router-dom";
import { Icon } from "./icon";
export function Note({ ev }: { ev: TaggedNostrEvent }) { export function Note({ ev }: { ev: TaggedNostrEvent }) {
return ( return (
<div className="bg-layer-2 rounded-xl px-4 py-3 flex flex-col gap-2"> <div className="bg-layer-2 rounded-xl px-4 py-3 flex flex-col gap-2">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<Profile pubkey={ev.pubkey} avatarSize={30} /> <Profile pubkey={ev.pubkey} avatarSize={30} />
<ExternalIconLink size={24} href={`https://snort.social/${NostrLink.fromEvent(ev).encode()}`} /> <Link to={`/${NostrLink.fromEvent(ev).encode()}`}>
<Icon name="link" size={24} />
</Link>
</div> </div>
<Text tags={ev.tags} content={ev.content} className="whitespace-pre-line overflow-wrap" /> <Text tags={ev.tags} content={ev.content} className="whitespace-pre-line overflow-wrap" />
<EventReactions ev={ev} /> <EventReactions ev={ev} />

View File

@ -145,7 +145,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
} }
} }
const startsTimestamp = Number(start ?? (new Date().getTime() / 1000)); const startsTimestamp = Number(start ?? new Date().getTime() / 1000);
const startsDate = new Date(startsTimestamp * 1000); const startsDate = new Date(startsTimestamp * 1000);
return ( return (

View File

@ -40,7 +40,7 @@ export default function LiveVideoPlayer({ title, stream, status, poster, link, .
<Nip94Player link={link} /> <Nip94Player link={link} />
</Suspense> </Suspense>
); );
} else if (stream && stream.toLowerCase().endsWith('.m3u8')) { } else if (stream && stream.toLowerCase().endsWith(".m3u8")) {
// hls video // hls video
/* @ts-ignore Web Componenet */ /* @ts-ignore Web Componenet */
return <hls-video {...props} slot="media" src={stream} playsInline={true} autoPlay={true} />; return <hls-video {...props} slot="media" src={stream} playsInline={true} autoPlay={true} />;

View File

@ -3,19 +3,43 @@ import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { getName } from "../profile"; import { getName } from "../profile";
import { StreamState } from "@/const"; import { NIP5_DOMAIN, StreamState } from "@/const";
import useImgProxy from "@/hooks/img-proxy"; import useImgProxy from "@/hooks/img-proxy";
import { formatSats } from "@/number"; import { formatSats } from "@/number";
import { extractStreamInfo, getHost, profileLink } from "@/utils"; import { extractStreamInfo, getHost, profileLink } from "@/utils";
import { useUserProfile } from "@snort/system-react"; import { useUserProfile } from "@snort/system-react";
import classNames from "classnames"; import classNames from "classnames";
import { useState } from "react"; import { useEffect, useState } from "react";
import { Avatar } from "../avatar"; import { Avatar } from "../avatar";
import Logo from "../logo"; import Logo from "../logo";
import { useContentWarning } from "../nsfw"; import { useContentWarning } from "../nsfw";
import PillOpaque from "../pill-opaque"; import PillOpaque from "../pill-opaque";
import { RelativeTime } from "../relative-time"; import { RelativeTime } from "../relative-time";
import { StatePill } from "../state-pill"; import { StatePill } from "../state-pill";
import { NostrJson } from "@snort/shared";
const nameCache = new Map<string, NostrJson>();
async function fetchNostrAddresByPubkey(pubkey: string, domain: string, timeout = 2_000): Promise<NostrJson | undefined> {
if (!pubkey || !domain) {
return undefined;
}
const cacheKey = `${pubkey}@${domain}`;
if (nameCache.has(cacheKey)) {
return nameCache.get(cacheKey);
}
try {
const res = await fetch(`https://${domain}/.well-known/nostr.json?pubkey=${pubkey}`, {
signal: AbortSignal.timeout(timeout),
});
const ret = (await res.json()) as NostrJson;
nameCache.set(cacheKey, ret);
return ret;
} catch {
// ignored
}
return undefined;
}
export function StreamTile({ export function StreamTile({
ev, ev,
@ -34,11 +58,26 @@ export function StreamTile({
}) { }) {
const { title, image, status, participants, contentWarning, recording, ends } = extractStreamInfo(ev); const { title, image, status, participants, contentWarning, recording, ends } = extractStreamInfo(ev);
const host = getHost(ev); const host = getHost(ev);
const link = NostrLink.fromEvent(ev);
const hostProfile = useUserProfile(host); const hostProfile = useUserProfile(host);
const isGrownUp = useContentWarning(); const isGrownUp = useContentWarning();
const { proxy } = useImgProxy(); const { proxy } = useImgProxy();
const [videoLink, setVideoLink] = useState(`/${link.encode()}`)
useEffect(() => {
if (status === StreamState.Live) {
fetchNostrAddresByPubkey(host, NIP5_DOMAIN).then((h) => {
if (h) {
const names = Object.entries(h.names);
if (names.length > 0) {
setVideoLink(`/${names[0][0]}`);
}
}
});
}
}, [status, videoLink]);
const link = NostrLink.fromEvent(ev);
const [hasImg, setHasImage] = useState((image?.length ?? 0) > 0 || (recording?.length ?? 0) > 0); const [hasImg, setHasImage] = useState((image?.length ?? 0) > 0 || (recording?.length ?? 0) > 0);
return ( return (
<div <div
@ -47,7 +86,7 @@ export function StreamTile({
"flex-row": style === "list", "flex-row": style === "list",
})}> })}>
<Link <Link
to={`/${link.encode()}`} to={videoLink}
className={classNames( className={classNames(
{ {
"blur transition": contentWarning, "blur transition": contentWarning,

View File

@ -74,7 +74,6 @@ export default function VideoGridSorted({
{liveByHashtag.map(t => ( {liveByHashtag.map(t => (
<GridSection header={`#${t.tag}`} items={t.live} /> <GridSection header={`#${t.tag}`} items={t.live} />
))} ))}
{showPopular && <PopularCategories items={evs} />}
{showVideos && ( {showVideos && (
<GridSection <GridSection
header={<FormattedMessage defaultMessage="Videos" />} header={<FormattedMessage defaultMessage="Videos" />}
@ -85,6 +84,7 @@ export default function VideoGridSorted({
{hasFollowingLive && liveNow.length > 0 && ( {hasFollowingLive && liveNow.length > 0 && (
<GridSection header={<FormattedMessage defaultMessage="Live" id="Dn82AL" />} items={liveNow} /> <GridSection header={<FormattedMessage defaultMessage="Live" id="Dn82AL" />} items={liveNow} />
)} )}
{showPopular && <PopularCategories items={evs} />}
{plannedEvents.length > 0 && (showPlanned ?? true) && ( {plannedEvents.length > 0 && (showPlanned ?? true) && (
<GridSection header={<FormattedMessage defaultMessage="Planned" id="kp0NPF" />} items={plannedEvents} /> <GridSection header={<FormattedMessage defaultMessage="Planned" id="kp0NPF" />} items={plannedEvents} />
)} )}

View File

@ -12,21 +12,11 @@ export function useStreamsFeed(tag?: string) {
}); });
if (WHITELIST) { if (WHITELIST) {
if (tag) { if (tag) {
rb.withFilter() rb.withFilter().kinds(liveStreamKinds).tag("t", [tag]).authors(WHITELIST);
.kinds(liveStreamKinds) rb.withFilter().kinds(liveStreamKinds).tag("t", [tag]).tag("p", WHITELIST);
.tag("t", [tag])
.authors(WHITELIST);
rb.withFilter()
.kinds(liveStreamKinds)
.tag("t", [tag])
.tag("p", WHITELIST);
} else { } else {
rb.withFilter() rb.withFilter().kinds(liveStreamKinds).authors(WHITELIST);
.kinds(liveStreamKinds) rb.withFilter().kinds(liveStreamKinds).tag("p", WHITELIST);
.authors(WHITELIST);
rb.withFilter()
.kinds(liveStreamKinds)
.tag("p", WHITELIST);
} }
} else { } else {
if (tag) { if (tag) {

View File

@ -1,19 +1,21 @@
import { NIP5_DOMAIN } from "@/const";
import { fetchNip05Pubkey } from "@snort/shared"; import { fetchNip05Pubkey } from "@snort/shared";
import { NostrLink, NostrPrefix, tryParseNostrLink } from "@snort/system"; import { NostrEvent, NostrLink, NostrPrefix, tryParseNostrLink } from "@snort/system";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
export function useStreamLink() { export function useStreamLink(evPreload?: NostrEvent) {
const params = useParams(); const params = useParams();
const [link, setLink] = useState<NostrLink>(); const [link, setLink] = useState<NostrLink | undefined>(evPreload ? NostrLink.fromEvent(evPreload) : undefined);
useEffect(() => { useEffect(() => {
if (evPreload !== undefined) return;
if (params.id) { if (params.id) {
const parsedLink = tryParseNostrLink(params.id); const parsedLink = tryParseNostrLink(params.id);
if (parsedLink) { if (parsedLink) {
setLink(parsedLink); setLink(parsedLink);
} else { } else {
const [handle, domain] = (params.id.includes("@") ? params.id : `${params.id}@zap.stream`).split("@"); const [handle, domain] = (params.id.includes("@") ? params.id : `${params.id}@${NIP5_DOMAIN}`).split("@");
fetchNip05Pubkey(handle, domain).then(d => { fetchNip05Pubkey(handle, domain).then(d => {
if (d) { if (d) {
setLink(new NostrLink(NostrPrefix.PublicKey, d)); setLink(new NostrLink(NostrPrefix.PublicKey, d));
@ -21,6 +23,6 @@ export function useStreamLink() {
}); });
} }
} }
}, [params.id]); }, [params.id, evPreload]);
return link; return link;
} }

View File

@ -0,0 +1,46 @@
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
<title>Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917</title>
<g>
<g>
<g>
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/>
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
</g>
<g id="_Group_" data-name="&lt;Group&gt;">
<g id="_Group_2" data-name="&lt;Group&gt;">
<g id="_Group_3" data-name="&lt;Group&gt;">
<path id="_Path_" data-name="&lt;Path&gt;" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/>
<path id="_Path_2" data-name="&lt;Path&gt;" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/>
</g>
</g>
<g>
<path d="M42.30227,27.13965h-4.7334l-1.13672,3.35645H34.42727l4.4834-12.418h2.083l4.4834,12.418H43.438ZM38.0591,25.59082h3.752l-1.84961-5.44727h-.05176Z" style="fill: #fff"/>
<path d="M55.15969,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H48.4302v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.645,21.34766,55.15969,23.16406,55.15969,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30227,29.01563,53.24953,27.81934,53.24953,25.96973Z" style="fill: #fff"/>
<path d="M65.12453,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H58.395v1.50586h.03418A3.21162,3.21162,0,0,1,61.312,21.34766C63.60988,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26711,29.01563,63.21438,27.81934,63.21438,25.96973Z" style="fill: #fff"/>
<path d="M71.71047,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.626,3.60645,3.44238,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/>
<path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z" style="fill: #fff"/>
<path d="M86.065,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72609,30.6084,86.065,28.82617,86.065,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40039,1.16211-2.40039,3.10742c0,1.96191.89453,3.10645,2.40039,3.10645S92.76027,27.93164,92.76027,25.96973Z" style="fill: #fff"/>
<path d="M96.18606,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z" style="fill: #fff"/>
<path d="M109.3843,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10207,25.13477Z" style="fill: #fff"/>
</g>
</g>
</g>
<g id="_Group_4" data-name="&lt;Group&gt;">
<g>
<path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z" style="fill: #fff"/>
<path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z" style="fill: #fff"/>
<path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z" style="fill: #fff"/>
<path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M59.09377,8.437h.88867v6.26074h-.88867Z" style="fill: #fff"/>
<path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z" style="fill: #fff"/>
<path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z" style="fill: #fff"/>
<path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z" style="fill: #fff"/>
<path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z" style="fill: #fff"/>
<path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z" style="fill: #fff"/>
<path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/>
<path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z" style="fill: #fff"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -42,6 +42,7 @@ import { LinkHandler } from "./pages/link-handler";
import { UploadPage } from "./pages/upload"; import { UploadPage } from "./pages/upload";
import { DebugPage } from "./pages/debug"; import { DebugPage } from "./pages/debug";
import { ShortsPage } from "./pages/shorts"; import { ShortsPage } from "./pages/shorts";
import { DownloadAppPage } from "./pages/download";
const hasWasm = "WebAssembly" in globalThis; const hasWasm = "WebAssembly" in globalThis;
const workerRelay = new WorkerRelayInterface( const workerRelay = new WorkerRelayInterface(
@ -92,6 +93,10 @@ const router = createBrowserRouter([
path: "/debug", path: "/debug",
element: <DebugPage />, element: <DebugPage />,
}, },
{
path: "/app",
element: <DownloadAppPage />,
},
{ {
path: "/", path: "/",
element: <RootPage />, element: <RootPage />,

View File

@ -170,9 +170,6 @@
"8YT6ja": { "8YT6ja": {
"defaultMessage": "Insert text to speak" "defaultMessage": "Insert text to speak"
}, },
"8aAwpp": {
"defaultMessage": "For manual hosting all you need is the HLS URL for the Stream URL field. You should be ale to find this in your hosting setup."
},
"8xVdjn": { "8xVdjn": {
"defaultMessage": "Video Codec" "defaultMessage": "Video Codec"
}, },
@ -236,6 +233,9 @@
"C81/uG": { "C81/uG": {
"defaultMessage": "Logout" "defaultMessage": "Logout"
}, },
"CFReLV": {
"defaultMessage": "For manual hosting all you need is the HLS URL for the Stream URL field. You should be able to find this in your hosting setup."
},
"CTgA2G": { "CTgA2G": {
"defaultMessage": "Create a new account if you don't have one already." "defaultMessage": "Create a new account if you don't have one already."
}, },
@ -491,9 +491,6 @@
"QNvtaq": { "QNvtaq": {
"defaultMessage": "Share on X" "defaultMessage": "Share on X"
}, },
"QRHNuF": {
"defaultMessage": "What are we streaming today?"
},
"QRRCp0": { "QRRCp0": {
"defaultMessage": "Stream URL" "defaultMessage": "Stream URL"
}, },
@ -582,6 +579,9 @@
"W7IRLs": { "W7IRLs": {
"defaultMessage": "Your title is too short" "defaultMessage": "Your title is too short"
}, },
"W8nHSd": {
"defaultMessage": "FAQ"
},
"W9355R": { "W9355R": {
"defaultMessage": "Unmute" "defaultMessage": "Unmute"
}, },
@ -733,6 +733,9 @@
"g5pX+a": { "g5pX+a": {
"defaultMessage": "About" "defaultMessage": "About"
}, },
"gJFhNJ": {
"defaultMessage": "What are we streaming today?"
},
"gQxxlw": { "gQxxlw": {
"defaultMessage": "Goal Name" "defaultMessage": "Goal Name"
}, },
@ -951,9 +954,6 @@
"uksRSi": { "uksRSi": {
"defaultMessage": "Latest Videos" "defaultMessage": "Latest Videos"
}, },
"vP4dFa": {
"defaultMessage": "Visit {link} to get some sweet zap.stream merch!"
},
"vaZKTn": { "vaZKTn": {
"defaultMessage": "Add more content" "defaultMessage": "Add more content"
}, },

39
src/pages/download.tsx Normal file
View File

@ -0,0 +1,39 @@
import { FormattedMessage } from "react-intl";
import PlayStore from "../images/GetItOnGooglePlay_Badge_Web_color_English.png";
import AppStore from "../images/Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917.svg";
import Obtanium from "../images/badge_obtainium.png";
import QrCode from "@/element/qr-code";
export function DownloadAppPage() {
const obtaniumLink = "https://github.com/nostrlabs-io/zap-stream-flutter"
const playStoreLink = "https://play.google.com/store/apps/details?id=io.nostrlabs.zap_stream_flutter";
const appStoreLink = "https://testflight.apple.com/join/5Qh7mfvU";
return <div className="mx-6 flex flex-col gap-4 w-full ">
<h2>
<FormattedMessage defaultMessage="Download the zap.stream app" />
</h2>
<div className="flex items-center max-md:flex-col md:w-full md:justify-evenly">
<div className="flex items-center flex-col gap-2">
<QrCode data={playStoreLink} link={playStoreLink} />
<a href={playStoreLink} target="_blank">
<img src={PlayStore} width={120} />
</a>
</div>
<div className="flex items-center flex-col gap-2">
<QrCode data={appStoreLink} link={appStoreLink} />
<a href={appStoreLink} target="_blank">
<img src={AppStore} width={120} />
</a>
</div>
<div className="flex items-center flex-col gap-2">
<QrCode data={obtaniumLink} link={obtaniumLink} />
<a href={obtaniumLink} target="_blank">
<img src={Obtanium} width={120} />
</a>
</div>
</div>
</div>
}

View File

@ -14,7 +14,7 @@ import { ShortPage } from "./short";
export function LinkHandler() { export function LinkHandler() {
const location = useLocation(); const location = useLocation();
const evPreload = getEventFromLocationState(location.state); const evPreload = getEventFromLocationState(location.state);
const link = useStreamLink(); const link = useStreamLink(evPreload);
const layoutContext = useLayout(); const layoutContext = useLayout();
if (!link) return; if (!link) return;

View File

@ -2,37 +2,17 @@ import { useStreamsFeed } from "@/hooks/live-streams";
import CategoryLink from "@/element/category/category-link"; import CategoryLink from "@/element/category/category-link";
import VideoGridSorted from "@/element/video-grid-sorted"; import VideoGridSorted from "@/element/video-grid-sorted";
import { AllCategories } from "./category"; import { AllCategories } from "./category";
import { FormattedMessage } from "react-intl"; import { Icon } from "@/element/icon";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import useImgProxy from "@/hooks/img-proxy";
export function RootPage() { export function RootPage() {
const streams = useStreamsFeed(); const streams = useStreamsFeed();
const { proxy } = useImgProxy();
const shirtSize = 120;
return ( return (
<div className="flex flex-col gap-6 p-4 min-w-0"> <div className="flex flex-col gap-6 p-4 min-w-0">
<div className="flex max-md:flex-col gap-2 items-center bg-layer-2 p-4 text-lg font-medium text-pretty"> <Link to="/app" className="flex gap-2 items-center px-4 py-2 rounded-xl bg-layer-2">
<img <Icon name="link" />
width={shirtSize} Get the new zap.stream app!
className="rounded-xl" </Link>
src={proxy(
"https://blossom.nogood.studio/f98bc742ba24b2c729420148d736c3c0f58e6551d7dc0e4bd263d78bf2ab58b8.png",
shirtSize,
"f98bc742ba24b2c729420148d736c3c0f58e6551d7dc0e4bd263d78bf2ab58b8",
)}
/>
<FormattedMessage
defaultMessage="Visit {link} to get some sweet zap.stream merch!"
values={{
link: (
<Link to="https://store.zap.stream" className="underline" target="_blank">
store.zap.stream
</Link>
),
}}
/>
</div>
<div className="min-w-0 overflow-x-scroll scrollbar-hidden"> <div className="min-w-0 overflow-x-scroll scrollbar-hidden">
<div className="flex gap-4 "> <div className="flex gap-4 ">
{AllCategories.filter(a => a.priority === 0).map(a => ( {AllCategories.filter(a => a.priority === 0).map(a => (

View File

@ -56,7 +56,6 @@
"7UOvbT": "Offline", "7UOvbT": "Offline",
"89UZph": "Configure your streaming software", "89UZph": "Configure your streaming software",
"8YT6ja": "Insert text to speak", "8YT6ja": "Insert text to speak",
"8aAwpp": "For manual hosting all you need is the HLS URL for the Stream URL field. You should be ale to find this in your hosting setup.",
"8xVdjn": "Video Codec", "8xVdjn": "Video Codec",
"9WRlF4": "Send", "9WRlF4": "Send",
"9ZoFpI": "Delete file", "9ZoFpI": "Delete file",
@ -78,6 +77,7 @@
"Bep/gA": "Private key", "Bep/gA": "Private key",
"BzQPM+": "Destination", "BzQPM+": "Destination",
"C81/uG": "Logout", "C81/uG": "Logout",
"CFReLV": "For manual hosting all you need is the HLS URL for the Stream URL field. You should be able to find this in your hosting setup.",
"CTgA2G": "Create a new account if you don't have one already.", "CTgA2G": "Create a new account if you don't have one already.",
"CsCUYo": "{n} sats", "CsCUYo": "{n} sats",
"CsS/fy": "{estimate} remaining ({balance} sats @ {rate} sats / {unit})", "CsS/fy": "{estimate} remaining ({balance} sats @ {rate} sats / {unit})",
@ -162,7 +162,6 @@
"Q3au2v": "About {estimate}", "Q3au2v": "About {estimate}",
"Q8Qw5B": "Description", "Q8Qw5B": "Description",
"QNvtaq": "Share on X", "QNvtaq": "Share on X",
"QRHNuF": "What are we streaming today?",
"QRRCp0": "Stream URL", "QRRCp0": "Stream URL",
"QWlMq9": "Stream key", "QWlMq9": "Stream key",
"QceMQZ": "Goal: {amount}", "QceMQZ": "Goal: {amount}",
@ -192,6 +191,7 @@
"VKb1MS": "Categories", "VKb1MS": "Categories",
"W7DNWx": "Stream Forwarding", "W7DNWx": "Stream Forwarding",
"W7IRLs": "Your title is too short", "W7IRLs": "Your title is too short",
"W8nHSd": "FAQ",
"W9355R": "Unmute", "W9355R": "Unmute",
"WVJZ0U": "Value", "WVJZ0U": "Value",
"WcZM+B": "File List", "WcZM+B": "File List",
@ -241,6 +241,7 @@
"fBI91o": "Zap", "fBI91o": "Zap",
"feZ/kG": "Login with Private Key (insecure)", "feZ/kG": "Login with Private Key (insecure)",
"g5pX+a": "About", "g5pX+a": "About",
"gJFhNJ": "What are we streaming today?",
"gQxxlw": "Goal Name", "gQxxlw": "Goal Name",
"gt65Gg": "Stream goals encourage viewers to support streamers via donations.", "gt65Gg": "Stream goals encourage viewers to support streamers via donations.",
"gzsn7k": "{n} messages", "gzsn7k": "{n} messages",
@ -313,7 +314,6 @@
"uYw2LD": "Stream", "uYw2LD": "Stream",
"ug01Mk": "Time", "ug01Mk": "Time",
"uksRSi": "Latest Videos", "uksRSi": "Latest Videos",
"vP4dFa": "Visit {link} to get some sweet zap.stream merch!",
"vaZKTn": "Add more content", "vaZKTn": "Add more content",
"vrTOHJ": "{amount} sats", "vrTOHJ": "{amount} sats",
"w+2Vw7": "Shorts", "w+2Vw7": "Shorts",