This commit is contained in:
2023-07-19 16:21:21 +01:00
parent 06b9dba09b
commit d292e658fc
23 changed files with 496 additions and 238 deletions

View File

@ -0,0 +1,34 @@
.fixed-tabs {
display: flex;
align-items: center;
flex-direction: row;
white-space: nowrap;
text-align: center;
user-select: none;
}
.fixed-tabs > a {
flex: 1;
padding: 16px;
color: var(--font-tertiary-color);
font-weight: 500;
font-size: 16px;
letter-spacing: 0.2px;
cursor: pointer;
text-decoration: none;
}
.fixed-tabs > a.active {
border-bottom: 1px solid var(--highlight);
color: var(--font-color);
}
.fixed-tabs > a.disabled {
opacity: 0.3;
cursor: not-allowed;
pointer-events: none;
}
.fixed-tabs > a:hover {
border-color: var(--highlight);
}

View File

@ -0,0 +1,6 @@
import "./FixedTabs.css";
import { ReactNode } from "react";
export function FixedTabs({ children }: { children: ReactNode }) {
return <div className="fixed-tabs">{children}</div>;
}

View File

@ -0,0 +1,54 @@
.stream-list {
display: flex;
gap: 4px;
overflow-x: auto;
}
.stream-list::-webkit-scrollbar {
height: 6.25px;
}
.stream-event {
display: flex;
padding: 8px 12px;
gap: 8px;
text-decoration: none;
}
.stream-event > div:first-of-type {
border-radius: 8px;
height: 49px;
width: 65px;
background-color: var(--gray-light);
background-image: var(--img);
background-position: center;
background-size: cover;
}
.stream-event span.live {
display: flex;
padding: 4px 6px;
justify-content: center;
align-items: center;
gap: 4px;
border-radius: 9px;
background: var(--live);
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
}
.stream-event .details .reactions {
color: var(--font-secondary-color);
}
.stream-event .details > div:nth-of-type(2) {
width: 100px;
min-width: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 16px;
font-weight: 600;
line-height: 24px;
}

View File

@ -0,0 +1,58 @@
import "./LiveStreams.css";
import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system";
import { findTag } from "SnortUtils";
import { CSSProperties, useMemo } from "react";
import { Link } from "react-router-dom";
import useImgProxy from "Hooks/useImgProxy";
import Icon from "Icons/Icon";
export function LiveStreams({ evs }: { evs: Array<NostrEvent> }) {
const streams = useMemo(() => {
return [...evs].sort((a, b) => {
const aStarts = Number(findTag(a, "starts") ?? a.created_at);
const bStarts = Number(findTag(b, "starts") ?? b.created_at);
return aStarts > bStarts ? -1 : 1;
});
}, [evs]);
if (streams.length === 0) return null;
return (
<div className="stream-list">
{streams.map(v => (
<LiveStreamEvent ev={v} key={`${v.kind}:${v.pubkey}:${findTag(v, "d")}`} />
))}
</div>
);
}
function LiveStreamEvent({ ev }: { ev: NostrEvent }) {
const { proxy } = useImgProxy();
const title = findTag(ev, "title");
const image = findTag(ev, "image");
const status = findTag(ev, "status");
const link = encodeTLV(NostrPrefix.Address, findTag(ev, "d") ?? "", undefined, ev.kind, ev.pubkey);
const imageProxy = proxy(image ?? "");
return (
<Link className="stream-event" to={`https://zap.stream/${link}`} target="_blank">
<div
style={
{
"--img": `url(${imageProxy})`,
} as CSSProperties
}></div>
<div className="flex f-col details">
<div className="flex g2">
<span className="live">{status}</span>
<div className="reaction-pill">
<Icon name="zap" size={24} />
<div className="reaction-pill-number">0</div>
</div>
</div>
<div>{title}</div>
</div>
</Link>
);
}

View File

@ -62,9 +62,9 @@
}
.note-quote {
border: 1px solid var(--gray);
border-radius: 10px;
padding: 5px;
border: 1px solid var(--gray-superdark);
border-radius: 12px;
padding: 8px 16px 16px 16px;
}
.note > .body .text-frag {
@ -91,54 +91,6 @@
}
}
.note .ctx-menu {
color: var(--font-secondary-color);
background: transparent;
box-shadow: 0px 8px 20px rgba(0, 0, 0, 0.4);
min-width: 0;
margin: 0;
padding: 0;
border-radius: 16px;
}
.note .ctx-menu li {
background: #1e1e1e;
padding-top: 8px;
padding-bottom: 8px;
display: grid;
grid-template-columns: 2rem auto;
}
.light .note .ctx-menu li {
background: var(--note-bg);
}
.note .ctx-menu li:first-of-type {
padding-top: 12px;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
}
.note .ctx-menu li:last-of-type {
padding-bottom: 12px;
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
}
.note .ctx-menu li:hover {
color: white;
background: #2a2a2a;
}
.light .note .ctx-menu li:hover {
color: white;
background: var(--font-secondary-color);
}
.ctx-menu .red {
color: var(--error);
}
.note > .header img:hover,
.note > .header .name > .reply:hover {
cursor: pointer;
@ -250,14 +202,6 @@
text-decoration: underline;
}
.close-menu {
position: absolute;
width: 100vw;
height: 100vh;
top: -400px;
left: -600px;
}
.close-menu-container {
position: absolute;
.note .body > .text > a {
color: var(--highlight);
}

View File

@ -1,46 +0,0 @@
.skeleton {
display: inline-block;
height: 1em;
position: relative;
overflow: hidden;
background-color: var(--note-bg);
border-radius: 16px;
}
html.light .skeleton {
background-color: var(--gray-secondary);
}
.skeleton::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
transform: translateX(-100%);
background-image: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0,
rgba(255, 255, 255, 0.02) 20%,
rgba(255, 255, 255, 0.05) 60%,
rgba(255, 255, 255, 0)
);
animation: shimmer 2s infinite;
content: "";
}
html.light .skeleton::after {
background-image: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0,
rgba(255, 255, 255, 0.2) 20%,
rgba(255, 255, 255, 0.5) 60%,
rgba(255, 255, 255, 0)
);
}
@keyframes shimmer {
100% {
transform: translateX(100%);
}
}

View File

@ -1,21 +0,0 @@
import "./Skeleton.css";
interface ISkepetonProps {
children?: React.ReactNode;
loading?: boolean;
width?: string;
height?: string;
margin?: string;
}
export default function Skeleton({ children, width, height, margin, loading = true }: ISkepetonProps) {
if (!loading) {
return <>{children}</>;
}
return (
<div className="skeleton" style={{ width: width, height: height, margin: margin }}>
{children}
</div>
);
}

View File

@ -55,9 +55,6 @@ export default function SuggestedProfiles() {
return (
<>
<h3>
<FormattedMessage defaultMessage="Suggested Follows" />
</h3>
<div className="card flex f-space">
<FormattedMessage defaultMessage="Provider" />
<select onChange={e => setProvider(Number(e.target.value))}>

View File

@ -2,9 +2,11 @@
display: flex;
align-items: center;
flex-direction: row;
overflow-x: scroll;
-ms-overflow-style: none; /* for Internet Explorer, Edge */
scrollbar-width: none; /* Firefox */
margin-bottom: 18px;
white-space: nowrap;
text-align: center;
user-select: none;
}
.tabs::-webkit-scrollbar {
@ -12,16 +14,22 @@
}
.tab {
flex: 1;
padding: 16px;
color: var(--font-tertiary-color);
font-weight: 500;
font-size: 16px;
letter-spacing: 0.2px;
border: 1px solid var(--border-color);
border-radius: 16px;
font-weight: 600;
font-size: 14px;
padding: 6px 12px;
text-align: center;
font-feature-settings: "tnum";
}
.tab:not(:last-of-type) {
margin-right: 8px;
}
.tab.active {
border-bottom: 1px solid var(--highlight);
border-color: var(--font-color);
color: var(--font-color);
}
@ -36,5 +44,5 @@
}
.tab:hover {
border-color: var(--highlight);
border-color: var(--font-color);
}

View File

@ -5,7 +5,6 @@ export interface Tab {
text: string;
value: number;
disabled?: boolean;
data?: string;
}
interface TabsProps {
@ -33,7 +32,7 @@ const Tabs = ({ tabs, tab, setTab }: TabsProps) => {
return (
<div className="tabs" ref={horizontalScroll}>
{tabs.map(t => (
<TabElement key={t.value} tab={tab} setTab={setTab} t={t} />
<TabElement tab={tab} setTab={setTab} t={t} />
))}
</div>
);

View File

@ -14,6 +14,7 @@ import NoteReaction from "Element/NoteReaction";
import useModeration from "Hooks/useModeration";
import ProfilePreview from "Element/ProfilePreview";
import { UserCache } from "Cache";
import { LiveStreams } from "Element/LiveStreams";
export interface TimelineProps {
postsOnly: boolean;
@ -44,7 +45,7 @@ const Timeline = (props: TimelineProps) => {
const filterPosts = useCallback(
(nts: readonly TaggedNostrEvent[]) => {
const a = [...nts];
const a = [...nts.filter(a => a.kind !== EventKind.LiveEvent)];
props.noSort || a.sort((a, b) => b.created_at - a.created_at);
return a
?.filter(a => (props.postsOnly ? !a.tags.some(b => b[0] === "e") : true))
@ -65,6 +66,10 @@ const Timeline = (props: TimelineProps) => {
},
[feed.related]
);
const liveStreams = useMemo(() => {
return (feed.main ?? []).filter(a => a.kind === EventKind.LiveEvent && findTag(a, "status") === "live");
}, [feed]);
const findRelated = useCallback(
(id?: u256) => {
if (!id) return undefined;
@ -138,6 +143,7 @@ const Timeline = (props: TimelineProps) => {
)}
</>
)}
<LiveStreams evs={liveStreams} />
{mainFeed.map(eventElement)}
{(props.loadMore === undefined || props.loadMore === true) && (
<div className="flex f-center">

View File

@ -1,6 +1,5 @@
import { useEffect, useState } from "react";
import { NostrEvent, TaggedNostrEvent } from "@snort/system";
import { FormattedMessage } from "react-intl";
import PageSpinner from "Element/PageSpinner";
import Note from "Element/Note";
@ -23,9 +22,6 @@ export default function TrendingNotes() {
return (
<>
<h3>
<FormattedMessage defaultMessage="Trending Notes" />
</h3>
{posts.map(e => (
<Note key={e.id} data={e as TaggedNostrEvent} related={[]} depth={0} />
))}

View File

@ -1,6 +1,5 @@
import { useEffect, useState } from "react";
import { HexKey } from "@snort/system";
import { FormattedMessage } from "react-intl";
import FollowListBase from "Element/FollowListBase";
import PageSpinner from "Element/PageSpinner";
@ -24,9 +23,6 @@ export default function TrendingUsers() {
return (
<>
<h3>
<FormattedMessage defaultMessage="Trending People" />
</h3>
<FollowListBase pubkeys={userList} showAbout={true} />
</>
);