diff --git a/src/Element/BackButton.tsx b/src/Element/BackButton.tsx index 25564c4..85b772e 100644 --- a/src/Element/BackButton.tsx +++ b/src/Element/BackButton.tsx @@ -1,15 +1,22 @@ import "./BackButton.css" -import { useNavigate } from "react-router-dom"; - import ArrowBack from "Icons/ArrowBack"; -const BackButton = () => { - const navigate = useNavigate() +interface BackButtonProps { + text?: string + onClick?(): void +} + +const BackButton = ({ text = "Back", onClick }: BackButtonProps) => { + const onClickHandler = () => { + if (onClick) { + onClick() + } + } return ( - ) } diff --git a/src/Element/Collapsed.tsx b/src/Element/Collapsed.tsx new file mode 100644 index 0000000..2649217 --- /dev/null +++ b/src/Element/Collapsed.tsx @@ -0,0 +1,24 @@ +import { useState, ReactNode } from "react"; + +import ShowMore from "Element/ShowMore"; + +interface CollapsedProps { + text?: string + children: ReactNode + collapsed: boolean + setCollapsed(b: boolean): void +} + +const Collapsed = ({ text, children, collapsed, setCollapsed }: CollapsedProps) => { + return collapsed ? ( +
+ setCollapsed(false)} /> +
+ ) : ( +
+ {children} +
+ ) +} + +export default Collapsed diff --git a/src/Element/Copy.css b/src/Element/Copy.css index 2c99df1..f85044a 100644 --- a/src/Element/Copy.css +++ b/src/Element/Copy.css @@ -1,9 +1,14 @@ .copy { cursor: pointer; + align-items: center; } .copy .body { font-size: var(--font-size-small); color: var(--font-color); - margin-right: 8px; + margin-right: 6px; +} + +.copy .icon { + margin-bottom: -4px; } diff --git a/src/Element/Copy.tsx b/src/Element/Copy.tsx index a2e557b..80ef926 100644 --- a/src/Element/Copy.tsx +++ b/src/Element/Copy.tsx @@ -1,6 +1,6 @@ import "./Copy.css"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faCopy, faCheck } from "@fortawesome/free-solid-svg-icons"; +import Check from "Icons/Check"; +import CopyIcon from "Icons/Copy"; import { useCopy } from "useCopy"; export interface CopyProps { @@ -15,13 +15,11 @@ export default function Copy({ text, maxSize = 32 }: CopyProps) { return (
copy(text)}> - {trimmed} + {trimmed} + + + {copied ? : } -
) } diff --git a/src/Element/FollowButton.css b/src/Element/FollowButton.css index 8c27ce2..66f0552 100644 --- a/src/Element/FollowButton.css +++ b/src/Element/FollowButton.css @@ -1,3 +1,2 @@ .follow-button { - width: 92px; } diff --git a/src/Element/FollowsYou.css b/src/Element/FollowsYou.css index 744aa21..2b1cf5e 100644 --- a/src/Element/FollowsYou.css +++ b/src/Element/FollowsYou.css @@ -1,5 +1,5 @@ .follows-you { - color: var(--gray-light); + color: var(--font-secondary-color); font-size: var(--font-size-tiny); margin-left: .2em; font-weight: normal diff --git a/src/Element/MuteButton.tsx b/src/Element/MuteButton.tsx index 8d34475..da981bf 100644 --- a/src/Element/MuteButton.tsx +++ b/src/Element/MuteButton.tsx @@ -12,7 +12,7 @@ const MuteButton = ({ pubkey }: MuteButtonProps) => { Unmute ) : ( - ) diff --git a/src/Element/Note.css b/src/Element/Note.css index cdabe87..20b67e0 100644 --- a/src/Element/Note.css +++ b/src/Element/Note.css @@ -2,12 +2,8 @@ min-height: 110px; } -.note.thread { - border-bottom: none; -} - .note>.header .reply { - font-size: var(--font-size-tiny); + font-size: 13px; color: var(--font-secondary-color); } @@ -21,6 +17,7 @@ .note>.header>.info { font-size: var(--font-size); + margin-left: 4px; white-space: nowrap; color: var(--font-secondary-color); } @@ -78,50 +75,10 @@ margin-left: 56px; } -.thread.note { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - -.thread.note, .indented .note { - margin-bottom: 0; -} - -.indented .note { - border-radius: 0; -} - -.indented { - border-left: 3px solid var(--gray-tertiary); - padding-left: 2px; -} - -.note:last-child { - border-bottom-right-radius: 16px; - margin-bottom: 24px; -} - -.indented .note.active:last-child { - border-bottom-right-radius: 16px; - margin-bottom: 24px; -} - -.indented>.indented .note:last-child { - border-bottom-right-radius: 0px; - margin-bottom: 0; -} - -.indented .active { - background-color: var(--gray-tertiary); - margin-left: -5px; - border-left: 3px solid var(--highlight); - border-radius: 0; -} - .reaction-pill { display: flex; flex-direction: row; - padding: 0px 10px; + margin: 0px 14px; user-select: none; color: var(--font-secondary-color); font-feature-settings: "tnum"; @@ -144,48 +101,9 @@ margin-right: auto; } -.note.active>.header .reply { - color: var(--font-tertiary-color); -} - -.note.active>.header>.info { - color: var(--font-tertiary-color); - font-weight: 500; -} - -.note.active>.footer>.reaction-pill { - color: var(--font-tertiary-color); -} - -.note.active>.footer>.reaction-pill.reacted { - color: var(--highlight); -} - -.light .indented .active { - background-color: var(--gray-secondary); -} - -.light .note.active>.header .reply { - color: var(--font-secondary-color); -} - -.light .note.active>.header>.info { - color: var(--font-secondary-color); -} - -.light .note.active>.footer>.reaction-pill { - color: var(--note-bg); -} - -.light .note.active>.footer>.reaction-pill.reacted { - color: var(--highlight); -} - .note-expand .body { max-height: 300px; overflow-y: hidden; - mask-image: linear-gradient(to bottom, var(--note-bg) 60%, rgba(0,0,0,0)); - -webkit-mask-image: linear-gradient(to bottom, var(--note-bg) 60%, rgba(0,0,0,0)); } .hidden-note .header { @@ -201,15 +119,15 @@ max-height: 30px; } -.show-more { - background: none; - margin: 0; - padding: 0; +.expand-note { + padding: 0 0 16px 0; font-weight: 400; color: var(--highlight); + cursor: pointer; } -.show-more:hover { - background: none; - color: var(--highlight); +.note.active { + border-left: 1px solid var(--highlight); + border-bottom-left-radius: 0; + margin-left: -1px; } diff --git a/src/Element/Note.tsx b/src/Element/Note.tsx index 4943ab0..0679eaa 100644 --- a/src/Element/Note.tsx +++ b/src/Element/Note.tsx @@ -1,6 +1,7 @@ import "./Note.css"; import { useCallback, useMemo, useState, useLayoutEffect, ReactNode } from "react"; import { useNavigate, Link } from "react-router-dom"; +import { useInView } from "react-intersection-observer"; import { default as NEvent } from "Nostr/Event"; import ProfileImage from "Element/ProfileImage"; @@ -9,15 +10,15 @@ import Text from "Element/Text"; import { eventLink, getReactions, hexToBech32 } from "Util"; import NoteFooter, { Translation } from "Element/NoteFooter"; import NoteTime from "Element/NoteTime"; +import ShowMore from "Element/ShowMore"; import EventKind from "Nostr/EventKind"; import { useUserProfiles } from "Feed/ProfileFeed"; import { TaggedRawEvent, u256 } from "Nostr"; -import { useInView } from "react-intersection-observer"; import useModeration from "Hooks/useModeration"; export interface NoteProps { data?: TaggedRawEvent, - isThread?: boolean, + className?: string related: TaggedRawEvent[], highlight?: boolean, ignoreModeration?: boolean, @@ -35,7 +36,7 @@ const HiddenNote = ({ children }: any) => {

- This note was hidden because of your moderation settings + This author has been muted

-
)} - {options.showFooter ? setTranslated(t)} /> : null} - + <> + {options.showHeader ? +
+ + {options.showTime ? +
+ +
: null} +
: null} +
goToEvent(e, ev.Id)}> + {transformBody()} + {translation()} +
+ {extendable && !showMore && ( + setShowMore(true)}> + Show more + + )} + {options.showFooter && setTranslated(t)} />} + ) } const note = ( -
+
{content()}
) diff --git a/src/Element/NoteCreator.css b/src/Element/NoteCreator.css index 47d9ea3..ad0a7d6 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 27bea55..60e0016 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 6eaa268..4b51df6 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 b33de86..e5f49c2 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 4c83acc..6143ad9 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 0000000..fcfc4b3 --- /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 0000000..666e02b --- /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 0000000..2b854d9 --- /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 0000000..b1703ff --- /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 c6267f1..d8cef82 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 7cf76c6..a4d5149 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 1e2846e..5de0871 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 41a2226..ef1df7e 100644 --- a/src/Element/Zap.tsx +++ b/src/Element/Zap.tsx @@ -131,16 +131,6 @@ export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => { )}  zapped
-
- {content && ( - - )} -
)} diff --git a/src/Icons/Check.tsx b/src/Icons/Check.tsx new file mode 100644 index 0000000..2c2eb3a --- /dev/null +++ b/src/Icons/Check.tsx @@ -0,0 +1,11 @@ +import IconProps from "./IconProps" + +const Check = (props: IconProps) => { + return ( + + + + ) +} + +export default Check diff --git a/src/Icons/Copy.tsx b/src/Icons/Copy.tsx new file mode 100644 index 0000000..679d3ca --- /dev/null +++ b/src/Icons/Copy.tsx @@ -0,0 +1,11 @@ +import IconProps from './IconProps' + +const Copy = (props: IconProps) => { + return ( + + + + ) +} + +export default Copy diff --git a/src/Icons/Reply.tsx b/src/Icons/Reply.tsx index 936c405..fa57e46 100644 --- a/src/Icons/Reply.tsx +++ b/src/Icons/Reply.tsx @@ -1,7 +1,7 @@ const Reply = () => { return ( - + ) } diff --git a/src/Icons/Zap.tsx b/src/Icons/Zap.tsx index 8a01d01..1423195 100644 --- a/src/Icons/Zap.tsx +++ b/src/Icons/Zap.tsx @@ -3,7 +3,7 @@ import type IconProps from './IconProps' const Zap = (props: IconProps) => { return ( - + ) } diff --git a/src/Pages/Layout.css b/src/Pages/Layout.css index 25922ec..2709066 100644 --- a/src/Pages/Layout.css +++ b/src/Pages/Layout.css @@ -27,10 +27,18 @@ header .pfp .avatar-wrapper { .header-actions { display: flex; flex-direction: row; + align-items: center; } .header-actions .btn-rnd { position: relative; + margin-right: 8px; +} + +@media (min-width: 520px) { + .header-actions .btn-rnd { + margin-right: 16px; + } } .header-actions .btn-rnd .has-unread { diff --git a/src/Pages/Layout.tsx b/src/Pages/Layout.tsx index e8a5abf..d3418ae 100644 --- a/src/Pages/Layout.tsx +++ b/src/Pages/Layout.tsx @@ -38,6 +38,7 @@ export default function Layout() { System.nip42Auth = pub.nip42Auth }, [pub]) + useEffect(() => { System.UserDb = usingDb; }, [usingDb]) @@ -47,7 +48,7 @@ export default function Layout() { for (let [k, v] of Object.entries(relays)) { System.ConnectToRelay(k, v); } - for (let [k, v] of System.Sockets) { + for (let [k] of System.Sockets) { if (!relays[k] && !SearchRelays.has(k)) { System.DisconnectRelay(k); } diff --git a/src/Pages/ProfilePage.css b/src/Pages/ProfilePage.css index 82d421f..0666ea0 100644 --- a/src/Pages/ProfilePage.css +++ b/src/Pages/ProfilePage.css @@ -14,7 +14,7 @@ .profile .profile-actions { position: absolute; - top: 80px; + top: 72px; right: 0; display: flex; flex-direction: row; @@ -27,12 +27,6 @@ align-items: center; } -@media (min-width: 520px) { - .profile .profile-actions { - top: 120px; - } -} - .profile .profile-actions button:not(:last-child) { margin-right: 8px; } @@ -46,7 +40,7 @@ .profile .banner { width: 100%; max-width: 720px; - height: 300px; + height: 280px; } .profile .profile-actions button.icon:not(:last-child) { margin-right: 2px; @@ -60,6 +54,7 @@ flex-direction: column; align-items: flex-start; position: relative; + overflow: hidden; } @@ -80,11 +75,6 @@ margin: 0 0 12px 0; } -.profile .nip05 .nick { - font-weight: normal; - color: var(--gray-light); -} - .profile .avatar-wrapper { z-index: 1; } @@ -92,6 +82,8 @@ .profile .avatar-wrapper .avatar { width: 120px; height: 120px; + background-image: var(--img-url); + border: 3px solid var(--bg-color); } .profile .name { @@ -138,6 +130,7 @@ } .profile .links { + font-size: 14px; margin-top: 4px; margin-left: 2px; margin-bottom: 12px; @@ -150,6 +143,12 @@ align-items: center; } +@media (max-width: 720px) { + .profile .lnurl { + display: none; + } +} + .profile .website a { color: var(--font-color); } @@ -199,6 +198,34 @@ .qr-modal .modal-body { width: unset; + margin-top: -120px; +} + +.qr-modal .pfp { + flex-direction: column; + align-items: center; + justify-content: center; +} + +.qr-modal .pfp .avatar { + width: 48px; + height: 48px; +} + +.qr-modal .pfp .avatar-wrapper { + margin: 0 0 8px 0; +} + +.qr-modal .pfp .avatar-wrapper .avatar { + border: none; +} + +.qr-modal .pfp .username { + text-align: center; +} + +.qr-modal canvas { + border-radius: 10px; } .profile .zap-amount { diff --git a/src/Pages/ProfilePage.tsx b/src/Pages/ProfilePage.tsx index b7fc62c..8b4e3a8 100644 --- a/src/Pages/ProfilePage.tsx +++ b/src/Pages/ProfilePage.tsx @@ -5,6 +5,7 @@ import { useSelector } from "react-redux"; import { useNavigate, useParams } from "react-router-dom"; import { formatShort } from "Number"; +import { Tab, TabElement } from "Element/Tabs"; import Link from "Icons/Link"; import Qr from "Icons/Qr"; import Zap from "Icons/Zap"; @@ -22,6 +23,7 @@ import LNURLTip from "Element/LNURLTip"; import Nip05 from "Element/Nip05"; import Copy from "Element/Copy"; import ProfilePreview from "Element/ProfilePreview"; +import ProfileImage from "Element/ProfileImage"; import FollowersList from "Element/FollowersList"; import BlockList from "Element/BlockList"; import MutedList from "Element/MutedList"; @@ -34,15 +36,15 @@ import QrCode from "Element/QrCode"; import Modal from "Element/Modal"; import { ProxyImg } from "Element/ProxyImg" -enum ProfileTab { - Notes = "Notes", - Reactions = "Reactions", - Followers = "Followers", - Follows = "Follows", - Zaps = "Zaps", - Muted = "Muted", - Blocked = "Blocked" -}; +const ProfileTab = { + Notes: { text: "Notes", value: 0 }, + Reactions: { text: "Reactions", value: 1 }, + Followers: { text: "Followers", value: 2 }, + Follows: { text: "Follows", value: 3 }, + Zaps: { text: "Zaps", value: 4 }, + Muted: { text: "Muted", value: 5 }, + Blocked: { text: "Blocked", value: 6 }, +} export default function ProfilePage() { const params = useParams(); @@ -54,7 +56,7 @@ export default function ProfilePage() { const follows = useSelector(s => s.login.follows); const isMe = loginPubKey === id; const [showLnQr, setShowLnQr] = useState(false); - const [tab, setTab] = useState(ProfileTab.Notes); + const [tab, setTab] = useState(ProfileTab.Notes); const [showProfileQr, setShowProfileQr] = useState(false); const aboutText = user?.about || '' const about = Text({ content: aboutText, tags: [], users: new Map(), creator: "" }) @@ -100,6 +102,15 @@ export default function ProfilePage() { )} + {lnurl && ( +
setShowLnQr(true)}> + + + + {lnurl} +
+ )} + setShowLnQr(false)} author={id} /> ) @@ -168,6 +179,7 @@ export default function ProfilePage() { {showProfileQr && ( setShowProfileQr(false)}> + )} @@ -211,8 +223,8 @@ export default function ProfilePage() { ) } - function renderTab(v: ProfileTab) { - return
setTab(v)}>{v}
+ function renderTab(v: Tab) { + return } const w = window.document.querySelector(".page")?.clientWidth; @@ -225,7 +237,7 @@ export default function ProfilePage() { {userDetails()} -
+
{[ProfileTab.Notes, ProfileTab.Followers, ProfileTab.Follows, ProfileTab.Zaps, ProfileTab.Muted].map(renderTab)} {isMe && renderTab(ProfileTab.Blocked)}
diff --git a/src/Pages/Root.tsx b/src/Pages/Root.tsx index 78f93d0..40ad6a9 100644 --- a/src/Pages/Root.tsx +++ b/src/Pages/Root.tsx @@ -3,20 +3,21 @@ import { useState } from "react"; import { useSelector } from "react-redux"; import { Link } from "react-router-dom"; +import Tabs, { Tab } from "Element/Tabs"; import { RootState } from "State/Store"; import Timeline from "Element/Timeline"; import { HexKey } from "Nostr"; import { TimelineSubject } from "Feed/TimelineFeed"; -const RootTab = { - Posts: 0, - PostsAndReplies: 1, - Global: 2 +const RootTab: Record = { + Posts: { text: 'Posts', value: 0, }, + PostsAndReplies: { text: 'Conversations', value: 1, }, + Global: { text: 'Global', value: 2 }, }; export default function RootPage() { const [loggedOut, pubKey, follows] = useSelector(s => [s.login.loggedOut, s.login.publicKey, s.login.follows]); - const [tab, setTab] = useState(RootTab.Posts); + const [tab, setTab] = useState(RootTab.Posts); function followHints() { if (follows?.length === 0 && pubKey && tab !== RootTab.Global) { @@ -26,24 +27,21 @@ export default function RootPage() { } } - const isGlobal = loggedOut || tab === RootTab.Global; + const isGlobal = loggedOut || tab.value === RootTab.Global.value; const timelineSubect: TimelineSubject = isGlobal ? { type: "global", items: [], discriminator: "all" } : { type: "pubkey", items: follows, discriminator: "follows" }; return ( <> - {pubKey ? <> -
-
setTab(RootTab.Posts)}> - Posts -
-
setTab(RootTab.PostsAndReplies)}> - Conversations -
-
setTab(RootTab.Global)}> - Global -
-
: null} +
+ {pubKey && } +
{followHints()} - + ); } diff --git a/src/Pages/SearchPage.tsx b/src/Pages/SearchPage.tsx index 28ba25b..2951752 100644 --- a/src/Pages/SearchPage.tsx +++ b/src/Pages/SearchPage.tsx @@ -38,14 +38,14 @@ const SearchPage = () => { }, []); return ( - <> +

Search

setSearch(e.target.value)} />
{keyword && } - +
) } -export default SearchPage; \ No newline at end of file +export default SearchPage; diff --git a/src/index.css b/src/index.css index a914862..692e33e 100644 --- a/src/index.css +++ b/src/index.css @@ -116,7 +116,7 @@ html.light .card { button { cursor: pointer; padding: 6px 12px; - font-weight: 700; + font-weight: 600; color: white; font-size: var(--font-size); background-color: var(--highlight); @@ -443,23 +443,6 @@ body.scroll-lock { margin-right: auto; } -.tabs { - display: flex; - align-content: center; - text-align: center; - margin: 10px 0; - overflow-x: auto; -} - -.tabs>div { - cursor: pointer; - margin: 0; -} - -.tabs .active { - font-weight: 700; -} - .error { color: var(--error); } @@ -472,26 +455,6 @@ body.scroll-lock { background-color: var(--success); } -.tabs { - padding: 0; - align-items: center; - justify-content: flex-start; - margin-bottom: 16px; -} - -.tab { - border-bottom: 1px solid var(--gray-secondary); - font-weight: 700; - line-height: 19px; - color: var(--font-secondary-color); - padding: 8px 0; -} - -.tab.active { - border-bottom: 1px solid var(--highlight); - color: var(--font-color); -} - .tweet { display: flex; align-items: center;