+
{content()}
)
diff --git a/src/Element/NoteCreator.css b/src/Element/NoteCreator.css
index 47d9ea30..ad0a7d62 100644
--- a/src/Element/NoteCreator.css
+++ b/src/Element/NoteCreator.css
@@ -116,7 +116,7 @@
@media (min-width: 1020px) {
.note-create-button {
- right: 25vw;
+ right: calc(50% - 360px);
}
}
diff --git a/src/Element/NoteFooter.tsx b/src/Element/NoteFooter.tsx
index 27bea558..60e00165 100644
--- a/src/Element/NoteFooter.tsx
+++ b/src/Element/NoteFooter.tsx
@@ -168,7 +168,7 @@ export default function NoteFooter(props: NoteFooterProps) {
body: JSON.stringify({
q: ev.Content,
source: "auto",
- target: "en"
+ target: lang,
}),
headers: { "Content-Type": "application/json" }
});
diff --git a/src/Element/NoteGhost.tsx b/src/Element/NoteGhost.tsx
index 6eaa268a..4b51df60 100644
--- a/src/Element/NoteGhost.tsx
+++ b/src/Element/NoteGhost.tsx
@@ -2,8 +2,9 @@ import "./Note.css";
import ProfileImage from "Element/ProfileImage";
export default function NoteGhost(props: any) {
+ const className = `note card ${props.className ? props.className : ''}`
return (
-
+
@@ -14,4 +15,4 @@ export default function NoteGhost(props: any) {
);
-}
\ No newline at end of file
+}
diff --git a/src/Element/NoteToSelf.css b/src/Element/NoteToSelf.css
index b33de862..e5f49c2d 100644
--- a/src/Element/NoteToSelf.css
+++ b/src/Element/NoteToSelf.css
@@ -25,11 +25,6 @@
text-decoration: none;
}
-.nts a:hover {
- text-decoration: underline;
- text-decoration-color: var(--gray-superlight);
-}
-
.nts .name {
margin-top: -.2em;
display: flex;
diff --git a/src/Element/ProfileImage.css b/src/Element/ProfileImage.css
index 4c83accc..6143ad96 100644
--- a/src/Element/ProfileImage.css
+++ b/src/Element/ProfileImage.css
@@ -6,6 +6,7 @@
.pfp .avatar-wrapper {
margin-right: 8px;
+ z-index: 2;
}
.pfp .avatar {
@@ -15,7 +16,7 @@
}
.pfp a {
- text-decoration: none;
+ text-decoration: none;
}
.pfp .username {
diff --git a/src/Element/ShowMore.css b/src/Element/ShowMore.css
new file mode 100644
index 00000000..fcfc4b34
--- /dev/null
+++ b/src/Element/ShowMore.css
@@ -0,0 +1,14 @@
+.show-more {
+ background: none;
+ border: none;
+ color: var(--highlight);
+ font-weight: normal;
+}
+
+.show-more:hover {
+ color: var(--highlight);
+ background: none;
+ border: none;
+ font-weight: normal;
+ text-decoration: underline;
+}
diff --git a/src/Element/ShowMore.tsx b/src/Element/ShowMore.tsx
new file mode 100644
index 00000000..666e02b5
--- /dev/null
+++ b/src/Element/ShowMore.tsx
@@ -0,0 +1,20 @@
+import './ShowMore.css'
+
+interface ShowMoreProps {
+ text?: string
+ className?: string
+ onClick: () => void
+}
+
+const ShowMore = ({ text = "Show more", onClick, className = "" }: ShowMoreProps) => {
+ const classNames = className ? `show-more ${className}` : "show-more"
+ return (
+
+
+
+ )
+}
+
+export default ShowMore
diff --git a/src/Element/Tabs.css b/src/Element/Tabs.css
new file mode 100644
index 00000000..2b854d91
--- /dev/null
+++ b/src/Element/Tabs.css
@@ -0,0 +1,36 @@
+.tabs {
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ overflow-x: auto;
+ scrollbar-width: none; /* Firefox */
+ margin-bottom: 18px;
+}
+
+.tabs::-webkit-scrollbar{
+ display: none;
+}
+
+.tab {
+ border: 1px solid var(--gray-secondary);
+ border-radius: 16px;
+ text-align: center;
+ font-weight: 600;
+ line-height: 19px;
+ padding: 8px 12px;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 17px;
+ color: #A3A3A3;
+ margin-right: 12px;
+}
+
+.tab.active {
+ border-color: var(--font-color);
+ color: var(--font-color);
+}
+
+
+.tabs>div {
+ cursor: pointer;
+}
diff --git a/src/Element/Tabs.tsx b/src/Element/Tabs.tsx
new file mode 100644
index 00000000..b1703ffb
--- /dev/null
+++ b/src/Element/Tabs.tsx
@@ -0,0 +1,39 @@
+import './Tabs.css'
+
+export interface Tab {
+ text: string, value: number
+}
+
+interface TabsProps {
+ tabs: Tab[]
+ tab: Tab
+ setTab: (t: Tab) => void
+}
+
+interface TabElementProps extends Omit
{
+ t: Tab
+}
+
+export const TabElement = ({ t, tab, setTab }: TabElementProps) => {
+ return (
+ setTab(t)}>
+ {t.text}
+
+ )
+}
+
+const Tabs = ({ tabs, tab, setTab }: TabsProps) => {
+ return (
+
+ {tabs.map((t) => {
+ return (
+
setTab(t)}>
+ {t.text}
+
+ )
+ })}
+
+ )
+}
+
+export default Tabs
diff --git a/src/Element/Thread.css b/src/Element/Thread.css
index c6267f18..d8cef828 100644
--- a/src/Element/Thread.css
+++ b/src/Element/Thread.css
@@ -1,3 +1,166 @@
.thread-container {
margin: 12px 0 150px 0;
}
+
+.thread-container .hidden-note {
+ margin: 0;
+ border-radius: 0;
+}
+
+.thread-root.note {
+ box-shadow: none;
+}
+
+.thread-root.note > .body {
+ margin-top: 8px;
+ padding-left: 8px;
+}
+
+.thread-root.note > .body .text {
+ font-size: 19px;
+}
+
+.thread-root.note > .footer {
+ padding-left: 0;
+}
+
+.thread-root.note {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ margin-bottom: 0;
+}
+
+.thread-note.note {
+ border-radius: 0;
+ margin-bottom: 0;
+}
+
+.light .thread-note.note.card {
+ box-shadow: none;
+}
+
+.thread-container .hidden-note {
+ margin: 0;
+ border-radius: 0;
+}
+
+.thread-container .show-more {
+ background: var(--note-bg);
+ padding-left: 76px;
+ width: 100%;
+ text-align: left;
+ border-radius: 0;
+ padding-top: 10px;
+ padding-bottom: 10px;
+}
+
+.subthread-container {
+ position: relative;
+}
+
+.line-container {
+ background: var(--note-bg);
+}
+
+.subthread-container.subthread-multi .line-container:before {
+ content: '';
+ position: absolute;
+ left: 36px;
+ top: 48px;
+ border-left: 1px solid var(--gray-superdark);
+ height: 100%;
+}
+
+@media (min-width: 720px) {
+ .subthread-container.subthread-multi .line-container:before {
+ left: 48px;
+ }
+}
+
+.subthread-container.subthread-mid:not(.subthread-last) .line-container:after {
+ content: '';
+ position: absolute;
+ left: 36px;
+ top: 48px;
+ border-left: 1px solid var(--gray-superdark);
+ height: 100%;
+}
+
+@media (min-width: 720px) {
+ .subthread-container.subthread-mid:not(.subthread-last) .line-container:after {
+ left: 48px;
+ }
+}
+
+.subthread-container.subthread-mid:not(.subthread-last) .line-container:after {
+ content: '';
+ position: absolute;
+ border-left: 1px solid var(--gray-superdark);
+ left: 36px;
+ top: 0;
+ height: 48px;
+}
+
+@media (min-width: 720px) {
+ .subthread-container.subthread-mid:not(.subthread-last) .line-container:after {
+ left: 48px;
+ }
+}
+
+.subthread-container.subthread-last .line-container:before {
+ content: '';
+ position: absolute;
+ border-left: 1px solid var(--gray-superdark);
+ left: 36px;
+ top: 0;
+ height: 48px;
+}
+
+@media (min-width: 720px) {
+ .subthread-container.subthread-last .line-container:before {
+ left: 48px;
+ }
+}
+
+.divider-container {
+ background: var(--note-bg);
+}
+
+.divider {
+ height: 1px;
+ background: var(--gray-superdark);
+ margin-left: 28px;
+ margin-right: 22px;
+}
+
+.divider.divider-small {
+ margin-left: 80px;
+}
+
+.thread-container .collapsed, .thread-container .show-more-container {
+ background: var(--note-bg);
+ min-height: 48px;
+}
+
+.thread-note.is-last-note {
+ border-bottom-left-radius: 16px;
+ border-bottom-right-radius: 16px;
+}
+
+.thread-container .collapsed {
+ background-color: var(--note-bg);
+}
+
+.thread-container .hidden-note {
+ padding-left: 48px;
+}
+
+.thread-root.thread-root-single.note {
+ border-bottom-left-radius: 16px;
+ border-bottom-right-radius: 16px;
+}
+
+.thread-root.ghost-root {
+ border-top-left-radius: 16px;
+ border-top-right-radius: 16px;
+}
diff --git a/src/Element/Thread.tsx b/src/Element/Thread.tsx
index 7cf76c65..a4d51497 100644
--- a/src/Element/Thread.tsx
+++ b/src/Element/Thread.tsx
@@ -1,26 +1,278 @@
import "./Thread.css";
-import { useMemo } from "react";
-import { Link } from "react-router-dom";
+import { useMemo, useState, useEffect, ReactNode } from "react";
+import { useSelector } from "react-redux";
+import { useNavigate, useLocation, Link } from "react-router-dom";
-import { TaggedRawEvent, u256 } from "Nostr";
+import { TaggedRawEvent, u256, HexKey } from "Nostr";
import { default as NEvent } from "Nostr/Event";
import EventKind from "Nostr/EventKind";
-import { eventLink } from "Util";
+import { eventLink, hexToBech32, bech32ToHex } from "Util";
import BackButton from "Element/BackButton";
import Note from "Element/Note";
import NoteGhost from "Element/NoteGhost";
+import Collapsed from "Element/Collapsed";
+import type { RootState } from "State/Store";
+
+function getParent(ev: HexKey, chains: Map): HexKey | undefined {
+ for (let [k, vs] of chains.entries()) {
+ const fs = vs.map(a => a.Id)
+ if (fs.includes(ev)) {
+ return k
+ }
+ }
+}
+
+interface DividerProps {
+ variant?: "regular" | "small"
+}
+
+const Divider = ({ variant = "regular" }: DividerProps) => {
+ const className = variant === "small" ? "divider divider-small" : "divider"
+ return (
+
+ )
+}
+
+interface SubthreadProps {
+ isLastSubthread?: boolean
+ from: u256
+ active: u256
+ path: u256[]
+ notes: NEvent[]
+ related: TaggedRawEvent[]
+ chains: Map
+ onNavigate: (e: u256) => void
+}
+
+const Subthread = ({ active, path, from, notes, related, chains, onNavigate }: SubthreadProps) => {
+ const renderSubthread = (a: NEvent, idx: number) => {
+ const isLastSubthread = idx === notes.length - 1
+ const replies = getReplies(a.Id, chains)
+ return (
+ <>
+ 0 ? 'subthread-multi' : ''}`}>
+
+
+
+
+
+ {replies.length > 0 && (
+
+ )}
+ >
+ )
+ }
+
+ return (
+
+ {notes.map(renderSubthread)}
+
+ )
+}
+
+interface ThreadNoteProps extends Omit {
+ note: NEvent
+ isLast: boolean
+}
+
+const ThreadNote = ({ active, note, isLast, path, isLastSubthread, from, related, chains, onNavigate }: ThreadNoteProps) => {
+ const replies = getReplies(note.Id, chains)
+ const activeInReplies = replies.map(r => r.Id).includes(active)
+ const [collapsed, setCollapsed] = useState(!activeInReplies)
+ const hasMultipleNotes = replies.length > 0
+ const isLastVisibleNote = isLastSubthread && isLast && !hasMultipleNotes
+ const className = `subthread-container ${isLast && collapsed ? 'subthread-last' : 'subthread-multi subthread-mid'}`
+ return (
+ <>
+
+ {replies.length > 0 && (
+ activeInReplies ? (
+
+ ) : (
+
+
+
+ )
+ )}
+ >
+ )
+}
+
+const TierTwo = ({ active, isLastSubthread, path, from, notes, related, chains, onNavigate }: SubthreadProps) => {
+ const [first, ...rest] = notes
+
+ return (
+ <>
+
+
+ {rest.map((r: NEvent, idx: number) => {
+ const lastReply = idx === rest.length - 1
+ return (
+
+ )
+ })
+ }
+
+ >
+ )
+}
+
+const TierThree = ({ active, path, isLastSubthread, from, notes, related, chains, onNavigate }: SubthreadProps) => {
+ const [first, ...rest] = notes
+ const replies = getReplies(first.Id, chains)
+ const activeInReplies = notes.map(r => r.Id).includes(active) || replies.map(r => r.Id).includes(active)
+ const hasMultipleNotes = rest.length > 0 || replies.length > 0
+ const isLast = replies.length === 0 && rest.length === 0
+ return (
+ <>
+
+
+ {path.length <= 1 || !activeInReplies ? (
+ replies.length > 0 && (
+
+
+
+ )
+ ) : (
+ replies.length > 0 && (
+
+ )
+ )}
+
+ {rest.map((r: NEvent, idx: number) => {
+ const lastReply = idx === rest.length - 1
+ const lastNote = isLastSubthread && lastReply
+ return (
+
+ )
+ })
+ }
+
+ >
+ )
+}
+
export interface ThreadProps {
this?: u256,
notes?: TaggedRawEvent[]
}
+
export default function Thread(props: ThreadProps) {
- const thisEvent = props.this;
const notes = props.notes ?? [];
const parsedNotes = notes.map(a => new NEvent(a));
-
// root note has no thread info
const root = useMemo(() => parsedNotes.find(a => a.Thread === null), [notes]);
+ const [path, setPath] = useState([])
+ const currentId = path.length > 0 && path[path.length - 1]
+ const currentRoot = useMemo(() => parsedNotes.find(a => a.Id === currentId), [notes, currentId]);
+ const [navigated, setNavigated] = useState(false)
+ const navigate = useNavigate()
+ const isSingleNote = parsedNotes.filter(a => a.Kind === EventKind.TextNote).length === 1
+ const location = useLocation()
+ const urlNoteId = location?.pathname.slice(3)
+ const urlNoteHex = urlNoteId && bech32ToHex(urlNoteId)
+ const rootNoteId = root && hexToBech32('note', root.Id)
const chains = useMemo(() => {
let chains = new Map();
@@ -40,70 +292,102 @@ export default function Thread(props: ThreadProps) {
return chains;
}, [notes]);
+ useEffect(() => {
+ if (!root) {
+ return
+ }
+
+ if (navigated) {
+ return
+ }
+
+ if (root.Id === urlNoteHex) {
+ setPath([root.Id])
+ setNavigated(true)
+ return
+ }
+
+ let subthreadPath = []
+ let parent = getParent(urlNoteHex, chains)
+ while (parent) {
+ subthreadPath.unshift(parent)
+ parent = getParent(parent, chains)
+ }
+ setPath(subthreadPath)
+ setNavigated(true)
+ }, [root, navigated, urlNoteHex, chains])
+
const brokenChains = useMemo(() => {
return Array.from(chains?.keys()).filter(a => !parsedNotes?.some(b => b.Id === a));
}, [chains]);
- const mentionsRoot = useMemo(() => {
- return parsedNotes?.filter(a => a.Kind === EventKind.TextNote && a.Thread)
- }, [chains]);
-
- function renderRoot() {
- if (root) {
- return
+ function renderRoot(note: NEvent) {
+ const className = `thread-root ${isSingleNote ? 'thread-root-single' : ''}`
+ if (note) {
+ return
} else {
- return
+ return (
+
Loading thread root.. ({notes?.length} notes loaded)
-
+
+ )
}
}
- function renderChain(from: u256) {
- if (from && chains) {
- let replies = chains.get(from);
- if (replies) {
- return (
-
- {replies.map(a => {
- return (
- <>
-
- {renderChain(a.Id)}
- >
- )
- })}
-
- )
- }
+ function onNavigate(to: u256) {
+ setPath([...path, to])
+ }
+
+ function renderChain(from: u256): ReactNode {
+ if (!from || !chains) {
+ return
}
+ let replies = chains.get(from);
+ if (replies) {
+ return
+ }
+ }
+
+ function goBack() {
+ if (path.length > 1) {
+ const newPath = path.slice(0, path.length - 1)
+ setPath(newPath)
+ } else {
+ navigate("/")
+ }
}
return (
- <>
-
-
- {renderRoot()}
- {root ? renderChain(root.Id) : null}
- {root ? null : <>
-
Other Replies
- {brokenChains.map(a => {
- return (
- <>
-
- Missing event {a.substring(0, 8)}
-
- {renderChain(a)}
- >
- )
- })}
- >}
-
- >
+
+
1 ? "Parent" : "Back"} />
+
+ {currentRoot && renderRoot(currentRoot)}
+ {currentRoot && renderChain(currentRoot.Id)}
+ {currentRoot === root && (
+ <>
+ {brokenChains.length > 0 &&
Other replies
}
+ {brokenChains.map(a => {
+ return (
+
+
+ Missing event {a.substring(0, 8)}
+
+ {renderChain(a)}
+
+ )
+ })}
+ >
+ )}
+
+
);
}
+
+function getReplies(from: u256, chains?: Map): NEvent[] {
+ if (!from || !chains) {
+ return []
+ }
+ let replies = chains.get(from);
+ return replies ? replies : []
+}
+
diff --git a/src/Element/Zap.css b/src/Element/Zap.css
index 1e2846e3..5de0871b 100644
--- a/src/Element/Zap.css
+++ b/src/Element/Zap.css
@@ -55,6 +55,10 @@
margin-left: 52px;
}
+.note.thread-root .zaps-summary {
+ margin-left: 14px;
+}
+
.top-zap {
font-size: 14px;
border: none;
@@ -83,7 +87,3 @@
.amount-number {
font-weight: bold;
}
-
-.note.zap > .body {
- margin-bottom: 0;
-}
diff --git a/src/Element/Zap.tsx b/src/Element/Zap.tsx
index 41a22268..ef1df7e2 100644
--- a/src/Element/Zap.tsx
+++ b/src/Element/Zap.tsx
@@ -131,16 +131,6 @@ export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
)}
zapped
-