UI improvements #24
@ -38,7 +38,7 @@ function transformHttpLink(a) {
|
|||||||
title="YouTube video player"
|
title="YouTube video player"
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
allowfullscreen=""
|
allowFullScreen=""
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
</>
|
</>
|
||||||
@ -85,7 +85,7 @@ export function extractMentions(fragments, tags, users) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return <b style={{ color: "red" }}>{matchTag[0]}?</b>;
|
return <b style={{ color: "var(--error)" }}>{matchTag[0]}?</b>;
|
||||||
} else {
|
} else {
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
.copy {
|
.copy {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy .body {
|
.copy .body {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
background: var(--gray-secondary);
|
||||||
width: 18em;
|
color: var(--font-color);
|
||||||
overflow: hidden;
|
padding: 2px 4px;
|
||||||
white-space: nowrap;
|
border-radius: 10px;
|
||||||
text-overflow: ellipsis;
|
margin: 0 4px 0 0;
|
||||||
}
|
}
|
@ -3,19 +3,21 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { faCopy, faCheck } from "@fortawesome/free-solid-svg-icons";
|
import { faCopy, faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { useCopy } from "../useCopy";
|
import { useCopy } from "../useCopy";
|
||||||
|
|
||||||
export default function Copy(props) {
|
export default function Copy({ text, maxSize = 32 }) {
|
||||||
|
|
||||||
const { copy, copied, error } = useCopy();
|
const { copy, copied, error } = useCopy();
|
||||||
|
const sliceLength = maxSize / 2
|
||||||
|
const trimmed = text.length > maxSize ? `${text.slice(0, sliceLength)}:${text.slice(-sliceLength)}` : text
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row copy" onClick={() => copy(props.text)}>
|
<div className="flex flex-row copy" onClick={() => copy(text)}>
|
||||||
|
<span className="body">
|
||||||
|
{trimmed}
|
||||||
|
</span>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={copied ? faCheck : faCopy}
|
icon={copied ? faCheck : faCopy}
|
||||||
size="xs"
|
size="xs"
|
||||||
style={{ color: copied ? 'green' : 'currentColor', marginRight: '2px' }}
|
style={{ color: copied ? 'var(--success)' : 'currentColor', marginRight: '2px' }}
|
||||||
/>
|
/>
|
||||||
<p className="body">
|
|
||||||
{props.text}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -1,11 +1,15 @@
|
|||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import useEventPublisher from "../feed/EventPublisher";
|
import useEventPublisher from "../feed/EventPublisher";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faUserMinus, faUserPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
export default function FollowButton(props) {
|
export default function FollowButton(props) {
|
||||||
const pubkey = props.pubkey;
|
const pubkey = props.pubkey;
|
||||||
const className = props.className ? `btn ${props.className}` : "btn";
|
|
||||||
const publiser = useEventPublisher();
|
const publiser = useEventPublisher();
|
||||||
const follows = useSelector(s => s.login.follows);
|
const follows = useSelector(s => s.login.follows);
|
||||||
|
let isFollowing = follows?.includes(pubkey) ?? false;
|
||||||
|
const baseClassName = isFollowing ? `btn btn-warn` : `btn btn-success`
|
||||||
|
const className = props.className ? `${baseClassName} ${props.className}` : `${baseClassName}`;
|
||||||
|
|
||||||
async function follow(pubkey) {
|
async function follow(pubkey) {
|
||||||
let ev = await publiser.addFollow(pubkey);
|
let ev = await publiser.addFollow(pubkey);
|
||||||
@ -17,10 +21,9 @@ export default function FollowButton(props) {
|
|||||||
publiser.broadcast(ev);
|
publiser.broadcast(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
let isFollowing = follows?.includes(pubkey) ?? false;
|
|
||||||
return (
|
return (
|
||||||
<div className={className} onClick={() => isFollowing ? unfollow(pubkey) : follow(pubkey)}>
|
<div className={className} onClick={() => isFollowing ? unfollow(pubkey) : follow(pubkey)}>
|
||||||
{isFollowing ? "Unfollow" : "Follow"}
|
<FontAwesomeIcon icon={isFollowing ? faUserMinus : faUserPlus} size="lg" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
.note-invoice {
|
.note-invoice {
|
||||||
|
background: var(--bg-color);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid #444;
|
border: 1px solid var(--gray-tertiary);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -9,5 +10,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.note-invoice small {
|
.note-invoice small {
|
||||||
color: #666;
|
color: var(--gray-medium);
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
.lnurl-tip {
|
.lnurl-tip {
|
||||||
background-color: #222;
|
background-color: var(--gray-secondary);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
@ -12,7 +12,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lnurl-tip .btn:hover {
|
.lnurl-tip .btn:hover {
|
||||||
background-color: #333;
|
background-color: var(--gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lnurl-tip .invoice {
|
.lnurl-tip .invoice {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
background-color: rgba(0,0,0, 0.8);
|
background-color: var(--modal-bg-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -2,22 +2,18 @@
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: .2em 0;
|
margin: .2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nip05 .nick {
|
.nip05 .nick {
|
||||||
color: #999;
|
color: var(--gray-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nip05 .domain {
|
.nip05 .domain {
|
||||||
color: #DDD;
|
color: var(--gray-superlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nip05 .badge {
|
.nip05 .badge {
|
||||||
margin-left: .2em;
|
margin-left: .2em;
|
||||||
}
|
margin-top: .1em;
|
||||||
|
|
||||||
.nip05 .error {
|
|
||||||
margin-top: .2em;
|
|
||||||
margin-left: .2em;
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faCheck, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
import { faCheck, faSpinner, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
import './Nip05.css'
|
import './Nip05.css'
|
||||||
|
|
||||||
@ -28,30 +28,35 @@ const Nip05 = ({ nip05, pubkey }) => {
|
|||||||
}, [nip05, name, domain])
|
}, [nip05, name, domain])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex nip05">
|
<div className="flex nip05" onClick={(ev) => ev.stopPropagation()}>
|
||||||
{!isDefaultUser && <div className="nick">{name}</div>}
|
{!isDefaultUser && <div className="nick">{name}</div>}
|
||||||
<div className="domain">
|
<div className="domain">
|
||||||
{!isDefaultUser && '@'}
|
{!isDefaultUser && '@'}
|
||||||
{domain}
|
{domain}
|
||||||
</div>
|
</div>
|
||||||
{isVerified && (
|
|
||||||
<span className="badge">
|
<span className="badge">
|
||||||
|
{!isVerified && !couldNotVerify && (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
color={"green"}
|
color={"var(--fg-color)"}
|
||||||
|
icon={faSpinner}
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isVerified && (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
color={"var(--success)"}
|
||||||
icon={faCheck}
|
icon={faCheck}
|
||||||
size="xs"
|
size="xs"
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
{couldNotVerify && (
|
{couldNotVerify && (
|
||||||
<span className="error">
|
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
color={"red"}
|
color={"var(--error)"}
|
||||||
icon={faTriangleExclamation}
|
icon={faTriangleExclamation}
|
||||||
size="xs"
|
size="xs"
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
.note {
|
.note {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
border-bottom: 1px solid #333;
|
border-bottom: 1px solid var(--gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
.note.active {
|
.note.thread {
|
||||||
background-color: #222;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note > .header > .pfp {
|
.note > .header > .pfp {
|
||||||
@ -13,13 +13,13 @@
|
|||||||
|
|
||||||
.note > .header .reply {
|
.note > .header .reply {
|
||||||
font-size: small;
|
font-size: small;
|
||||||
color: #999;
|
color: var(--gray-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.note > .header > .info {
|
.note > .header > .info {
|
||||||
font-size: small;
|
font-size: small;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: #999;
|
color: var(--gray-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.note > .body {
|
.note > .body {
|
||||||
@ -32,10 +32,10 @@
|
|||||||
.note > .body > img, .note > .body > video, .note > .body > iframe {
|
.note > .body > img, .note > .body > video, .note > .body > iframe {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
}
|
margin: 10px;
|
||||||
|
margin-left: auto;
|
||||||
.note > .body > iframe {
|
margin-right: auto;
|
||||||
margin: 10px 0;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note > .header > img:hover, .note > .header > .name > .reply:hover, .note > .body:hover {
|
.note > .header > img:hover, .note > .header > .name > .reply:hover, .note > .body:hover {
|
||||||
@ -48,6 +48,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.indented {
|
.indented {
|
||||||
border-left: 3px solid #444;
|
border-left: 3px solid var(--gray-tertiary);
|
||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.indented .active {
|
||||||
|
background-color: var(--gray-tertiary);
|
||||||
|
margin-left: -5px;
|
||||||
|
border-left: 3px solid var(--highlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.indented .note {
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note .body a {
|
||||||
|
color: var(--highlight);
|
||||||
|
}
|
||||||
|
@ -12,12 +12,9 @@ import NoteTime from "./NoteTime";
|
|||||||
|
|
||||||
export default function Note(props) {
|
export default function Note(props) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const data = props.data;
|
|
||||||
const opt = props.options;
|
const opt = props.options;
|
||||||
const dataEvent = props["data-ev"];
|
const dataEvent = props["data-ev"];
|
||||||
const reactions = props.reactions;
|
const { data, isThread, reactions, deletion, hightlight } = props
|
||||||
const deletion = props.deletion;
|
|
||||||
const hightlight = props.hightlight;
|
|
||||||
|
|
||||||
const users = useSelector(s => s.users?.users);
|
const users = useSelector(s => s.users?.users);
|
||||||
const ev = dataEvent ?? Event.FromObject(data);
|
const ev = dataEvent ?? Event.FromObject(data);
|
||||||
@ -81,7 +78,7 @@ export default function Note(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`note ${hightlight ? "active" : ""}`}>
|
<div className={`note ${hightlight ? "active" : ""} ${isThread ? "thread" : ""}`}>
|
||||||
{options.showHeader ?
|
{options.showHeader ?
|
||||||
<div className="header flex">
|
<div className="header flex">
|
||||||
<ProfileImage pubkey={ev.RootPubKey} subHeader={replyTag()} />
|
<ProfileImage pubkey={ev.RootPubKey} subHeader={replyTag()} />
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
.note-creator {
|
.note-creator {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
background-color: #333;
|
background-color: var(--gray);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
.reaction {
|
.reaction {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
border-bottom: 1px solid #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.reaction > .note {
|
.reaction > .note {
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
border: 1px solid #333;
|
border: 1px solid var(--gray);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
@ -19,5 +18,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.reaction > .header > .info {
|
.reaction > .header > .info {
|
||||||
|
color: #999;
|
||||||
font-size: small;
|
font-size: small;
|
||||||
}
|
}
|
@ -16,9 +16,12 @@ export default function NoteTime(props) {
|
|||||||
return fromDate.toLocaleDateString(undefined, { year: "2-digit", month: "short", day: "2-digit", weekday: "short" });
|
return fromDate.toLocaleDateString(undefined, { year: "2-digit", month: "short", day: "2-digit", weekday: "short" });
|
||||||
} else if (absAgo > HourInMs) {
|
} else if (absAgo > HourInMs) {
|
||||||
return `${fromDate.getHours().toString().padStart(2, '0')}:${fromDate.getMinutes().toString().padStart(2, '0')}`;
|
return `${fromDate.getHours().toString().padStart(2, '0')}:${fromDate.getMinutes().toString().padStart(2, '0')}`;
|
||||||
|
} else if (absAgo < MinuteInMs) {
|
||||||
|
return 'Just now'
|
||||||
} else {
|
} else {
|
||||||
let mins = parseInt(absAgo / MinuteInMs);
|
let mins = parseInt(absAgo / MinuteInMs);
|
||||||
return `${mins} mins ago`;
|
let minutes = mins === 1 ? 'min' : 'mins'
|
||||||
|
return `${mins} ${minutes} ago`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
border-radius: 10px;
|
border-radius: 100%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,9 +6,7 @@ import { Link, useNavigate } from "react-router-dom";
|
|||||||
import useProfile from "../feed/ProfileFeed";
|
import useProfile from "../feed/ProfileFeed";
|
||||||
import { profileLink } from "../Util";
|
import { profileLink } from "../Util";
|
||||||
|
|
||||||
export default function ProfileImage(props) {
|
export default function ProfileImage({ pubkey, subHeader, showUsername = true }) {
|
||||||
const pubkey = props.pubkey;
|
|
||||||
const subHeader = props.subHeader;
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const user = useProfile(pubkey);
|
const user = useProfile(pubkey);
|
||||||
|
|
||||||
@ -25,10 +23,12 @@ export default function ProfileImage(props) {
|
|||||||
return (
|
return (
|
||||||
<div className="pfp">
|
<div className="pfp">
|
||||||
<img src={hasImage ? user.picture : Nostrich} onClick={() => navigate(profileLink(pubkey))} />
|
<img src={hasImage ? user.picture : Nostrich} onClick={() => navigate(profileLink(pubkey))} />
|
||||||
|
{showUsername && (
|
||||||
<div>
|
<div>
|
||||||
<Link key={pubkey} to={profileLink(pubkey)}>{name}</Link>
|
<Link key={pubkey} to={profileLink(pubkey)}>{name}</Link>
|
||||||
{subHeader ? <div>{subHeader}</div> : null}
|
{subHeader ? <div>{subHeader}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
.relay {
|
.relay {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
background-color: #222;
|
background-color: var(--gray-secondary);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ export default function Relay(props) {
|
|||||||
<>
|
<>
|
||||||
<div className="flex relay w-max">
|
<div className="flex relay w-max">
|
||||||
<div>
|
<div>
|
||||||
<FontAwesomeIcon icon={faPlug} color={state?.connected ? "green" : "red"} />
|
<FontAwesomeIcon icon={faPlug} color={state?.connected ? "var(--success)" : "var(--error)"} />
|
||||||
</div>
|
</div>
|
||||||
<div className="f-grow f-col">
|
<div className="f-grow f-col">
|
||||||
<b>{name}</b>
|
<b>{name}</b>
|
||||||
|
@ -47,7 +47,7 @@ export default function Thread(props) {
|
|||||||
|
|
||||||
function renderRoot() {
|
function renderRoot() {
|
||||||
if (root) {
|
if (root) {
|
||||||
return <Note data-ev={root} reactions={reactions(root.Id)} deletion={reactions(root.Id, EventKind.Deletion)} />
|
return <Note data-ev={root} reactions={reactions(root.Id)} deletion={reactions(root.Id, EventKind.Deletion)} isThread />
|
||||||
} else {
|
} else {
|
||||||
return <NoteGhost>
|
return <NoteGhost>
|
||||||
Loading thread root.. ({notes.length} notes loaded)
|
Loading thread root.. ({notes.length} notes loaded)
|
||||||
|
@ -1,12 +1,41 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-color: #FFF;
|
||||||
|
--bg-color: #000;
|
||||||
|
--modal-bg-color: rgba(0,0,0, 0.8);
|
||||||
|
--gray-superlight: #EEE;
|
||||||
|
--gray-light: #999;
|
||||||
|
--gray-medium: #666;
|
||||||
|
--gray: #333;
|
||||||
|
--gray-secondary: #222;
|
||||||
|
--gray-tertiary: #444;
|
||||||
|
--highlight: #A9E000;
|
||||||
|
--error: #FF6053;
|
||||||
|
--success: #2AD544;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
--font-color: #000;
|
||||||
|
--bg-color: #FFF;
|
||||||
|
--highlight: #FF9B00;
|
||||||
|
--modal-bg-color: rgba(240, 240, 240, 0.8);
|
||||||
|
--gray: #CCC;
|
||||||
|
--gray-secondary: #DDD;
|
||||||
|
--gray-tertiary: #EEE;
|
||||||
|
--gray-superlight: #333;
|
||||||
|
--gray-light: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'Montserrat', sans-serif;
|
font-family: 'Montserrat', sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
background-color: #000;
|
background-color: var(--bg-color);
|
||||||
color: #fff;
|
color: var(--font-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
@ -42,19 +71,28 @@ code {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
background-color: #000;
|
background-color: var(--bg-color);
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-warn {
|
||||||
|
border-color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
border-color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
.btn.active {
|
.btn.active {
|
||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
background-color: #222;
|
background-color: var(--gray-secondary);
|
||||||
|
color: var(--font-color);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover {
|
.btn:hover {
|
||||||
background-color: #333;
|
background-color: var(--gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-sm {
|
.btn-sm {
|
||||||
@ -73,8 +111,12 @@ input[type="text"], input[type="password"], input[type="number"], textarea {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border: 0;
|
border: 0;
|
||||||
background-color: #333;
|
background-color: var(--gray);
|
||||||
color: #eee;
|
color: var(--font-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea:placeholder {
|
||||||
|
color: var(--gray-superlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex {
|
.flex {
|
||||||
@ -122,7 +164,7 @@ a {
|
|||||||
|
|
||||||
span.pill {
|
span.pill {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-color: #333;
|
background-color: var(--gray);
|
||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -130,7 +172,8 @@ span.pill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
span.pill.active {
|
span.pill.active {
|
||||||
background-color: #444;
|
background-color: var(--gray-tertiary);
|
||||||
|
color: var(--font-color);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,7 +229,7 @@ div.form-group > div:nth-child(2) input {
|
|||||||
.modal .modal-content > div {
|
.modal .modal-content > div {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: #333;
|
background-color: var(--gray);
|
||||||
margin-top: 5vh;
|
margin-top: 5vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,11 +267,19 @@ body.scroll-lock {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs > div.active {
|
.error {
|
||||||
background-color: #222;
|
color: var(--error);
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.root-tabs {
|
||||||
color: red;
|
padding: 0 2px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root-tab {
|
||||||
|
border-bottom: 3px solid var(--gray-secondary);
|
||||||
|
}
|
||||||
|
.root-tab.active {
|
||||||
|
border-bottom: 3px solid var(--highlight);
|
||||||
}
|
}
|
@ -1,3 +1,7 @@
|
|||||||
.notifications {
|
.notifications {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unread-count {
|
||||||
|
margin-left: .2em;
|
||||||
|
}
|
||||||
|
@ -54,14 +54,18 @@ export default function Layout(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function accountHeader() {
|
function accountHeader() {
|
||||||
const unreadNotifications = notifications?.filter(a => (a.created_at * 1000) > readNotifications).length ?? 0;
|
const unreadNotifications = notifications?.filter(a => (a.created_at * 1000) > readNotifications).length;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="btn btn-rnd notifications" onClick={(e) => goToNotifications(e)}>
|
<div className="btn btn-rnd notifications" onClick={(e) => goToNotifications(e)}>
|
||||||
<FontAwesomeIcon icon={faBell} size="xl" />
|
<FontAwesomeIcon icon={faBell} size="xl" />
|
||||||
|
{unreadNotifications !== 0 && (
|
||||||
|
<span className="unread-count">
|
||||||
{unreadNotifications}
|
{unreadNotifications}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ProfileImage pubkey={key} />
|
<ProfileImage pubkey={key} showUsername={false} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -18,11 +18,46 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile .avatar-wrapper {
|
||||||
|
margin: auto 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.profile .avatar {
|
.profile .avatar {
|
||||||
width: 256px;
|
width: 256px;
|
||||||
height: 256px;
|
height: 256px;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
border-radius: 10px;
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .details {
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .website {
|
||||||
|
padding-left: 0;
|
||||||
|
color: var(--highlight);
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .lnurl {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .btn-icon {
|
||||||
|
padding: 6px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .website::before {
|
||||||
|
content: '🔗 ';
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .lnurl::before {
|
||||||
|
content: '⚡️ ';
|
||||||
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(max-width: 720px) {
|
@media(max-width: 720px) {
|
||||||
@ -31,7 +66,34 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.profile > div:last-child {
|
.profile > div:last-child {
|
||||||
margin: 0;
|
margin: 5px 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media(max-width: 360px) {
|
||||||
|
.profile .name { flex-direction: column; }
|
||||||
|
.profile .name .btn {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs > div {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
margin: 0;
|
||||||
|
padding: 4px;
|
||||||
|
border-bottom: 3px solid var(--gray-secondary);
|
||||||
|
}
|
||||||
|
.tab.active {
|
||||||
|
border-bottom: 3px solid var(--highlight);
|
||||||
|
}
|
||||||
|
@ -4,7 +4,7 @@ import Nostrich from "../nostrich.jpg";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faQrcode } from "@fortawesome/free-solid-svg-icons";
|
import { faQrcode, faGear } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import useProfile from "../feed/ProfileFeed";
|
import useProfile from "../feed/ProfileFeed";
|
||||||
@ -33,20 +33,30 @@ export default function ProfilePage() {
|
|||||||
<div className="f-grow">
|
<div className="f-grow">
|
||||||
<h2>{user?.display_name || user?.name}</h2>
|
<h2>{user?.display_name || user?.name}</h2>
|
||||||
<Copy text={params.id} />
|
<Copy text={params.id} />
|
||||||
|
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{isMe ? <div className="btn" onClick={() => navigate("/settings")}>Settings</div> : <FollowButton pubkey={id} />}
|
{isMe ? (
|
||||||
|
<div className="btn btn-icon" onClick={() => navigate("/settings")}>
|
||||||
|
<FontAwesomeIcon icon={faGear} size="lg" />
|
||||||
|
</div>
|
||||||
|
) : <FollowButton pubkey={id} />
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
|
|
||||||
<p>{extractLinks([user?.about])}</p>
|
<p>{extractLinks([user?.about])}</p>
|
||||||
{user?.website ? <a href={user?.website} target="_blank" rel="noreferrer">{user?.website}</a> : null}
|
|
||||||
|
|
||||||
{lnurl ? <div className="flex">
|
{user?.website && (
|
||||||
<div className="btn" onClick={(e) => setShowLnQr(true)}>
|
<div className="website f-ellipsis">
|
||||||
<FontAwesomeIcon icon={faQrcode} size="xl" />
|
<a href={user.website} target="_blank" rel="noreferrer">{user.website}</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{lnurl ? <div className="lnurl f-ellipsis">
|
||||||
|
{lnurl}
|
||||||
|
<div className="btn btn-icon" onClick={(e) => setShowLnQr(true)}>
|
||||||
|
<FontAwesomeIcon icon={faQrcode} size="lg" />
|
||||||
</div>
|
</div>
|
||||||
<div className="f-ellipsis"> ⚡️ {lnurl}</div>
|
|
||||||
</div> : null}
|
</div> : null}
|
||||||
<LNURLTip svc={lnurl} show={showLnQr} onClose={() => setShowLnQr(false)} />
|
<LNURLTip svc={lnurl} show={showLnQr} onClose={() => setShowLnQr(false)} />
|
||||||
</>
|
</>
|
||||||
@ -56,19 +66,19 @@ export default function ProfilePage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="profile flex">
|
<div className="profile flex">
|
||||||
<div>
|
<div className="avatar-wrapper">
|
||||||
<div style={{ backgroundImage: `url(${(user?.picture?.length ?? 0) === 0 ? Nostrich : user?.picture})` }} className="avatar">
|
<div style={{ backgroundImage: `url(${(user?.picture?.length ?? 0) === 0 ? Nostrich : user?.picture})` }} className="avatar">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="f-grow">
|
<div className="f-grow details">
|
||||||
{details()}
|
{details()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="tabs">
|
<div className="tabs">
|
||||||
<div className="btn active">Notes</div>
|
<div className="tab f-1 active">Notes</div>
|
||||||
<div className="btn">Reactions</div>
|
<div className="tab f-1">Reactions</div>
|
||||||
<div className="btn">Followers</div>
|
<div className="tab f-1">Followers</div>
|
||||||
<div className="btn">Follows</div>
|
<div className="tab f-1">Follows</div>
|
||||||
</div>
|
</div>
|
||||||
<Timeline pubkeys={id} />
|
<Timeline pubkeys={id} />
|
||||||
</>
|
</>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
.root-tabs > div {
|
.root-tabs > div {
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
background-color: #333;
|
margin-right: 0;
|
||||||
}
|
}
|
@ -29,10 +29,10 @@ export default function RootPage() {
|
|||||||
{pubKey ? <>
|
{pubKey ? <>
|
||||||
<NoteCreator show={true}/>
|
<NoteCreator show={true}/>
|
||||||
<div className="tabs root-tabs">
|
<div className="tabs root-tabs">
|
||||||
<div className={`f-1 ${tab === RootTab.Follows ? "active" : ""}`} onClick={() => setTab(RootTab.Follows)}>
|
<div className={`root-tab f-1 ${tab === RootTab.Follows ? "active" : ""}`} onClick={() => setTab(RootTab.Follows)}>
|
||||||
Follows
|
Follows
|
||||||
</div>
|
</div>
|
||||||
<div className={`f-1 ${tab === RootTab.Global ? "active" : ""}`} onClick={() => setTab(RootTab.Global)}>
|
<div className={`root-tab f-1 ${tab === RootTab.Global ? "active" : ""}`} onClick={() => setTab(RootTab.Global)}>
|
||||||
Global
|
Global
|
||||||
</div>
|
</div>
|
||||||
</div></> : null}
|
</div></> : null}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
|
|
||||||
.settings .avatar {
|
.settings .avatar {
|
||||||
width: 256px;
|
width: 256px;
|
||||||
height: 256px;
|
height: 256px;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
border-radius: 10px;
|
border-radius: 100%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings .avatar .edit {
|
.settings .avatar .edit {
|
||||||
@ -14,7 +14,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
background-color: black;
|
background-color: var(--bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings .avatar .edit:hover {
|
.settings .avatar .edit:hover {
|
||||||
|
@ -20,6 +20,7 @@ export default function SettingsPage(props) {
|
|||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
|
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
|
const [displayName, setDisplayName] = useState("");
|
||||||
const [picture, setPicture] = useState("");
|
const [picture, setPicture] = useState("");
|
||||||
const [about, setAbout] = useState("");
|
const [about, setAbout] = useState("");
|
||||||
const [website, setWebsite] = useState("");
|
const [website, setWebsite] = useState("");
|
||||||
@ -31,6 +32,7 @@ export default function SettingsPage(props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
setName(user.name ?? "");
|
setName(user.name ?? "");
|
||||||
|
setDisplayName(user.display_name ?? "")
|
||||||
setPicture(user.picture ?? "");
|
setPicture(user.picture ?? "");
|
||||||
setAbout(user.about ?? "");
|
setAbout(user.about ?? "");
|
||||||
setWebsite(user.website ?? "");
|
setWebsite(user.website ?? "");
|
||||||
@ -45,6 +47,7 @@ export default function SettingsPage(props) {
|
|||||||
let userCopy = {
|
let userCopy = {
|
||||||
...user,
|
...user,
|
||||||
name,
|
name,
|
||||||
|
display_name: displayName,
|
||||||
about,
|
about,
|
||||||
picture,
|
picture,
|
||||||
website,
|
website,
|
||||||
@ -102,6 +105,12 @@ export default function SettingsPage(props) {
|
|||||||
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
|
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<div>Display name:</div>
|
||||||
|
<div>
|
||||||
|
<input type="text" value={displayName} onChange={(e) => setDisplayName(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="form-group f-col">
|
<div className="form-group f-col">
|
||||||
<div>About:</div>
|
<div>About:</div>
|
||||||
<div className="w-max">
|
<div className="w-max">
|
||||||
|
Loading…
Reference in New Issue
Block a user