Threads (#170)
This commit is contained in:
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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<RootState, HexKey[]>(s => s.login.follows);
|
||||
const isMe = loginPubKey === id;
|
||||
const [showLnQr, setShowLnQr] = useState<boolean>(false);
|
||||
const [tab, setTab] = useState(ProfileTab.Notes);
|
||||
const [tab, setTab] = useState<Tab>(ProfileTab.Notes);
|
||||
const [showProfileQr, setShowProfileQr] = useState<boolean>(false);
|
||||
const aboutText = user?.about || ''
|
||||
const about = Text({ content: aboutText, tags: [], users: new Map(), creator: "" })
|
||||
@ -100,6 +102,15 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{lnurl && (
|
||||
<div className="lnurl f-ellipsis" onClick={() => setShowLnQr(true)}>
|
||||
<span className="link-icon">
|
||||
<Zap />
|
||||
</span>
|
||||
{lnurl}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LNURLTip svc={lnurl} show={showLnQr} onClose={() => setShowLnQr(false)} author={id} />
|
||||
</div>
|
||||
)
|
||||
@ -168,6 +179,7 @@ export default function ProfilePage() {
|
||||
</IconButton>
|
||||
{showProfileQr && (
|
||||
<Modal className="qr-modal" onClose={() => setShowProfileQr(false)}>
|
||||
<ProfileImage pubkey={id} />
|
||||
<QrCode data={`nostr:${hexToBech32("npub", id)}`} link={undefined} className="m10" />
|
||||
</Modal>
|
||||
)}
|
||||
@ -211,8 +223,8 @@ export default function ProfilePage() {
|
||||
)
|
||||
}
|
||||
|
||||
function renderTab(v: ProfileTab) {
|
||||
return <div className={`tab f-1${tab === v ? " active" : ""}`} key={v} onClick={() => setTab(v)}>{v}</div>
|
||||
function renderTab(v: Tab) {
|
||||
return <TabElement t={v} tab={tab} setTab={setTab} />
|
||||
}
|
||||
|
||||
const w = window.document.querySelector(".page")?.clientWidth;
|
||||
@ -225,7 +237,7 @@ export default function ProfilePage() {
|
||||
{userDetails()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="tabs">
|
||||
<div className="tabs main-content">
|
||||
{[ProfileTab.Notes, ProfileTab.Followers, ProfileTab.Follows, ProfileTab.Zaps, ProfileTab.Muted].map(renderTab)}
|
||||
{isMe && renderTab(ProfileTab.Blocked)}
|
||||
</div>
|
||||
|
@ -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<string, Tab> = {
|
||||
Posts: { text: 'Posts', value: 0, },
|
||||
PostsAndReplies: { text: 'Conversations', value: 1, },
|
||||
Global: { text: 'Global', value: 2 },
|
||||
};
|
||||
|
||||
export default function RootPage() {
|
||||
const [loggedOut, pubKey, follows] = useSelector<RootState, [boolean | undefined, HexKey | undefined, HexKey[]]>(s => [s.login.loggedOut, s.login.publicKey, s.login.follows]);
|
||||
const [tab, setTab] = useState(RootTab.Posts);
|
||||
const [tab, setTab] = useState<Tab>(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 ? <>
|
||||
<div className="tabs">
|
||||
<div className={`tab f-1 ${tab === RootTab.Posts ? "active" : ""}`} onClick={() => setTab(RootTab.Posts)}>
|
||||
Posts
|
||||
</div>
|
||||
<div className={`tab f-1 ${tab === RootTab.PostsAndReplies ? "active" : ""}`} onClick={() => setTab(RootTab.PostsAndReplies)}>
|
||||
Conversations
|
||||
</div>
|
||||
<div className={`tab f-1 ${tab === RootTab.Global ? "active" : ""}`} onClick={() => setTab(RootTab.Global)}>
|
||||
Global
|
||||
</div>
|
||||
</div></> : null}
|
||||
<div className="main-content">
|
||||
{pubKey && <Tabs tabs={[RootTab.Posts, RootTab.PostsAndReplies, RootTab.Global]} tab={tab} setTab={setTab} />}
|
||||
</div>
|
||||
{followHints()}
|
||||
<Timeline key={tab} subject={timelineSubect} postsOnly={tab === RootTab.Posts} method={"TIME_RANGE"} window={tab === RootTab.Global ? 60 : undefined} />
|
||||
<Timeline
|
||||
key={tab.value}
|
||||
subject={timelineSubect}
|
||||
postsOnly={tab.value === RootTab.Posts.value}
|
||||
method={"TIME_RANGE"}
|
||||
window={tab.value === RootTab.Global.value ? 60 : undefined}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -38,14 +38,14 @@ const SearchPage = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="main-content">
|
||||
<h2>Search</h2>
|
||||
<div className="flex mb10">
|
||||
<input type="text" className="f-grow mr10" placeholder="Search.." value={search} onChange={e => setSearch(e.target.value)} />
|
||||
</div>
|
||||
{keyword && <Timeline key={keyword} subject={{ type: "keyword", items: [keyword], discriminator: keyword }} postsOnly={false} method={"LIMIT_UNTIL"} />}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchPage;
|
||||
export default SearchPage;
|
||||
|
Reference in New Issue
Block a user