UI updates (#135)

This commit is contained in:
Alejandro
2023-01-25 19:08:53 +01:00
committed by GitHub
parent a8414d2b0f
commit 9ca3b547c2
47 changed files with 628 additions and 433 deletions

View File

@ -2,8 +2,8 @@
border-radius: 50%;
height: 210px;
width: 210px;
background-image: var(--img-url), var(--gray-gradient);
border: 2px solid transparent;
background-image: var(--img-url);
border: 1px solid transparent;
background-origin: border-box;
background-clip: content-box, border-box;
background-size: cover;

View File

@ -1,15 +1,9 @@
.copy {
user-select: none;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
cursor: pointer;
}
.copy .body {
font-family: monospace;
font-size: 14px;
background: var(--note-bg);
font-size: var(--font-size-small);
color: var(--font-color);
padding: 2px 4px;
border-radius: 10px;
margin: 0 4px 0 0;
margin-right: 8px;
}

View File

@ -10,7 +10,7 @@ export interface CopyProps {
export default function Copy({ text, maxSize = 32 }: CopyProps) {
const { copy, copied, error } = useCopy();
const sliceLength = maxSize / 2
const trimmed = text.length > maxSize ? `${text.slice(0, sliceLength)}:${text.slice(-sliceLength)}` : text
const trimmed = text.length > maxSize ? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}` : text
return (
<div className="flex flex-row copy" onClick={() => copy(text)}>
@ -20,7 +20,7 @@ export default function Copy({ text, maxSize = 32 }: CopyProps) {
<FontAwesomeIcon
icon={copied ? faCheck : faCopy}
size="xs"
style={{ color: copied ? 'var(--success)' : 'currentColor', marginRight: '2px' }}
style={{ color: copied ? 'var(--success)' : 'var(--highlight)', marginRight: '2px' }}
/>
</div>
)

View File

@ -7,14 +7,12 @@ import { RootState } from "State/Store";
export interface FollowButtonProps {
pubkey: HexKey,
className?: string,
className?: string
}
export default function FollowButton(props: FollowButtonProps) {
const pubkey = props.pubkey;
const publiser = useEventPublisher();
const isFollowing = useSelector<RootState, boolean>(s => s.login.follows?.includes(pubkey) ?? false);
const baseClassName = isFollowing ? `btn btn-warn follow-button` : `btn btn-success follow-button`
const className = props.className ? `${baseClassName} ${props.className}` : `${baseClassName}`;
async function follow(pubkey: HexKey) {
let ev = await publiser.addFollow(pubkey);
@ -27,8 +25,8 @@ export default function FollowButton(props: FollowButtonProps) {
}
return (
<div className={className} onClick={() => isFollowing ? unfollow(pubkey) : follow(pubkey)}>
<FontAwesomeIcon icon={isFollowing ? faUserMinus : faUserPlus} size="lg" />
</div>
<button type="button" className={isFollowing ? `${props.className ?? ''} secondary` : props.className} onClick={() => isFollowing ? unfollow(pubkey) : follow(pubkey)}>
{isFollowing ? 'Unfollow' : 'Follow'}
</button>
)
}
}

View File

@ -15,12 +15,12 @@ export default function FollowListBase({ pubkeys, title }: FollowListBaseProps)
}
return (
<>
<div className="main-content">
<div className="flex mt10">
<div className="f-grow">{title}</div>
<div className="btn" onClick={() => followAll()}>Follow All</div>
<button type="button" onClick={() => followAll()}>Follow All</button>
</div>
{pubkeys?.map(a => <ProfilePreview pubkey={a} key={a} />)}
</>
</div>
)
}
}

View File

@ -1,5 +1,5 @@
.follows-you {
color: var(--font-color);
color: var(--gray-light);
font-size: var(--font-size-tiny);
margin-left: .2em;
font-weight: normal

View File

@ -1,10 +1,5 @@
.lnurl-tip {
background-color: var(--note-bg);
padding: 10px;
border-radius: 10px;
width: 500px;
text-align: center;
min-height: 10vh;
}
.lnurl-tip .btn {
@ -62,10 +57,3 @@
align-items: center;
justify-content: center;
}
@media(max-width: 720px) {
.lnurl-tip {
width: 100vw;
margin: 0 10px;
}
}

View File

@ -8,5 +8,20 @@
display: flex;
justify-content: center;
align-items: center;
z-index: 9999999;
z-index: 42;
}
.modal-body {
background-color: var(--note-bg);
padding: 10px;
border-radius: 10px;
width: 500px;
min-height: 10vh;
}
@media(max-width: 720px) {
.modal-body {
width: 100vw;
margin: 0 10px;
}
}

View File

@ -17,7 +17,9 @@ export default function Modal(props: ModalProps) {
return (
<div className="modal" onClick={(e) => { e.stopPropagation(); onClose(); }}>
<div className="modal-body">
{props.children}
</div>
</div>
)
}
}

View File

@ -1,7 +1,6 @@
.nip05 {
justify-content: flex-start;
align-items: center;
font-size: 14px;
margin: .2em;
}
@ -10,50 +9,8 @@
}
.nip05 .nick {
color: var(--gray-light);
color: var(--font-secondary-color);
font-weight: bold;
margin-right: .2em;
}
.nip05 .domain {
color: var(--gray-superlight);
font-weight: bold;
}
.nip05 .text-gradient {
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
background-color: var(--gray-superlight);
}
.nip05 .domain[data-domain="snort.social"] {
background-image: var(--snort-gradient);
}
.nip05 .domain[data-domain="nostrplebs.com"] {
background-image: var(--nostrplebs-gradient);
}
.nip05 .domain[data-domain="nostrpurple.com"] {
background-image: var(--nostrplebs-gradient);
}
.nip05 .domain[data-domain="nostr.fan"] {
background-image: var(--nostrplebs-gradient);
}
.nip05 .domain[data-domain="nostrich.zone"] {
background-image: var(--nostrplebs-gradient);
}
.nip05 .domain[data-domain="nostriches.net"] {
background-image: var(--nostrplebs-gradient);
}
.nip05 .domain[data-domain="strike.army"] {
background-image: var(--strike-army-gradient);
}
.nip05 .badge {

View File

@ -1,7 +1,7 @@
import { useQuery } from "react-query";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSpinner, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import { faCircleCheck, faSpinner, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import './Nip05.css'
import { HexKey } from "Nostr";
@ -57,16 +57,20 @@ const Nip05 = (props: Nip05Params) => {
const { isVerified, couldNotVerify } = useIsVerified(props.pubkey, props.nip05)
return (
<div className={`flex nip05${couldNotVerify ? " failed" : ""}`}>
{!isDefaultUser && (
<div className="nick">
{name}
</div>
)}
<div className={`domain text-gradient`} data-domain={domain?.toLowerCase()}>
{domain}
<div className={`flex nip05${couldNotVerify ? " failed" : ""}`} onClick={(ev) => ev.stopPropagation()}>
<div className="nick">
{isDefaultUser ? (
`${domain}`
) : `@${name}`}
</div>
<span className="badge">
{isVerified && (
<FontAwesomeIcon
color={"var(--highlight)"}
icon={faCircleCheck}
size="xs"
/>
)}
{!isVerified && !couldNotVerify && (
<FontAwesomeIcon
color={"var(--fg-color)"}

View File

@ -11,6 +11,14 @@
color: var(--font-secondary-color);
}
.note>.header .reply a {
color: var(--highlight);
}
.note>.header .reply a:hover {
text-decoration-color: var(--highlight);
}
.note>.header>.info {
font-size: var(--font-size);
white-space: nowrap;
@ -18,7 +26,7 @@
}
.note>.body {
margin-top: 12px;
margin-top: 4px;
padding-left: 56px;
text-overflow: ellipsis;
white-space: pre-wrap;

View File

@ -1,6 +1,6 @@
import "./Note.css";
import { useCallback, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { useCallback, useMemo, ReactNode } from "react";
import { useNavigate, Link } from "react-router-dom";
import { default as NEvent } from "Nostr/Event";
import ProfileImage from "Element/ProfileImage";
@ -64,21 +64,57 @@ export default function Note(props: NoteProps) {
const maxMentions = 2;
let replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event;
let mentions: string[] = [];
let mentions: {pk: string, name: string, link: ReactNode}[] = [];
for (let pk of ev.Thread?.PubKeys) {
let u = users?.get(pk);
const u = users?.get(pk);
const npub = hexToBech32("npub", pk)
const shortNpub = npub.substring(0, 12);
if (u) {
mentions.push(u.name ?? hexToBech32("npub", pk).substring(0, 12));
mentions.push({
pk,
name: u.name ?? shortNpub,
link: (
<Link to={`/p/${npub}`}>
{u.name ? `@${u.name}` : shortNpub}
</Link>
)
});
} else {
mentions.push(hexToBech32("npub", pk).substring(0, 12));
mentions.push({
pk,
name: shortNpub,
link: (
<Link to={`/p/${npub}`}>
{shortNpub}
</Link>
)
});
}
}
mentions.sort((a, b) => a.startsWith("npub") ? 1 : -1);
mentions.sort((a, b) => a.name.startsWith("npub") ? 1 : -1);
let othersLength = mentions.length - maxMentions
let pubMentions = mentions.length > maxMentions ? `${mentions?.slice(0, maxMentions).join(", ")} & ${othersLength} other${othersLength > 1 ? 's' : ''}` : mentions?.join(", ");
const renderMention = (m: any, idx: number) => {
return (
<>
{idx > 0 && ", "}
{m.link}
</>
)
}
const pubMentions = mentions.length > maxMentions ? (
mentions?.slice(0, maxMentions).map(renderMention)
) : mentions?.map(renderMention);
const others = mentions.length > maxMentions ? ` & ${othersLength} other${othersLength > 1 ? 's' : ''}` : ''
return (
<div className="reply">
{(pubMentions?.length ?? 0) > 0 ? pubMentions : replyId ? hexToBech32("note", replyId)?.substring(0, 12) : ""}
{(mentions?.length ?? 0) > 0 ? (
<>
{pubMentions}
{others}
</>
) : replyId ? (
hexToBech32("note", replyId)?.substring(0, 12) // todo: link
) : ""}
</div>
)
}

View File

@ -13,23 +13,28 @@
.note-creator textarea {
outline: none;
resize: none;
min-height: 40px;
background-color: var(--note-bg);
border-radius: 10px 10px 0 0;
max-width: stretch;
min-width: stretch;
}
.note-creator .actions {
.note-creator-actions {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
margin-bottom: 5px;
}
.note-creator .attachment {
cursor: pointer;
padding: 5px 10px;
border-radius: 10px;
margin-left: auto;
}
.note-creator-actions button:not(:last-child) {
margin-right: 4px;
}
.note-creator .attachment .error {
@ -45,3 +50,26 @@
color: var(--font-color);
font-size: var(--font-size);
}
.note-create-button {
width: 48px;
height: 48px;
background-color: var(--highlight);
border: none;
border-radius: 100%;
position: fixed;
bottom: 50px;
right: 16px;
}
@media (min-width: 520px) {
.note-create-button {
right: 10vw;
}
}
@media (min-width: 1020px) {
.note-create-button {
right: 25vw;
}
}

View File

@ -4,21 +4,26 @@ import { faPaperclip } from "@fortawesome/free-solid-svg-icons";
import "./NoteCreator.css";
import Plus from "Icons/Plus";
import useEventPublisher from "Feed/EventPublisher";
import { openFile } from "Util";
import VoidUpload from "Feed/VoidUpload";
import { FileExtensionRegex } from "Const";
import Textarea from "Element/Textarea";
import Event, { default as NEvent } from "Nostr/Event";
import Modal from "Element/Modal";
import { default as NEvent } from "Nostr/Event";
export interface NoteCreatorProps {
show: boolean
setShow: (s: boolean) => void
replyTo?: NEvent,
onSend?: Function,
show: boolean,
onClose?(): void
autoFocus: boolean
}
export function NoteCreator(props: NoteCreatorProps) {
const { show, setShow } = props
const publisher = useEventPublisher();
const [note, setNote] = useState<string>();
const [error, setError] = useState<string>();
@ -68,14 +73,23 @@ export function NoteCreator(props: NoteCreatorProps) {
}
}
function cancel(ev: any) {
setShow(false)
setNote("")
}
function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) {
ev.stopPropagation();
sendNote().catch(console.warn);
}
if (!props.show) return null;
return (
<>
<button className="note-create-button" type="button" onClick={() => setShow(!show)}>
<Plus />
</button>
{show && (
<Modal onClose={props.onClose}>
<div className={`flex note-creator ${props.replyTo ? 'note-reply' : ''}`}>
<div className="flex f-col mr10 f-grow">
<Textarea
@ -85,19 +99,22 @@ export function NoteCreator(props: NoteCreatorProps) {
value={note}
onFocus={() => setActive(true)}
/>
{active && note && (
<div className="actions flex f-row">
<div className="attachment flex f-row">
{(error?.length ?? 0) > 0 ? <b className="error">{error}</b> : null}
<FontAwesomeIcon icon={faPaperclip} size="xl" onClick={(e) => attachFile()} />
</div>
<button type="button" className="btn" onClick={onSubmit}>
{props.replyTo ? 'Reply' : 'Send'}
</button>
</div>
)}
<div className="attachment">
{(error?.length ?? 0) > 0 ? <b className="error">{error}</b> : null}
<FontAwesomeIcon icon={faPaperclip} size="xl" onClick={(e) => attachFile()} />
</div>
</div>
</div>
<div className="note-creator-actions">
<button className="secondary" type="button" onClick={cancel}>
Cancel
</button>
<button type="button" onClick={onSubmit}>
{props.replyTo ? 'Reply' : 'Send'}
</button>
</div>
</Modal>
)}
</>
);
}

View File

@ -1,9 +1,14 @@
import { useMemo, useState } from "react";
import { useSelector } from "react-redux";
import { faHeart, faReply, faThumbsDown, faTrash, faBolt, faRepeat, faEllipsisVertical, faShareNodes, faCopy } from "@fortawesome/free-solid-svg-icons";
import { faTrash, faRepeat, faShareNodes, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Menu, MenuItem } from '@szhsin/react-menu';
import Dislike from "Icons/Dislike";
import Heart from "Icons/Heart";
import Dots from "Icons/Dots";
import Zap from "Icons/Zap";
import Reply from "Icons/Reply";
import { formatShort } from "Number";
import useEventPublisher from "Feed/EventPublisher";
import { getReactions, hexToBech32, normalizeReaction, Reaction } from "Util";
@ -82,7 +87,7 @@ export default function NoteFooter(props: NoteFooterProps) {
<>
<div className="reaction-pill" onClick={() => setTip(true)}>
<div className="reaction-pill-icon">
<FontAwesomeIcon icon={faBolt} />
<Zap />
</div>
</div>
</>
@ -114,7 +119,7 @@ export default function NoteFooter(props: NoteFooterProps) {
<>
<div className={`reaction-pill ${hasReacted('+') ? 'reacted' : ''} `} onClick={() => react("+")}>
<div className="reaction-pill-icon">
<FontAwesomeIcon icon={faHeart} />
<Heart />
</div>
<div className="reaction-pill-number">
{formatShort(groupReactions[Reaction.Positive])}
@ -148,14 +153,14 @@ export default function NoteFooter(props: NoteFooterProps) {
function menuItems() {
return (
<>
{prefs.enableReactions && (<MenuItem onClick={() => react("-")}>
<div>
<FontAwesomeIcon icon={faThumbsDown} className={hasReacted('-') ? 'reacted' : ''} />
&nbsp;
{prefs.enableReactions && (
<MenuItem onClick={() => react("-")}>
<Dislike />
{formatShort(groupReactions[Reaction.Negative])}
</div>
Dislike
</MenuItem>)}
&nbsp;
Dislike
</MenuItem>
)}
<MenuItem onClick={() => share()}>
<FontAwesomeIcon icon={faShareNodes} />
Share
@ -183,18 +188,18 @@ export default function NoteFooter(props: NoteFooterProps) {
return (
<>
<div className="footer">
<div className={`reaction-pill ${reply ? 'reacted' : ''}`} onClick={(e) => setReply(s => !s)}>
<div className="reaction-pill-icon">
<FontAwesomeIcon icon={faReply} />
</div>
</div>
<Menu menuButton={<div className="reaction-pill">
<div className="reaction-pill-icon">
<FontAwesomeIcon icon={faEllipsisVertical} />
<Dots />
</div>
</div>} menuClassName="ctx-menu">
{menuItems()}
</Menu>
<div className={`reaction-pill ${reply ? 'reacted' : ''}`} onClick={(e) => setReply(s => !s)}>
<div className="reaction-pill-icon">
<Reply />
</div>
</div>
{reactionIcons()}
{tipButton()}
@ -204,6 +209,7 @@ export default function NoteFooter(props: NoteFooterProps) {
replyTo={ev}
onSend={() => setReply(false)}
show={reply}
setShow={setReply}
/>
<LNURLTip svc={author?.lud16 || author?.lud06} onClose={() => setTip(false)} show={tip} />
</>

View File

@ -3,7 +3,7 @@
}
.reaction > .note {
margin: 10px 20px;
margin: 10px 0;
}
.reaction > .header {

View File

@ -1,6 +1,6 @@
.pfp {
display: flex;
align-items: center;
display: flex;
align-items: center;
}
.pfp .avatar-wrapper {
@ -8,7 +8,6 @@
}
.pfp .avatar {
border-width: 1px;
width: 48px;
height: 48px;
cursor: pointer;
@ -23,13 +22,15 @@
text-decoration-color: var(--gray-superlight);
}
.pfp .profile-name {
.pfp .username {
display: flex;
flex-direction: column;
flex-direction: row;
align-items: center;
font-weight: bold;
}
.pfp .nip05 {
margin: 0;
margin-top: -.2em;
.pfp .profile-name {
display: flex;
flex-direction: column;
}

View File

@ -1,7 +1,7 @@
import "./ProfileImage.css";
import { useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { useNavigate, Link } from "react-router-dom";
import useProfile from "Feed/ProfileFeed";
import { hexToBech32, profileLink } from "Util";
import Avatar from "Element/Avatar"
@ -30,13 +30,18 @@ export default function ProfileImage({ pubkey, subHeader, showUsername = true, c
<div className="avatar-wrapper">
<Avatar user={user} onClick={() => navigate(link ?? profileLink(pubkey))} />
</div>
{showUsername && (<div className="f-grow pointer" onClick={e => { e.stopPropagation(); navigate(link ?? profileLink(pubkey)) }}>
<div className="profile-name">
<div>{name}</div>
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
{showUsername && (
<div className="profile-name f-grow">
<div className="username">
<Link className="display-name" key={pubkey} to={link ?? profileLink(pubkey)}>
{name}
</Link>
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
</div>
{subHeader ? <>{subHeader}</> : null}
</div>
<div className="subheader">
{subHeader}
</div>
</div>
)}
</div>
)

View File

@ -48,12 +48,12 @@
align-items: flex-start;
}
.nip05 {
font-size: 12px;
.user-item .nip05 {
font-size: var(--font-size-tiny);
}
.emoji-item {
font-size: 12px;
font-size: var(--font-size-tiny);
}
.emoji-item .emoji {

View File

@ -2,4 +2,4 @@
cursor: pointer;
font-weight: bold;
user-select: none;
}
}

View File

@ -49,7 +49,7 @@ export default function Timeline({ subject, postsOnly = false, method }: Timelin
}
return (
<>
<div className="main-content">
{latestFeed.length > 1 && (<div className="card latest-notes pointer" onClick={() => showLatest()}>
<FontAwesomeIcon icon={faForward} size="xl"/>
&nbsp;
@ -57,6 +57,6 @@ export default function Timeline({ subject, postsOnly = false, method }: Timelin
</div>)}
{mainFeed.map(eventElement)}
<LoadMore onLoadMore={loadMore} shouldLoadMore={main.end}/>
</>
</div>
);
}
}