Compare commits

6 Commits
v0.5.0 ... main

Author SHA1 Message Date
694dd00f47 fix: dont replace link when evPreload is provided 2025-06-01 13:30:33 +01:00
4e76aee818 fix: use preload event in link handler 2025-05-28 21:23:44 +01:00
e400debde2 fix: short link only live streams 2025-05-28 21:18:25 +01:00
21c6be9195 feat: link to short link 2025-05-28 21:15:11 +01:00
63ca7fbbfc chore: add apple deep link json 2025-05-27 15:11:11 +01:00
26b5249283 chore: add assetlinks 2025-05-22 10:30:30 +01:00
6 changed files with 92 additions and 10 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

@ -54,3 +54,5 @@ function loadWhitelist() {
}
export const WHITELIST: Array<string> | undefined = loadWhitelist();
export const NIP5_DOMAIN: string = import.meta.env.VITE_NIP5_DOMAIN || "zap.stream";

View File

@ -3,19 +3,43 @@ import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";
import { getName } from "../profile";
import { StreamState } from "@/const";
import { NIP5_DOMAIN, StreamState } from "@/const";
import useImgProxy from "@/hooks/img-proxy";
import { formatSats } from "@/number";
import { extractStreamInfo, getHost, profileLink } from "@/utils";
import { useUserProfile } from "@snort/system-react";
import classNames from "classnames";
import { useState } from "react";
import { useEffect, useState } from "react";
import { Avatar } from "../avatar";
import Logo from "../logo";
import { useContentWarning } from "../nsfw";
import PillOpaque from "../pill-opaque";
import { RelativeTime } from "../relative-time";
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({
ev,
@ -34,11 +58,26 @@ export function StreamTile({
}) {
const { title, image, status, participants, contentWarning, recording, ends } = extractStreamInfo(ev);
const host = getHost(ev);
const link = NostrLink.fromEvent(ev);
const hostProfile = useUserProfile(host);
const isGrownUp = useContentWarning();
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);
return (
<div
@ -47,7 +86,7 @@ export function StreamTile({
"flex-row": style === "list",
})}>
<Link
to={`/${link.encode()}`}
to={videoLink}
className={classNames(
{
"blur transition": contentWarning,

View File

@ -1,19 +1,21 @@
import { NIP5_DOMAIN } from "@/const";
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 { useParams } from "react-router-dom";
export function useStreamLink() {
export function useStreamLink(evPreload?: NostrEvent) {
const params = useParams();
const [link, setLink] = useState<NostrLink>();
const [link, setLink] = useState<NostrLink | undefined>(evPreload ? NostrLink.fromEvent(evPreload) : undefined);
useEffect(() => {
if (evPreload !== undefined) return;
if (params.id) {
const parsedLink = tryParseNostrLink(params.id);
if (parsedLink) {
setLink(parsedLink);
} 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 => {
if (d) {
setLink(new NostrLink(NostrPrefix.PublicKey, d));
@ -21,6 +23,6 @@ export function useStreamLink() {
});
}
}
}, [params.id]);
}, [params.id, evPreload]);
return link;
}

View File

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