login and onboarding fixes
This commit is contained in:
12
packages/app/src/Element/Logo.tsx
Normal file
12
packages/app/src/Element/Logo.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const Logo = () => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<h1 className="logo" onClick={() => navigate("/")}>
|
||||
Snort
|
||||
</h1>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logo;
|
@ -40,9 +40,25 @@
|
||||
.nip05 .domain[data-domain="nostriches.net"] {
|
||||
color: var(--highlight);
|
||||
background-color: var(--highlight);
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.nip05 .badge {
|
||||
color: var(--highlight);
|
||||
margin: 0.1em 0.2em;
|
||||
margin-left: 0.1em;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.nip05 .nick {
|
||||
display: none;
|
||||
}
|
||||
.nip05 .domain {
|
||||
display: none;
|
||||
}
|
||||
.nip05 .badge svg {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
margin-left: 0.15em;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
}
|
||||
|
@ -56,10 +56,10 @@ const Nip05 = ({ nip05, pubkey, verifyNip = true }: Nip05Params) => {
|
||||
|
||||
return (
|
||||
<div className={`flex nip05${couldNotVerify ? " failed" : ""}`} onClick={ev => ev.stopPropagation()}>
|
||||
{!isDefaultUser && isVerified && <div className="nick">{`${name}@`}</div>}
|
||||
{!isDefaultUser && isVerified && <span className="nick">{`${name}@`}</span>}
|
||||
{isVerified && (
|
||||
<>
|
||||
<span className="domain" data-domain={domain?.toLowerCase()}>
|
||||
<span className="domain f-ellipsis" data-domain={domain?.toLowerCase()}>
|
||||
{domain}
|
||||
</span>
|
||||
<span className="badge">
|
||||
|
@ -229,7 +229,13 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
{error && <b className="error">{error.error}</b>}
|
||||
{!registerStatus && (
|
||||
<div className="flex mb10">
|
||||
<input type="text" placeholder={formatMessage(messages.Handle)} value={handle} onChange={onHandleChange} />
|
||||
<input
|
||||
type="text"
|
||||
className="nip-handle"
|
||||
placeholder={formatMessage(messages.Handle)}
|
||||
value={handle}
|
||||
onChange={onHandleChange}
|
||||
/>
|
||||
@
|
||||
<select value={domain} onChange={onDomainChange}>
|
||||
{serviceConfig?.domains.map(a => (
|
||||
|
@ -197,3 +197,22 @@
|
||||
border-bottom-left-radius: 0;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.note .header .nip05 .badge {
|
||||
margin-top: -0.2em;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.note .reactions-link {
|
||||
color: var(--font-secondary-color);
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
margin-top: 4px;
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
|
||||
.note .reactions-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
@ -8,14 +8,14 @@ import { useIntl, FormattedMessage } from "react-intl";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import Bookmark from "Icons/Bookmark";
|
||||
import Pin from "Icons/Pin";
|
||||
import { Event as NEvent, EventKind } from "@snort/nostr";
|
||||
import { parseZap } from "Element/Zap";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import Text from "Element/Text";
|
||||
import { eventLink, getReactions, hexToBech32 } from "Util";
|
||||
import { eventLink, getReactions, dedupeByPubkey, hexToBech32, normalizeReaction, Reaction } from "Util";
|
||||
import NoteFooter, { Translation } from "Element/NoteFooter";
|
||||
import NoteTime from "Element/NoteTime";
|
||||
import { useUserProfiles } from "Feed/ProfileFeed";
|
||||
import { TaggedRawEvent, u256, HexKey } from "@snort/nostr";
|
||||
import { TaggedRawEvent, u256, HexKey, Event as NEvent, EventKind } from "@snort/nostr";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import { setPinned, setBookmarked } from "State/Login";
|
||||
import type { RootState } from "State/Store";
|
||||
@ -34,6 +34,7 @@ export interface NoteProps {
|
||||
showPinned?: boolean;
|
||||
showBookmarked?: boolean;
|
||||
showFooter?: boolean;
|
||||
showReactionsLink?: boolean;
|
||||
canUnpin?: boolean;
|
||||
canUnbookmark?: boolean;
|
||||
};
|
||||
@ -62,6 +63,7 @@ export default function Note(props: NoteProps) {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const { data, related, highlight, options: opt, ["data-ev"]: parsedEvent, ignoreModeration = false } = props;
|
||||
const [showReactions, setShowReactions] = useState(false);
|
||||
const ev = useMemo(() => parsedEvent ?? new NEvent(data), [data]);
|
||||
const pubKeys = useMemo(() => ev.Thread?.PubKeys || [], [ev]);
|
||||
const users = useUserProfiles(pubKeys);
|
||||
@ -76,6 +78,35 @@ export default function Note(props: NoteProps) {
|
||||
const publisher = useEventPublisher();
|
||||
const [translated, setTranslated] = useState<Translation>();
|
||||
const { formatMessage } = useIntl();
|
||||
const reactions = useMemo(() => getReactions(related, ev.Id, EventKind.Reaction), [related, ev]);
|
||||
const groupReactions = useMemo(() => {
|
||||
const result = reactions?.reduce(
|
||||
(acc, reaction) => {
|
||||
const kind = normalizeReaction(reaction.content);
|
||||
const rs = acc[kind] || [];
|
||||
return { ...acc, [kind]: [...rs, reaction] };
|
||||
},
|
||||
{
|
||||
[Reaction.Positive]: [] as TaggedRawEvent[],
|
||||
[Reaction.Negative]: [] as TaggedRawEvent[],
|
||||
}
|
||||
);
|
||||
return {
|
||||
[Reaction.Positive]: dedupeByPubkey(result[Reaction.Positive]),
|
||||
[Reaction.Negative]: dedupeByPubkey(result[Reaction.Negative]),
|
||||
};
|
||||
}, [reactions]);
|
||||
const positive = groupReactions[Reaction.Positive];
|
||||
const negative = groupReactions[Reaction.Negative];
|
||||
const reposts = useMemo(() => dedupeByPubkey(getReactions(related, ev.Id, EventKind.Repost)), [related, ev]);
|
||||
const zaps = useMemo(() => {
|
||||
const sortedZaps = getReactions(related, ev.Id, EventKind.ZapReceipt)
|
||||
.map(parseZap)
|
||||
.filter(z => z.valid && z.zapper !== ev.PubKey);
|
||||
sortedZaps.sort((a, b) => b.amount - a.amount);
|
||||
return sortedZaps;
|
||||
}, [related]);
|
||||
const totalReactions = positive.length + negative.length + reposts.length + zaps.length;
|
||||
|
||||
const options = {
|
||||
showHeader: true,
|
||||
@ -245,13 +276,29 @@ export default function Note(props: NoteProps) {
|
||||
<div className="body" onClick={e => goToEvent(e, ev.Id)}>
|
||||
{transformBody()}
|
||||
{translation()}
|
||||
{options.showReactionsLink && (
|
||||
<div className="reactions-link" onClick={() => setShowReactions(true)}>
|
||||
<FormattedMessage {...messages.ReactionsLink} values={{ n: totalReactions }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{extendable && !showMore && (
|
||||
<span className="expand-note mt10 flex f-center" onClick={() => setShowMore(true)}>
|
||||
<FormattedMessage {...messages.ShowMore} />
|
||||
</span>
|
||||
)}
|
||||
{options.showFooter && <NoteFooter ev={ev} related={related} onTranslated={t => setTranslated(t)} />}
|
||||
{options.showFooter && (
|
||||
<NoteFooter
|
||||
ev={ev}
|
||||
positive={positive}
|
||||
negative={negative}
|
||||
reposts={reposts}
|
||||
zaps={zaps}
|
||||
onTranslated={t => setTranslated(t)}
|
||||
showReactions={showReactions}
|
||||
setShowReactions={setShowReactions}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||
@ -20,13 +20,13 @@ import Zap from "Icons/Zap";
|
||||
import Reply from "Icons/Reply";
|
||||
import { formatShort } from "Number";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { getReactions, dedupeByPubkey, hexToBech32, normalizeReaction, Reaction } from "Util";
|
||||
import { hexToBech32, normalizeReaction } from "Util";
|
||||
import { NoteCreator } from "Element/NoteCreator";
|
||||
import Reactions from "Element/Reactions";
|
||||
import SendSats from "Element/SendSats";
|
||||
import { parseZap, ZapsSummary } from "Element/Zap";
|
||||
import { ParsedZap, ZapsSummary } from "Element/Zap";
|
||||
import { useUserProfile } from "Feed/ProfileFeed";
|
||||
import { Event as NEvent, EventKind, TaggedRawEvent, HexKey } from "@snort/nostr";
|
||||
import { Event as NEvent, TaggedRawEvent, HexKey } from "@snort/nostr";
|
||||
import { RootState } from "State/Store";
|
||||
import { UserPreferences, setPinned, setBookmarked } from "State/Login";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
@ -41,13 +41,18 @@ export interface Translation {
|
||||
}
|
||||
|
||||
export interface NoteFooterProps {
|
||||
related: TaggedRawEvent[];
|
||||
reposts: TaggedRawEvent[];
|
||||
zaps: ParsedZap[];
|
||||
positive: TaggedRawEvent[];
|
||||
negative: TaggedRawEvent[];
|
||||
showReactions: boolean;
|
||||
setShowReactions(b: boolean): void;
|
||||
ev: NEvent;
|
||||
onTranslated?: (content: Translation) => void;
|
||||
}
|
||||
|
||||
export default function NoteFooter(props: NoteFooterProps) {
|
||||
const { related, ev } = props;
|
||||
const { ev, showReactions, setShowReactions, positive, negative, reposts, zaps } = props;
|
||||
const dispatch = useDispatch();
|
||||
const { formatMessage } = useIntl();
|
||||
const { pinned, bookmarked } = useSelector((s: RootState) => s.login);
|
||||
@ -57,49 +62,17 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
const author = useUserProfile(ev.RootPubKey);
|
||||
const publisher = useEventPublisher();
|
||||
const [reply, setReply] = useState(false);
|
||||
const [showReactions, setShowReactions] = useState(false);
|
||||
const [tip, setTip] = useState(false);
|
||||
const isMine = ev.RootPubKey === login;
|
||||
const lang = window.navigator.language;
|
||||
const langNames = new Intl.DisplayNames([...window.navigator.languages], {
|
||||
type: "language",
|
||||
});
|
||||
const reactions = useMemo(() => getReactions(related, ev.Id, EventKind.Reaction), [related, ev]);
|
||||
const reposts = useMemo(() => dedupeByPubkey(getReactions(related, ev.Id, EventKind.Repost)), [related, ev]);
|
||||
const zaps = useMemo(() => {
|
||||
const sortedZaps = getReactions(related, ev.Id, EventKind.ZapReceipt)
|
||||
.map(parseZap)
|
||||
.filter(z => z.valid && z.zapper !== ev.PubKey);
|
||||
sortedZaps.sort((a, b) => b.amount - a.amount);
|
||||
return sortedZaps;
|
||||
}, [related]);
|
||||
const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
||||
const didZap = zaps.some(a => a.zapper === login);
|
||||
const groupReactions = useMemo(() => {
|
||||
const result = reactions?.reduce(
|
||||
(acc, reaction) => {
|
||||
const kind = normalizeReaction(reaction.content);
|
||||
const rs = acc[kind] || [];
|
||||
if (rs.map(e => e.pubkey).includes(reaction.pubkey)) {
|
||||
return acc;
|
||||
}
|
||||
return { ...acc, [kind]: [...rs, reaction] };
|
||||
},
|
||||
{
|
||||
[Reaction.Positive]: [] as TaggedRawEvent[],
|
||||
[Reaction.Negative]: [] as TaggedRawEvent[],
|
||||
}
|
||||
);
|
||||
return {
|
||||
[Reaction.Positive]: dedupeByPubkey(result[Reaction.Positive]),
|
||||
[Reaction.Negative]: dedupeByPubkey(result[Reaction.Negative]),
|
||||
};
|
||||
}, [reactions]);
|
||||
const positive = groupReactions[Reaction.Positive];
|
||||
const negative = groupReactions[Reaction.Negative];
|
||||
|
||||
function hasReacted(emoji: string) {
|
||||
return reactions?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === login);
|
||||
return positive?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === login);
|
||||
}
|
||||
|
||||
function hasReposted() {
|
||||
|
@ -1,7 +1,6 @@
|
||||
.pfp {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pfp .avatar-wrapper {
|
||||
@ -30,3 +29,16 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pfp .display-name {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (min-width: 520px) {
|
||||
.pfp .display-name {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import Avatar from "Element/Avatar";
|
||||
import Nip05 from "Element/Nip05";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
import { MetadataCache } from "State/Users";
|
||||
import useClientWidth from "Hooks/useClientWidth";
|
||||
|
||||
export interface ProfileImageProps {
|
||||
pubkey: HexKey;
|
||||
@ -31,6 +32,7 @@ export default function ProfileImage({
|
||||
const navigate = useNavigate();
|
||||
const user = useUserProfile(pubkey);
|
||||
const nip05 = defaultNip ? defaultNip : user?.nip05;
|
||||
const { ref, width } = useClientWidth();
|
||||
|
||||
const name = useMemo(() => {
|
||||
return getDisplayName(user, pubkey);
|
||||
@ -41,19 +43,21 @@ export default function ProfileImage({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`pfp${className ? ` ${className}` : ""}`}>
|
||||
<div className={`pfp${className ? ` ${className}` : ""}`} ref={ref}>
|
||||
<div className="avatar-wrapper">
|
||||
<Avatar user={user} onClick={() => navigate(link ?? profileLink(pubkey))} />
|
||||
</div>
|
||||
{showUsername && (
|
||||
<div className="profile-name f-grow">
|
||||
<div className="profile-name">
|
||||
<div className="username">
|
||||
<Link className="display-name" key={pubkey} to={link ?? profileLink(pubkey)}>
|
||||
{name}
|
||||
{nip05 && <Nip05 nip05={nip05} pubkey={pubkey} verifyNip={verifyNip} />}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="subheader">{subHeader}</div>
|
||||
<div className="subheader" style={{ width: width - 80 }}>
|
||||
{subHeader}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -5,11 +5,17 @@
|
||||
}
|
||||
|
||||
.profile-preview .pfp {
|
||||
flex-grow: 1;
|
||||
min-width: 200px;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.profile-preview .about {
|
||||
font-size: small;
|
||||
color: var(--gray-light);
|
||||
color: var(--font-secondary-color);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.profile-preview button {
|
||||
min-width: 98px;
|
||||
}
|
||||
|
@ -25,20 +25,22 @@ export default function ProfilePreview(props: ProfilePreviewProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`profile-preview${props.className ? ` ${props.className}` : ""}`} ref={ref}>
|
||||
{inView && (
|
||||
<>
|
||||
<ProfileImage
|
||||
pubkey={pubkey}
|
||||
subHeader={options.about ? <div className="f-ellipsis about">{user?.about}</div> : undefined}
|
||||
/>
|
||||
{props.actions ?? (
|
||||
<div className="follow-button-container">
|
||||
<FollowButton pubkey={pubkey} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
<div className={`profile-preview${props.className ? ` ${props.className}` : ""}`} ref={ref}>
|
||||
{inView && (
|
||||
<>
|
||||
<ProfileImage
|
||||
pubkey={pubkey}
|
||||
subHeader={options.about ? <div className="about">{user?.about}</div> : undefined}
|
||||
/>
|
||||
{props.actions ?? (
|
||||
<div className="follow-button-container">
|
||||
<FollowButton pubkey={pubkey} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -96,27 +96,4 @@
|
||||
.reactions-view .tab.disabled {
|
||||
display: none;
|
||||
}
|
||||
.reactions-item .reaction-icon {
|
||||
width: 42px;
|
||||
}
|
||||
.reactions-item .avatar {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
}
|
||||
.reactions-item .pfp .username {
|
||||
font-size: 14px;
|
||||
}
|
||||
.reactions-item .pfp .nip05 {
|
||||
display: none;
|
||||
}
|
||||
.reactions-item button {
|
||||
font-size: 14px;
|
||||
}
|
||||
.reactions-item .zap-reaction-icon svg {
|
||||
width: 12px;
|
||||
height: l2px;
|
||||
}
|
||||
.reactions-item .zap-amount {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
@ -327,7 +327,15 @@ export default function Thread(props: ThreadProps) {
|
||||
function renderRoot(note: NEvent) {
|
||||
const className = `thread-root ${isSingleNote ? "thread-root-single" : ""}`;
|
||||
if (note) {
|
||||
return <Note className={className} key={note.Id} data-ev={note} related={notes} />;
|
||||
return (
|
||||
<Note
|
||||
className={className}
|
||||
key={note.Id}
|
||||
data-ev={note}
|
||||
related={notes}
|
||||
options={{ showReactionsLink: true }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <NoteGhost className={className}>Loading thread root.. ({notes?.length} notes loaded)</NoteGhost>;
|
||||
}
|
||||
|
@ -69,7 +69,7 @@
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
|
||||
.top-zap .avatar {
|
||||
.top-zap .summary .pfp .avatar-wrapper .avatar {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
@ -85,7 +85,7 @@
|
||||
}
|
||||
|
||||
.amount-number {
|
||||
font-weight: bold;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.zap.note .body {
|
||||
|
@ -102,4 +102,5 @@ export default defineMessages({
|
||||
All: { defaultMessage: "All" },
|
||||
ConfirmUnbookmark: { defaultMessage: "Are you sure you want to remove this note from bookmarks?" },
|
||||
ConfirmUnpin: { defaultMessage: "Are you sure you want to unpin this note?" },
|
||||
ReactionsLink: { defaultMessage: "{n} Reactions" },
|
||||
});
|
||||
|
Reference in New Issue
Block a user