feat: modular right bar
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
kieran 2024-09-18 15:37:59 +01:00
parent b38b6b27ef
commit 290dedb333
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
14 changed files with 262 additions and 92 deletions

View File

@ -1,6 +1,6 @@
import "./ZapButton.css";
import { HexKey } from "@snort/system";
import { HexKey, NostrLink } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { useState } from "react";
@ -17,7 +17,7 @@ const ZapButton = ({
pubkey: HexKey;
lnurl?: string;
children?: React.ReactNode;
event?: string;
event?: NostrLink;
}) => {
const profile = useUserProfile(pubkey);
const [zap, setZap] = useState(false);
@ -37,12 +37,11 @@ const ZapButton = ({
value: service,
weight: 1,
name: profile?.display_name || profile?.name,
zap: { pubkey: pubkey },
zap: { pubkey: pubkey, event },
} as ZapTarget,
]}
show={zap}
onClose={() => setZap(false)}
note={event}
/>
</>
);

View File

@ -1,50 +1,30 @@
import { unixNow } from "@snort/shared";
import { EventKind, NostrEvent, NostrLink, RequestBuilder } from "@snort/system";
import { useRequestBuilder, useUserProfile } from "@snort/system-react";
import { CSSProperties, useMemo } from "react";
import { NostrEvent, NostrLink } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import classNames from "classnames";
import { CSSProperties } from "react";
import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";
import useImgProxy from "@/Hooks/useImgProxy";
import useLiveStreams from "@/Hooks/useLiveStreams";
import { findTag } from "@/Utils";
import { Hour } from "@/Utils/Const";
import Avatar from "../User/Avatar";
export function LiveStreams() {
const sub = useMemo(() => {
const rb = new RequestBuilder("streams");
rb.withFilter()
.kinds([EventKind.LiveEvent])
.since(unixNow() - Hour);
rb.withFilter()
.kinds([EventKind.LiveEvent])
.since(unixNow() - Hour);
return rb;
}, []);
const streams = useRequestBuilder(sub);
const streams = useLiveStreams();
if (streams.length === 0) return null;
return (
<div className="flex mx-2 gap-4 overflow-x-auto sm-hide-scrollbar">
{streams
.filter(a => {
return findTag(a, "status") === "live";
})
.sort((a, b) => {
const sA = Number(findTag(a, "starts"));
const sB = Number(findTag(b, "starts"));
return sA > sB ? -1 : 1;
})
.map(v => (
<LiveStreamEvent ev={v} key={`${v.kind}:${v.pubkey}:${findTag(v, "d")}`} />
))}
{streams.map(v => (
<LiveStreamEvent ev={v} key={`${v.kind}:${v.pubkey}:${findTag(v, "d")}`} className="h-[80px]" />
))}
</div>
);
}
function LiveStreamEvent({ ev }: { ev: NostrEvent }) {
export function LiveStreamEvent({ ev, className }: { ev: NostrEvent; className?: string }) {
const { proxy } = useImgProxy();
const title = findTag(ev, "title");
const image = findTag(ev, "image");
@ -57,7 +37,7 @@ function LiveStreamEvent({ ev }: { ev: NostrEvent }) {
const imageProxy = proxy(image ?? "");
return (
<Link className="flex gap-2 h-[80px]" to={`https://zap.stream/${link}`} target="_blank">
<Link className={classNames("flex gap-2", className)} to={`https://zap.stream/${link}`} target="_blank">
<div className="relative aspect-video">
<div
className="absolute h-full w-full bg-center bg-cover bg-gray-ultradark rounded-lg"

View File

@ -0,0 +1,31 @@
import { ReactNode } from "react";
import Icon from "../Icons/Icon";
export interface BaseWidgetProps {
title?: ReactNode;
icon?: string;
iconClassName?: string;
children?: ReactNode;
contextMenu?: ReactNode;
}
export function BaseWidget({ children, title, icon, iconClassName, contextMenu }: BaseWidgetProps) {
return (
<div className="br p bg-gray-ultradark">
{title && (
<div className="flex justify-between items-center">
<div className="flex gap-2 items-center text-xl text-white font-semibold mb-1">
{icon && (
<div className="p-2 bg-gray-dark rounded-full">
<Icon name={icon} className={iconClassName} />
</div>
)}
<div>{title}</div>
</div>
{contextMenu}
</div>
)}
{children}
</div>
);
}

View File

@ -0,0 +1,9 @@
export enum RightColumnWidget {
TaskList,
TrendingNotes,
TrendingPeople,
TrendingHashtags,
TrendingArticls,
LiveStreams,
InviteFriends,
}

View File

@ -0,0 +1,43 @@
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import SnortApi, { RefCodeResponse } from "@/External/SnortApi";
import { useCopy } from "@/Hooks/useCopy";
import useEventPublisher from "@/Hooks/useEventPublisher";
import AsyncButton from "../Button/AsyncButton";
import Icon from "../Icons/Icon";
import { BaseWidget } from "./base";
export default function InviteFriendsWidget() {
const [refCode, setRefCode] = useState<RefCodeResponse>();
const { publisher } = useEventPublisher();
const api = new SnortApi(undefined, publisher);
const copy = useCopy();
async function loadRefCode() {
const c = await api.getRefCode();
setRefCode(c);
}
useEffect(() => {
loadRefCode();
}, [publisher]);
return (
<BaseWidget
title={<FormattedMessage defaultMessage="Invite Friends" />}
icon="heart-solid"
iconClassName="text-heart">
<div className="flex flex-col gap-2">
<FormattedMessage defaultMessage="Share a personalized invitation with friends!" />
<div>
<AsyncButton onClick={() => copy.copy(`https://${window.location.host}?ref=${refCode?.code}`)}>
<Icon name="copy" />
<FormattedMessage defaultMessage="Copy link" />
</AsyncButton>
</div>
</div>
</BaseWidget>
);
}

View File

@ -0,0 +1,51 @@
import { NostrLink } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import useLiveStreams from "@/Hooks/useLiveStreams";
import { findTag, getDisplayName } from "@/Utils";
import IconButton from "../Button/IconButton";
import ZapButton from "../Event/ZapButton";
import { ProxyImg } from "../ProxyImg";
import Avatar from "../User/Avatar";
import { BaseWidget } from "./base";
export default function MiniStreamWidget() {
const streams = useLiveStreams();
const ev = streams.at(0);
const host = ev?.tags.find(a => a[0] === "p" && a.at(3) === "host")?.at(1) ?? ev?.pubkey;
const hostProfile = useUserProfile(host);
if (!ev) return;
const link = NostrLink.fromEvent(ev);
const image = findTag(ev, "image");
const title = findTag(ev, "title");
return (
<BaseWidget>
<div className="flex flex-col gap-4">
<div className="rounded-xl relative aspect-video w-full overflow-hidden">
<ProxyImg src={image} className="absolute w-full h-full" />
<div className="absolute flex items-center justify-center w-full h-full">
<IconButton
icon={{
name: "play-square-outline",
}}
onClick={() => window.open(`https://zap.stream/${link.encode()}`, "_blank")}
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Avatar pubkey={host ?? ""} user={hostProfile} size={48} />
<div className="flex flex-col">
<div className="text-lg text-white f-ellipsis font-semibold">{title}</div>
<div>{getDisplayName(hostProfile, host!)}</div>
</div>
</div>
<div>{host && <ZapButton pubkey={host} event={link} />}</div>
</div>
</div>
</BaseWidget>
);
}

View File

@ -2,13 +2,25 @@ import { HexKey } from "@snort/system";
import { ReactNode } from "react";
import PageSpinner from "@/Components/PageSpinner";
import FollowListBase from "@/Components/User/FollowListBase";
import FollowListBase, { FollowListBaseProps } from "@/Components/User/FollowListBase";
import NostrBandApi from "@/External/NostrBand";
import useCachedFetch from "@/Hooks/useCachedFetch";
import { ErrorOrOffline } from "../ErrorOrOffline";
export default function TrendingUsers({ title, count = Infinity }: { title?: ReactNode; count?: number }) {
export default function TrendingUsers({
title,
count = Infinity,
followAll = true,
actions,
profileActions,
}: {
title?: ReactNode;
count?: number;
followAll?: boolean;
actions?: FollowListBaseProps["actions"];
profileActions?: FollowListBaseProps["profileActions"];
}) {
const api = new NostrBandApi();
const trendingProfilesUrl = api.trendingProfilesUrl();
const storageKey = `nostr-band-${trendingProfilesUrl}`;
@ -27,5 +39,14 @@ export default function TrendingUsers({ title, count = Infinity }: { title?: Rea
return <PageSpinner />;
}
return <FollowListBase pubkeys={trendingUsersData.slice(0, count) as HexKey[]} showAbout={true} title={title} />;
return (
<FollowListBase
pubkeys={trendingUsersData.slice(0, count) as HexKey[]}
showAbout={true}
title={title}
showFollowAll={followAll}
actions={actions}
profileActions={profileActions}
/>
);
}

View File

@ -1,8 +1,7 @@
import "./ZapModal.css";
import { LNURLSuccessAction } from "@snort/shared";
import { HexKey } from "@snort/system";
import React, { ReactNode, useEffect, useState } from "react";
import { ReactNode, useEffect, useState } from "react";
import CloseButton from "@/Components/Button/CloseButton";
import Modal from "@/Components/Modal/Modal";
@ -23,7 +22,6 @@ export interface SendSatsProps {
invoice?: string; // shortcut to invoice qr tab
title?: ReactNode;
notice?: string;
note?: HexKey;
allocatePool?: boolean;
}

View File

@ -0,0 +1,27 @@
import { unixNow } from "@snort/shared";
import { EventKind, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { useMemo } from "react";
import { findTag } from "@/Utils";
import { Hour } from "@/Utils/Const";
export default function useLiveStreams() {
const sub = useMemo(() => {
const rb = new RequestBuilder("streams");
rb.withFilter()
.kinds([EventKind.LiveEvent])
.since(unixNow() - Hour);
return rb;
}, []);
return useRequestBuilder(sub)
.filter(a => {
return findTag(a, "status") === "live";
})
.sort((a, b) => {
const sA = Number(findTag(a, "starts"));
const sB = Number(findTag(b, "starts"));
return sA > sB ? -1 : 1;
});
}

View File

@ -1,9 +1,15 @@
import classNames from "classnames";
import { FormattedMessage } from "react-intl";
import { RightColumnWidget } from "@/Components/RightWidgets";
import { BaseWidget } from "@/Components/RightWidgets/base";
import InviteFriendsWidget from "@/Components/RightWidgets/invite-friends";
import MiniStreamWidget from "@/Components/RightWidgets/mini-stream";
import SearchBox from "@/Components/SearchBox/SearchBox";
import { TaskList } from "@/Components/Tasks/TaskList";
import TrendingHashtags from "@/Components/Trending/TrendingHashtags";
import TrendingNotes from "@/Components/Trending/TrendingPosts";
import TrendingUsers from "@/Components/Trending/TrendingUsers";
import useLogin from "@/Hooks/useLogin";
export default function RightColumn() {
@ -11,16 +17,44 @@ export default function RightColumn() {
const hideRightColumnPaths = ["/login", "/new", "/messages"];
const show = !hideRightColumnPaths.some(path => globalThis.location.pathname.startsWith(path));
const getTitleMessage = () => {
return pubkey ? (
<FormattedMessage defaultMessage="Trending notes" />
) : (
<FormattedMessage defaultMessage="Trending hashtags" />
);
};
const widgets = pubkey
? [
RightColumnWidget.TaskList,
RightColumnWidget.InviteFriends,
//RightColumnWidget.LiveStreams,
RightColumnWidget.TrendingNotes,
RightColumnWidget.TrendingPeople,
RightColumnWidget.TrendingHashtags,
]
: [RightColumnWidget.TrendingPeople, RightColumnWidget.TrendingHashtags];
const getContent = () => {
return pubkey ? <TrendingNotes small={true} count={100} /> : <TrendingHashtags short={true} />;
const getWidget = (t: RightColumnWidget) => {
switch (t) {
case RightColumnWidget.TaskList:
return <TaskList />;
case RightColumnWidget.TrendingNotes:
return (
<BaseWidget title={<FormattedMessage defaultMessage="Trending Notes" />}>
<TrendingNotes small={true} count={6} />
</BaseWidget>
);
case RightColumnWidget.TrendingPeople:
return (
<BaseWidget title={<FormattedMessage defaultMessage="Trending People" />}>
<TrendingUsers count={6} followAll={false} profileActions={pubkey ? () => undefined : () => <></>} />
</BaseWidget>
);
case RightColumnWidget.TrendingHashtags:
return (
<BaseWidget title={<FormattedMessage defaultMessage="Popular Hashtags" />}>
<TrendingHashtags short={true} count={6} />
</BaseWidget>
);
case RightColumnWidget.InviteFriends:
return <InviteFriendsWidget />;
case RightColumnWidget.LiveStreams:
return <MiniStreamWidget />;
}
};
return (
@ -34,8 +68,7 @@ export default function RightColumn() {
<div>
<SearchBox />
</div>
<div className="font-bold text-xs mt-4 mb-2 uppercase tracking-wide">{getTitleMessage()}</div>
<div className="overflow-y-auto hide-scrollbar flex-grow rounded-lg">{getContent()}</div>
<div className="flex flex-col gap-4 overflow-y-auto">{widgets.map(getWidget)}</div>
</div>
);
}

View File

@ -1,39 +1,17 @@
import { EventKind, NostrEvent, RequestBuilder, TaggedNostrEvent } from "@snort/system";
import { WorkerRelayInterface } from "@snort/worker-relay";
import { memo, useEffect, useMemo, useState } from "react";
import { FormattedMessage } from "react-intl";
import { Link, useNavigationType } from "react-router-dom";
import { useNavigationType } from "react-router-dom";
import { Relay } from "@/Cache";
import { DisplayAs, DisplayAsSelector } from "@/Components/Feed/DisplayAsSelector";
import { TimelineRenderer } from "@/Components/Feed/TimelineRenderer";
import { TaskList } from "@/Components/Tasks/TaskList";
import useTimelineFeed, { TimelineFeedOptions, TimelineSubject } from "@/Feed/TimelineFeed";
import useFollowsControls from "@/Hooks/useFollowControls";
import useHistoryState from "@/Hooks/useHistoryState";
import useLogin from "@/Hooks/useLogin";
import messages from "@/Pages/messages";
import { System } from "@/system";
const FollowsHint = () => {
const publicKey = useLogin(s => s.publicKey);
const { followList } = useFollowsControls();
if (followList.length === 0 && publicKey) {
return (
<FormattedMessage
{...messages.NoFollows}
values={{
newUsersPage: (
<Link to={"/discover"}>
<FormattedMessage {...messages.NewUsers} />
</Link>
),
}}
/>
);
}
};
let forYouFeed = {
events: [] as NostrEvent[],
created_at: 0,
@ -180,8 +158,6 @@ export const ForYouTab = memo(function ForYouTab() {
return (
<>
<DisplayAsSelector activeSelection={displayAs} onSelect={a => setDisplayAs(a)} />
<FollowsHint />
<TaskList />
<TimelineRenderer
frags={frags}
latest={[]}

View File

@ -2,7 +2,6 @@ import { NostrEvent, NostrLink } from "@snort/system";
import { useContext, useMemo } from "react";
import TimelineFollows from "@/Components/Feed/TimelineFollows";
import { TaskList } from "@/Components/Tasks/TaskList";
import { DeckContext } from "@/Pages/Deck/DeckLayout";
export const NotesTab = () => {
@ -18,10 +17,5 @@ export const NotesTab = () => {
return undefined;
}, [deckContext]);
return (
<>
<TaskList />
<TimelineFollows postsOnly={true} noteOnClick={noteOnClick} />
</>
);
return <TimelineFollows postsOnly={true} noteOnClick={noteOnClick} />;
};

View File

@ -306,9 +306,6 @@
"6ewQqw": {
"defaultMessage": "Likes ({n})"
},
"6k7xfM": {
"defaultMessage": "Trending notes"
},
"6mr8WU": {
"defaultMessage": "Followed by"
},
@ -506,9 +503,6 @@
"CYkOCI": {
"defaultMessage": "and {count} others you follow"
},
"CbM2hK": {
"defaultMessage": "Trending hashtags"
},
"CmZ9ls": {
"defaultMessage": "{n} Muted"
},
@ -1151,6 +1145,9 @@
"UrKTqQ": {
"defaultMessage": "You have an active iris.to account"
},
"UsCzPc": {
"defaultMessage": "Share a personalized invitation with friends!"
},
"UxgyeY": {
"defaultMessage": "Your referral code is {code}"
},
@ -1307,6 +1304,9 @@
"abbGKq": {
"defaultMessage": "{n} km"
},
"ak3MTf": {
"defaultMessage": "Invite Friends"
},
"b12Goz": {
"defaultMessage": "Mnemonic"
},
@ -1407,6 +1407,9 @@
"dOQCL8": {
"defaultMessage": "Display name"
},
"ddd3JX": {
"defaultMessage": "Popular Hashtags"
},
"deEeEI": {
"defaultMessage": "Register"
},
@ -1701,6 +1704,9 @@
"lTbT3s": {
"defaultMessage": "Wallet password"
},
"lbr3Lq": {
"defaultMessage": "Copy link"
},
"lfOesV": {
"defaultMessage": "Non-Zap"
},

View File

@ -101,7 +101,6 @@
"6WWD34": "Looking for: {noteId}",
"6bgpn+": "Not all clients support this, you may still receive some zaps as if zap splits was not configured",
"6ewQqw": "Likes ({n})",
"6k7xfM": "Trending notes",
"6mr8WU": "Followed by",
"6pdxsi": "Extra metadata fields and tags",
"6uMqL1": "Unpaid",
@ -167,7 +166,6 @@
"CM0k0d": "Prune follow list",
"CVWeJ6": "Trending People",
"CYkOCI": "and {count} others you follow",
"CbM2hK": "Trending hashtags",
"CmZ9ls": "{n} Muted",
"CsCUYo": "{n} sats",
"Cu/K85": "Translated from {lang}",
@ -381,6 +379,7 @@
"Up5U7K": "Block",
"Ups2/p": "Your application is pending",
"UrKTqQ": "You have an active iris.to account",
"UsCzPc": "Share a personalized invitation with friends!",
"UxgyeY": "Your referral code is {code}",
"V20Og0": "Labeling",
"VOjC1i": "Pick which upload service you want to upload attachments to",
@ -433,6 +432,7 @@
"aSGz4J": "Connect to your own LND node with Lightning Node Connect",
"aWpBzj": "Show more",
"abbGKq": "{n} km",
"ak3MTf": "Invite Friends",
"b12Goz": "Mnemonic",
"b5vAk0": "Your handle will act like a lightning address and will redirect to your chosen LNURL or Lightning address",
"bF1MYT": "You are a community leader and are earning <b>{percent}</b> of referred users subscriptions!",
@ -466,6 +466,7 @@
"d7d0/x": "LN Address",
"dK2CcV": "The public key is like your username, you can share it with anyone.",
"dOQCL8": "Display name",
"ddd3JX": "Popular Hashtags",
"deEeEI": "Register",
"djLctd": "Amount in sats",
"djNL6D": "Read-only",
@ -564,6 +565,7 @@
"lEnclp": "My events: {n}",
"lPWASz": "Snort nostr address",
"lTbT3s": "Wallet password",
"lbr3Lq": "Copy link",
"lfOesV": "Non-Zap",
"lgg1KN": "account page",
"ll3xBp": "Image proxy service",