Progress
This commit is contained in:
34
packages/app/src/Element/FixedTabs.css
Normal file
34
packages/app/src/Element/FixedTabs.css
Normal 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);
|
||||
}
|
6
packages/app/src/Element/FixedTabs.tsx
Normal file
6
packages/app/src/Element/FixedTabs.tsx
Normal 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>;
|
||||
}
|
54
packages/app/src/Element/LiveStreams.css
Normal file
54
packages/app/src/Element/LiveStreams.css
Normal 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;
|
||||
}
|
58
packages/app/src/Element/LiveStreams.tsx
Normal file
58
packages/app/src/Element/LiveStreams.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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%);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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))}>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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">
|
||||
|
@ -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} />
|
||||
))}
|
||||
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
|
Reference in New Issue
Block a user