refactor: polish
This commit is contained in:
parent
4b3e7710e0
commit
c274c0a842
@ -42,7 +42,7 @@
|
|||||||
top: 48px;
|
top: 48px;
|
||||||
border-left: 1px solid var(--border-color);
|
border-left: 1px solid var(--border-color);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subthread-container.subthread-mid:not(.subthread-last) .line-container:before {
|
.subthread-container.subthread-mid:not(.subthread-last) .line-container:before {
|
||||||
@ -52,7 +52,7 @@
|
|||||||
left: calc(48px / 2 + 16px);
|
left: calc(48px / 2 + 16px);
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
z-index: 1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subthread-container.subthread-last .line-container:before {
|
.subthread-container.subthread-last .line-container:before {
|
||||||
@ -62,7 +62,7 @@
|
|||||||
left: calc(48px / 2 + 16px);
|
left: calc(48px / 2 + 16px);
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
z-index: 1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
|
@ -63,6 +63,7 @@
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ export default function TrendingUsers({
|
|||||||
pubkeys={trendingUsersData.slice(0, count) as HexKey[]}
|
pubkeys={trendingUsersData.slice(0, count) as HexKey[]}
|
||||||
title={title}
|
title={title}
|
||||||
showFollowAll={true}
|
showFollowAll={true}
|
||||||
|
className="flex flex-col gap-2"
|
||||||
profilePreviewProps={{
|
profilePreviewProps={{
|
||||||
options: {
|
options: {
|
||||||
about: true,
|
about: true,
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
.avatar {
|
|
||||||
@apply rounded-full;
|
|
||||||
height: 210px;
|
|
||||||
width: 210px;
|
|
||||||
background-color: var(--gray);
|
|
||||||
z-index: 2;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar[data-domain="iris.to"],
|
|
||||||
.avatar[data-domain="snort.social"] {
|
|
||||||
background-image: var(--img-url), var(--snort-gradient);
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar .overlay {
|
|
||||||
color: white;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: rgba(0, 0, 0, 0.4);
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar .icons {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transform-origin: center;
|
|
||||||
transform: rotate(-135deg) translateY(50%);
|
|
||||||
}
|
|
@ -1,8 +1,6 @@
|
|||||||
import "./Avatar.css";
|
|
||||||
|
|
||||||
import type { UserMetadata } from "@snort/system";
|
import type { UserMetadata } from "@snort/system";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { ReactNode, useMemo } from "react";
|
import { HTMLProps, ReactNode, useMemo } from "react";
|
||||||
|
|
||||||
import { ProxyImg } from "@/Components/ProxyImg";
|
import { ProxyImg } from "@/Components/ProxyImg";
|
||||||
import { defaultAvatar, getDisplayName } from "@/Utils";
|
import { defaultAvatar, getDisplayName } from "@/Utils";
|
||||||
@ -22,14 +20,15 @@ interface AvatarProps {
|
|||||||
const Avatar = ({
|
const Avatar = ({
|
||||||
pubkey,
|
pubkey,
|
||||||
user,
|
user,
|
||||||
size,
|
size = 48,
|
||||||
onClick,
|
onClick,
|
||||||
image,
|
image,
|
||||||
imageOverlay,
|
imageOverlay,
|
||||||
icons,
|
icons,
|
||||||
className,
|
className,
|
||||||
showTitle = true,
|
showTitle = true,
|
||||||
}: AvatarProps) => {
|
...others
|
||||||
|
}: AvatarProps & Omit<HTMLProps<HTMLDivElement>, "onClick" | "style" | "className">) => {
|
||||||
const defaultImg = defaultAvatar(pubkey);
|
const defaultImg = defaultAvatar(pubkey);
|
||||||
const url = useMemo(() => {
|
const url = useMemo(() => {
|
||||||
if ((image?.length ?? 0) > 0) return image;
|
if ((image?.length ?? 0) > 0) return image;
|
||||||
@ -51,22 +50,39 @@ const Avatar = ({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
style={style}
|
style={style}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"avatar relative flex items-center justify-center",
|
"relative rounded-full aspect-square flex items-center justify-center gap-2 bg-gray",
|
||||||
{ "with-overlay": imageOverlay },
|
|
||||||
{ "outline outline-2 outline-nostr-purple m-[2px]": isDefault },
|
{ "outline outline-2 outline-nostr-purple m-[2px]": isDefault },
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
data-domain={domain?.toLowerCase()}
|
data-domain={domain?.toLowerCase()}
|
||||||
title={showTitle ? getDisplayName(user, "") : undefined}>
|
title={showTitle ? getDisplayName(user, "") : undefined}
|
||||||
|
{...others}>
|
||||||
<ProxyImg
|
<ProxyImg
|
||||||
className="rounded-full object-cover aspect-square"
|
className="absolute rounded-full w-full h-full object-cover"
|
||||||
src={url}
|
src={url}
|
||||||
size={s}
|
size={s}
|
||||||
alt={getDisplayName(user, "")}
|
alt={getDisplayName(user, "")}
|
||||||
promptToLoadDirectly={false}
|
promptToLoadDirectly={false}
|
||||||
/>
|
/>
|
||||||
{icons && <div className="icons">{icons}</div>}
|
{icons && (
|
||||||
{imageOverlay && <div className="overlay">{imageOverlay}</div>}
|
<div
|
||||||
|
className="absolute flex items-center justify-center w-full h-full origin-center"
|
||||||
|
style={{
|
||||||
|
transform: "rotate(-135deg) translateY(50%)",
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
transform: "rotate(135deg)",
|
||||||
|
}}>
|
||||||
|
{icons}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{imageOverlay && (
|
||||||
|
<div className="absolute rounded-full bg-black/40 w-full h-full flex items-center justify-center">
|
||||||
|
{imageOverlay}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { HexKey } from "@snort/system";
|
import { HexKey } from "@snort/system";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import ProfileImage from "@/Components/User/ProfileImage";
|
import ProfileImage from "@/Components/User/ProfileImage";
|
||||||
|
|
||||||
@ -13,6 +12,8 @@ export function AvatarGroup({ ids, onClick, size }: { ids: HexKey[]; onClick?: (
|
|||||||
pubkey={a}
|
pubkey={a}
|
||||||
size={size ?? 24}
|
size={size ?? 24}
|
||||||
showUsername={false}
|
showUsername={false}
|
||||||
|
showBadges={false}
|
||||||
|
showProfileCard={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { HexKey, socialGraphInstance } from "@snort/system";
|
import { HexKey, socialGraphInstance } from "@snort/system";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import Icon from "@/Components/Icons/Icon";
|
import Icon from "@/Components/Icons/Icon";
|
||||||
|
|
||||||
@ -31,8 +30,10 @@ export default function FollowDistanceIndicator({ pubkey, className }: FollowDis
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={classNames("icon-circle", className)} title={title}>
|
<div
|
||||||
|
className={classNames("w-5 h-5 bg-gray-superdark rounded-full flex items-center justify-center", className)}
|
||||||
|
title={title}>
|
||||||
<Icon name="check" className={followDistanceColor} size={10} />
|
<Icon name="check" className={followDistanceColor} size={10} />
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ export default function FollowListBase({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col g8">
|
<div className="flex flex-col gap-2">
|
||||||
{(showFollowAll ?? true) && (
|
{(showFollowAll ?? true) && (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="grow font-bold">{title}</div>
|
<div className="grow font-bold">{title}</div>
|
||||||
@ -45,9 +45,7 @@ export default function FollowListBase({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{pubkeys?.map((a, index) => (
|
{pubkeys?.slice(0, 20).map(a => <ProfilePreview pubkey={a} key={a} {...profilePreviewProps} />)}
|
||||||
<ProfilePreview pubkey={a} key={a} waitUntilInView={index > 10} {...profilePreviewProps} />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -4,7 +4,6 @@ import { FormattedMessage } from "react-intl";
|
|||||||
|
|
||||||
import { AvatarGroup } from "@/Components/User/AvatarGroup";
|
import { AvatarGroup } from "@/Components/User/AvatarGroup";
|
||||||
import DisplayName from "@/Components/User/DisplayName";
|
import DisplayName from "@/Components/User/DisplayName";
|
||||||
import FollowDistanceIndicator from "@/Components/User/FollowDistanceIndicator";
|
|
||||||
import { ProfileLink } from "@/Components/User/ProfileLink";
|
import { ProfileLink } from "@/Components/User/ProfileLink";
|
||||||
|
|
||||||
const MAX_FOLLOWED_BY_FRIENDS = 3;
|
const MAX_FOLLOWED_BY_FRIENDS = 3;
|
||||||
@ -31,9 +30,8 @@ export default function FollowedBy({ pubkey }: { pubkey: HexKey }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex items-center">
|
||||||
<FollowDistanceIndicator className="p-2" pubkey={pubkey} />
|
|
||||||
<AvatarGroup ids={followedByFriendsArray} />
|
<AvatarGroup ids={followedByFriendsArray} />
|
||||||
</div>
|
</div>
|
||||||
{totalFollowedByFriends > 0 && (
|
{totalFollowedByFriends > 0 && (
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
.pfp {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: min-content auto;
|
|
||||||
align-items: center;
|
|
||||||
text-decoration: none;
|
|
||||||
user-select: none;
|
|
||||||
min-width: 0;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pfp .avatar {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.pfp {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pfp .profile-name {
|
|
||||||
max-width: stretch;
|
|
||||||
max-width: -webkit-fill-available;
|
|
||||||
max-width: -moz-available;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pfp a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pfp .icon-circle {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transform-origin: center;
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: 100%;
|
|
||||||
background-color: var(--gray-superdark);
|
|
||||||
transform: rotate(135deg);
|
|
||||||
}
|
|
@ -1,5 +1,3 @@
|
|||||||
import "./ProfileImage.css";
|
|
||||||
|
|
||||||
import { HexKey, UserMetadata } from "@snort/system";
|
import { HexKey, UserMetadata } from "@snort/system";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
@ -31,6 +29,7 @@ export interface ProfileImageProps {
|
|||||||
icons?: ReactNode;
|
icons?: ReactNode;
|
||||||
showProfileCard?: boolean;
|
showProfileCard?: boolean;
|
||||||
showBadges?: boolean;
|
showBadges?: boolean;
|
||||||
|
displayNameClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProfileImage({
|
export default function ProfileImage({
|
||||||
@ -48,6 +47,7 @@ export default function ProfileImage({
|
|||||||
icons,
|
icons,
|
||||||
showProfileCard = false,
|
showProfileCard = false,
|
||||||
showBadges = false,
|
showBadges = false,
|
||||||
|
displayNameClassName,
|
||||||
}: ProfileImageProps) {
|
}: ProfileImageProps) {
|
||||||
const user = useUserProfile(profile ? "" : pubkey) ?? profile;
|
const user = useUserProfile(profile ? "" : pubkey) ?? profile;
|
||||||
const [isHovering, setIsHovering] = useState(false);
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
@ -75,30 +75,29 @@ export default function ProfileImage({
|
|||||||
function inner() {
|
function inner() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="avatar-wrapper" onMouseEnter={handleMouseEnter}>
|
<Avatar
|
||||||
<Avatar
|
pubkey={pubkey}
|
||||||
pubkey={pubkey}
|
user={user}
|
||||||
user={user}
|
size={size}
|
||||||
size={size}
|
imageOverlay={imageOverlay}
|
||||||
imageOverlay={imageOverlay}
|
showTitle={!showProfileCard}
|
||||||
showTitle={!showProfileCard}
|
onMouseEnter={handleMouseEnter}
|
||||||
icons={
|
icons={
|
||||||
showFollowDistance || icons ? (
|
showFollowDistance || icons ? (
|
||||||
<>
|
<>
|
||||||
{icons}
|
{icons}
|
||||||
{showFollowDistance && <FollowDistanceIndicator pubkey={pubkey} />}
|
{showFollowDistance && <FollowDistanceIndicator pubkey={pubkey} />}
|
||||||
</>
|
</>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
{showUsername && (
|
{showUsername && (
|
||||||
<div className="f-ellipsis">
|
<div className={displayNameClassName}>
|
||||||
<div className="flex gap-2 items-center font-medium">
|
<div className="flex gap-2 items-center font-medium">
|
||||||
{overrideUsername ? overrideUsername : <DisplayName pubkey={pubkey} user={user} />}
|
{overrideUsername ? overrideUsername : <DisplayName pubkey={pubkey} user={user} />}
|
||||||
{leader && showBadges && CONFIG.features.communityLeaders && <LeaderBadge />}
|
{leader && showBadges && CONFIG.features.communityLeaders && <LeaderBadge />}
|
||||||
</div>
|
</div>
|
||||||
<div className="subheader">{subHeader}</div>
|
{subHeader}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -108,7 +107,7 @@ export default function ProfileImage({
|
|||||||
function profileCard() {
|
function profileCard() {
|
||||||
if (showProfileCard && user && isHovering) {
|
if (showProfileCard && user && isHovering) {
|
||||||
return (
|
return (
|
||||||
<div className="absolute shadow-lg z-10 fade-in">
|
<div className="absolute shadow-lg fade-in">
|
||||||
<ProfileCard pubkey={pubkey} user={user} show={true} delay={100} />
|
<ProfileCard pubkey={pubkey} user={user} show={true} delay={100} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -116,10 +115,17 @@ export default function ProfileImage({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const classNamesOverInner = classNames(
|
||||||
|
"min-w-0",
|
||||||
|
{
|
||||||
|
"grid grid-cols-[min-content_auto] gap-3 items-center": showUsername,
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
);
|
||||||
if (link === "") {
|
if (link === "") {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={classNames("pfp", className)} onClick={handleClick}>
|
<div className={classNamesOverInner} onClick={handleClick}>
|
||||||
{inner()}
|
{inner()}
|
||||||
</div>
|
</div>
|
||||||
{profileCard()}
|
{profileCard()}
|
||||||
@ -127,12 +133,12 @@ export default function ProfileImage({
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className="relative" onMouseLeave={handleMouseLeave}>
|
<div onMouseLeave={handleMouseLeave}>
|
||||||
<ProfileLink
|
<ProfileLink
|
||||||
pubkey={pubkey}
|
pubkey={pubkey}
|
||||||
className={classNames("pfp", className)}
|
|
||||||
user={user}
|
user={user}
|
||||||
explicitLink={link}
|
explicitLink={link}
|
||||||
|
className={classNamesOverInner}
|
||||||
onClick={handleClick}>
|
onClick={handleClick}>
|
||||||
{inner()}
|
{inner()}
|
||||||
</ProfileLink>
|
</ProfileLink>
|
||||||
|
@ -4,6 +4,7 @@ import { ReactNode, useContext } from "react";
|
|||||||
import { Link, LinkProps } from "react-router-dom";
|
import { Link, LinkProps } from "react-router-dom";
|
||||||
|
|
||||||
import { randomSample } from "@/Utils";
|
import { randomSample } from "@/Utils";
|
||||||
|
import { useProfileLink } from "@/Hooks/useProfileLink";
|
||||||
|
|
||||||
export function ProfileLink({
|
export function ProfileLink({
|
||||||
pubkey,
|
pubkey,
|
||||||
@ -17,39 +18,13 @@ export function ProfileLink({
|
|||||||
explicitLink?: string;
|
explicitLink?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
} & Omit<LinkProps, "to">) {
|
} & Omit<LinkProps, "to">) {
|
||||||
const system = useContext(SnortContext);
|
const link = useProfileLink(pubkey, user);
|
||||||
const relays = system.relayCache
|
|
||||||
.getFromCache(pubkey)
|
|
||||||
?.relays?.filter(a => a.settings.write)
|
|
||||||
?.map(a => a.url);
|
|
||||||
|
|
||||||
function profileLink() {
|
|
||||||
if (explicitLink) {
|
|
||||||
return explicitLink;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
user?.nip05 &&
|
|
||||||
user.nip05.endsWith(`@${CONFIG.nip05Domain}`) &&
|
|
||||||
(!("isNostrAddressValid" in user) || user.isNostrAddressValid)
|
|
||||||
) {
|
|
||||||
const [username] = user.nip05.split("@");
|
|
||||||
return `/${username}`;
|
|
||||||
}
|
|
||||||
return `/${new NostrLink(
|
|
||||||
NostrPrefix.Profile,
|
|
||||||
pubkey,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
relays ? randomSample(relays, 3) : undefined,
|
|
||||||
).encode(CONFIG.profileLinkPrefix)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oFiltered = others as Record<string, unknown>;
|
const oFiltered = others as Record<string, unknown>;
|
||||||
delete oFiltered["user"];
|
delete oFiltered["user"];
|
||||||
delete oFiltered["link"];
|
delete oFiltered["link"];
|
||||||
delete oFiltered["children"];
|
delete oFiltered["children"];
|
||||||
return (
|
return (
|
||||||
<Link {...oFiltered} to={profileLink()} state={user}>
|
<Link {...oFiltered} to={explicitLink ?? link} state={user}>
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
.profile-preview {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 59px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-preview .pfp {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-preview .about {
|
|
||||||
font-size: small;
|
|
||||||
color: var(--font-secondary-color);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
@ -1,9 +1,7 @@
|
|||||||
import "./ProfilePreview.css";
|
|
||||||
|
|
||||||
import { HexKey, UserMetadata } from "@snort/system";
|
import { HexKey, UserMetadata } from "@snort/system";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
import { ReactNode } from "react";
|
import classNames from "classnames";
|
||||||
import { useInView } from "react-intersection-observer";
|
import { forwardRef, ReactNode } from "react";
|
||||||
|
|
||||||
import FollowButton from "@/Components/User/FollowButton";
|
import FollowButton from "@/Components/User/FollowButton";
|
||||||
import ProfileImage, { ProfileImageProps } from "@/Components/User/ProfileImage";
|
import ProfileImage, { ProfileImageProps } from "@/Components/User/ProfileImage";
|
||||||
@ -17,13 +15,14 @@ export interface ProfilePreviewProps {
|
|||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
waitUntilInView?: boolean;
|
|
||||||
profileImageProps?: Omit<ProfileImageProps, "pubkey" | "profile">;
|
profileImageProps?: Omit<ProfileImageProps, "pubkey" | "profile">;
|
||||||
}
|
}
|
||||||
export default function ProfilePreview(props: ProfilePreviewProps) {
|
const ProfilePreview = forwardRef<HTMLDivElement, ProfilePreviewProps>(function ProfilePreview(
|
||||||
|
props: ProfilePreviewProps,
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
const pubkey = props.pubkey;
|
const pubkey = props.pubkey;
|
||||||
const { ref, inView } = useInView({ triggerOnce: true, rootMargin: "500px" });
|
const user = useUserProfile(pubkey);
|
||||||
const user = useUserProfile(inView ? pubkey : undefined);
|
|
||||||
const options = {
|
const options = {
|
||||||
about: true,
|
about: true,
|
||||||
...props.options,
|
...props.options,
|
||||||
@ -39,26 +38,29 @@ export default function ProfilePreview(props: ProfilePreviewProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div className={classNames("flex items-center justify-between", props.className)} ref={ref} onClick={handleClick}>
|
||||||
className={`justify-between profile-preview${props.className ? ` ${props.className}` : ""}`}
|
<ProfileImage
|
||||||
ref={ref}
|
pubkey={pubkey}
|
||||||
onClick={handleClick}>
|
profile={props.profile}
|
||||||
{(!props.waitUntilInView || inView) && (
|
className="overflow-hidden"
|
||||||
<>
|
displayNameClassName="min-w-0"
|
||||||
<ProfileImage
|
subHeader={
|
||||||
pubkey={pubkey}
|
options.about && (
|
||||||
profile={props.profile}
|
<div className="text-sm text-secondary whitespace-nowrap text-ellipsis overflow-hidden">
|
||||||
subHeader={options.about && <div className="about">{user?.about}</div>}
|
{user?.about}
|
||||||
{...props.profileImageProps}
|
|
||||||
/>
|
|
||||||
{props.actions ?? (
|
|
||||||
<div className="whitespace-nowrap">
|
|
||||||
<FollowButton pubkey={pubkey} />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
</>
|
}
|
||||||
|
{...props.profileImageProps}
|
||||||
|
/>
|
||||||
|
{props.actions ?? (
|
||||||
|
<div className="whitespace-nowrap">
|
||||||
|
<FollowButton pubkey={pubkey} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default ProfilePreview;
|
||||||
|
@ -86,9 +86,9 @@ export default function ZapModal(props: SendSatsProps) {
|
|||||||
if (!(props.show ?? false)) return null;
|
if (!(props.show ?? false)) return null;
|
||||||
return (
|
return (
|
||||||
<Modal id="send-sats" className="lnurl-modal" onClose={onClose}>
|
<Modal id="send-sats" className="lnurl-modal" onClose={onClose}>
|
||||||
<div className="p flex flex-col g12">
|
<div className="p flex flex-col gap-3">
|
||||||
<div className="flex g12">
|
<div className="flex gap-3">
|
||||||
<div className="flex items-center grow">
|
<div className="flex items-center grow gap-3">
|
||||||
{props.title || <ZapModalTitle amount={amount} targets={props.targets} zapper={zapper} />}
|
{props.title || <ZapModalTitle amount={amount} targets={props.targets} zapper={zapper} />}
|
||||||
</div>
|
</div>
|
||||||
<CloseButton onClick={onClose} />
|
<CloseButton onClick={onClose} />
|
||||||
|
@ -10,7 +10,7 @@ export function useArticles() {
|
|||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
const rb = new RequestBuilder("articles");
|
const rb = new RequestBuilder("articles");
|
||||||
if (followList.length > 0) {
|
if (followList.length > 0) {
|
||||||
rb.withFilter().kinds([EventKind.LongFormTextNote]).authors(followList).limit(20);
|
rb.withFilter().kinds([EventKind.LongFormTextNote]).authors(followList);
|
||||||
}
|
}
|
||||||
return rb;
|
return rb;
|
||||||
}, [followList]);
|
}, [followList]);
|
||||||
|
@ -8,14 +8,14 @@ export function useNotificationsView() {
|
|||||||
const publicKey = useLogin(s => s.publicKey);
|
const publicKey = useLogin(s => s.publicKey);
|
||||||
const kinds = [EventKind.TextNote, EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt];
|
const kinds = [EventKind.TextNote, EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt];
|
||||||
const req = useMemo(() => {
|
const req = useMemo(() => {
|
||||||
|
const rb = new RequestBuilder("notifications");
|
||||||
|
rb.withOptions({
|
||||||
|
leaveOpen: true,
|
||||||
|
});
|
||||||
if (publicKey) {
|
if (publicKey) {
|
||||||
const rb = new RequestBuilder("notifications");
|
|
||||||
rb.withOptions({
|
|
||||||
leaveOpen: true,
|
|
||||||
});
|
|
||||||
rb.withFilter().kinds(kinds).tag("p", [publicKey]);
|
rb.withFilter().kinds(kinds).tag("p", [publicKey]);
|
||||||
return rb;
|
|
||||||
}
|
}
|
||||||
|
return rb;
|
||||||
}, [publicKey]);
|
}, [publicKey]);
|
||||||
return useRequestBuilder(req);
|
return useRequestBuilder(req);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { NostrLink } from "@snort/system";
|
import { NostrLink } from "@snort/system";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import useLogin from "./useLogin";
|
import useLogin from "./useLogin";
|
||||||
|
|
||||||
@ -6,27 +7,30 @@ import useLogin from "./useLogin";
|
|||||||
* Simple hook for adding / removing follows
|
* Simple hook for adding / removing follows
|
||||||
*/
|
*/
|
||||||
export default function useFollowsControls() {
|
export default function useFollowsControls() {
|
||||||
const { state } = useLogin(s => ({ v: s.state.version, state: s.state }));
|
const { state, v } = useLogin(s => ({ v: s.state.version, state: s.state }));
|
||||||
|
|
||||||
return {
|
return useMemo(() => {
|
||||||
isFollowing: (pk: string) => {
|
const follows = state.follows;
|
||||||
return state.follows?.includes(pk);
|
return {
|
||||||
},
|
isFollowing: (pk: string) => {
|
||||||
addFollow: async (pk: Array<string>) => {
|
return follows?.includes(pk);
|
||||||
for (const p of pk) {
|
},
|
||||||
await state.follow(NostrLink.publicKey(p), false);
|
addFollow: async (pk: Array<string>) => {
|
||||||
}
|
for (const p of pk) {
|
||||||
await state.saveContacts();
|
await state.follow(NostrLink.publicKey(p), false);
|
||||||
},
|
}
|
||||||
removeFollow: async (pk: Array<string>) => {
|
await state.saveContacts();
|
||||||
for (const p of pk) {
|
},
|
||||||
await state.unfollow(NostrLink.publicKey(p), false);
|
removeFollow: async (pk: Array<string>) => {
|
||||||
}
|
for (const p of pk) {
|
||||||
await state.saveContacts();
|
await state.unfollow(NostrLink.publicKey(p), false);
|
||||||
},
|
}
|
||||||
setFollows: async (pk: Array<string>) => {
|
await state.saveContacts();
|
||||||
await state.replaceFollows(pk.map(a => NostrLink.publicKey(a)));
|
},
|
||||||
},
|
setFollows: async (pk: Array<string>) => {
|
||||||
followList: state.follows ?? [],
|
await state.replaceFollows(pk.map(a => NostrLink.publicKey(a)));
|
||||||
};
|
},
|
||||||
|
followList: follows ?? [],
|
||||||
|
};
|
||||||
|
}, [v]);
|
||||||
}
|
}
|
||||||
|
25
packages/app/src/Hooks/useProfileLink.ts
Normal file
25
packages/app/src/Hooks/useProfileLink.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { CachedMetadata, NostrLink, UserMetadata } from "@snort/system";
|
||||||
|
import { SnortContext } from "@snort/system-react";
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
import { randomSample } from "@/Utils";
|
||||||
|
|
||||||
|
export function useProfileLink(pubkey?: string, user?: UserMetadata | CachedMetadata) {
|
||||||
|
const system = useContext(SnortContext);
|
||||||
|
if (!pubkey) return "#";
|
||||||
|
const relays = system.relayCache
|
||||||
|
.getFromCache(pubkey)
|
||||||
|
?.relays?.filter(a => a.settings.write)
|
||||||
|
?.map(a => a.url);
|
||||||
|
|
||||||
|
if (
|
||||||
|
user?.nip05 &&
|
||||||
|
user.nip05.endsWith(`@${CONFIG.nip05Domain}`) &&
|
||||||
|
(!("isNostrAddressValid" in user) || user.isNostrAddressValid)
|
||||||
|
) {
|
||||||
|
const [username] = user.nip05.split("@");
|
||||||
|
return `/${username}`;
|
||||||
|
}
|
||||||
|
const link = NostrLink.profile(pubkey, relays ? randomSample(relays, 3) : undefined);
|
||||||
|
return `/${link.encode(CONFIG.profileLinkPrefix)}`;
|
||||||
|
}
|
20
packages/app/src/Hooks/useWindowSize.ts
Normal file
20
packages/app/src/Hooks/useWindowSize.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function useWindowSize() {
|
||||||
|
const [dims, setDims] = useState({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = () => {
|
||||||
|
setDims({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
window.addEventListener("resize", handler);
|
||||||
|
return () => window.removeEventListener("resize", handler);
|
||||||
|
}, []);
|
||||||
|
return dims;
|
||||||
|
}
|
@ -1,11 +1,15 @@
|
|||||||
import { socialGraphInstance, TaggedNostrEvent } from "@snort/system";
|
import { socialGraphInstance, TaggedNostrEvent } from "@snort/system";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export default function useWoT() {
|
export default function useWoT() {
|
||||||
return {
|
return useMemo(
|
||||||
sortEvents: (events: Array<TaggedNostrEvent>) =>
|
() => ({
|
||||||
events.sort(
|
sortEvents: (events: Array<TaggedNostrEvent>) =>
|
||||||
(a, b) => socialGraphInstance.getFollowDistance(a.pubkey) - socialGraphInstance.getFollowDistance(b.pubkey),
|
events.sort(
|
||||||
),
|
(a, b) => socialGraphInstance.getFollowDistance(a.pubkey) - socialGraphInstance.getFollowDistance(b.pubkey),
|
||||||
followDistance: (pk: string) => socialGraphInstance.getFollowDistance(pk),
|
),
|
||||||
};
|
followDistance: (pk: string) => socialGraphInstance.getFollowDistance(pk),
|
||||||
|
}),
|
||||||
|
[socialGraphInstance.root],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { useUserProfile } from "@snort/system-react";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useIntl } from "react-intl";
|
|
||||||
|
|
||||||
import NavLink from "@/Components/Button/NavLink";
|
import NavLink from "@/Components/Button/NavLink";
|
||||||
import { NoteCreatorButton } from "@/Components/Event/Create/NoteCreatorButton";
|
import { NoteCreatorButton } from "@/Components/Event/Create/NoteCreatorButton";
|
||||||
import Icon from "@/Components/Icons/Icon";
|
import Icon from "@/Components/Icons/Icon";
|
||||||
import Avatar from "@/Components/User/Avatar";
|
|
||||||
import { ProfileLink } from "@/Components/User/ProfileLink";
|
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
|
import useWindowSize from "@/Hooks/useWindowSize";
|
||||||
|
|
||||||
|
import ProfileMenu from "./ProfileMenu";
|
||||||
|
|
||||||
type MenuItem = {
|
type MenuItem = {
|
||||||
label?: string;
|
label?: string;
|
||||||
@ -34,33 +33,21 @@ const MENU_ITEMS: MenuItem[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
const { publicKey, readonly } = useLogin(s => ({
|
const { readonly } = useLogin(s => ({
|
||||||
publicKey: s.publicKey,
|
|
||||||
readonly: s.readonly,
|
readonly: s.readonly,
|
||||||
}));
|
}));
|
||||||
const profile = useUserProfile(publicKey);
|
const pageSize = useWindowSize();
|
||||||
const { formatMessage } = useIntl();
|
const isMobile = pageSize.width <= 768; //max-md
|
||||||
|
if (!isMobile) return;
|
||||||
const readOnlyIcon = readonly && (
|
|
||||||
<span style={{ transform: "rotate(135deg)" }} title={formatMessage({ defaultMessage: "Read-only", id: "djNL6D" })}>
|
|
||||||
<Icon name="openeye" className="text-nostr-red" size={20} />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="md:hidden fixed bottom-0 z-10 w-full bg-base-200 pb-safe-area bg-background">
|
<footer className="md:hidden fixed bottom-0 z-10 w-full bg-base-200 pb-safe-area bg-background">
|
||||||
<div className="flex">
|
<div className="grid grid-flow-col">
|
||||||
{MENU_ITEMS.map((item, index) => (
|
{MENU_ITEMS.map((item, index) => (
|
||||||
<FooterNavItem key={index} item={item} readonly={readonly} />
|
<FooterNavItem key={index} item={item} readonly={readonly} />
|
||||||
))}
|
))}
|
||||||
{publicKey && (
|
|
||||||
<ProfileLink
|
<ProfileMenu className="flex justify-center items-center" />
|
||||||
className="flex flex-grow p-2 justify-center items-center cursor-pointer"
|
|
||||||
pubkey={publicKey}
|
|
||||||
user={profile}>
|
|
||||||
<Avatar pubkey={publicKey} user={profile} icons={readOnlyIcon} size={40} />
|
|
||||||
</ProfileLink>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
@ -83,7 +70,7 @@ const FooterNavItem = ({ item, readonly }: { item: MenuItem; readonly: boolean }
|
|||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
classNames({ active: isActive || isHovered }, "flex flex-grow p-4 justify-center items-center cursor-pointer")
|
classNames({ active: isActive || isHovered }, "flex flex-1 p-4 justify-center items-center cursor-pointer")
|
||||||
}>
|
}>
|
||||||
<Icon name={`${item.icon}-solid`} className="icon-solid" size={24} />
|
<Icon name={`${item.icon}-solid`} className="icon-solid" size={24} />
|
||||||
<Icon name={`${item.icon}-outline`} className="icon-outline" size={24} />
|
<Icon name={`${item.icon}-outline`} className="icon-outline" size={24} />
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
import { useUserProfile } from "@snort/system-react";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import NavLink from "@/Components/Button/NavLink";
|
import NavLink from "@/Components/Button/NavLink";
|
||||||
import { NoteCreatorButton } from "@/Components/Event/Create/NoteCreatorButton";
|
import { NoteCreatorButton } from "@/Components/Event/Create/NoteCreatorButton";
|
||||||
import Icon from "@/Components/Icons/Icon";
|
import Icon from "@/Components/Icons/Icon";
|
||||||
import Avatar from "@/Components/User/Avatar";
|
|
||||||
import { ProfileLink } from "@/Components/User/ProfileLink";
|
|
||||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
|
import useWindowSize from "@/Hooks/useWindowSize";
|
||||||
import { HasNotificationsMarker } from "@/Pages/Layout/HasNotificationsMarker";
|
import { HasNotificationsMarker } from "@/Pages/Layout/HasNotificationsMarker";
|
||||||
import { WalletBalance } from "@/Pages/Layout/WalletBalance";
|
import { WalletBalance } from "@/Pages/Layout/WalletBalance";
|
||||||
import { subscribeToNotifications } from "@/Utils/Notifications";
|
import { subscribeToNotifications } from "@/Utils/Notifications";
|
||||||
|
|
||||||
import { LogoHeader } from "./LogoHeader";
|
import { LogoHeader } from "./LogoHeader";
|
||||||
|
import ProfileMenu from "./ProfileMenu";
|
||||||
|
|
||||||
const MENU_ITEMS = [
|
const MENU_ITEMS = [
|
||||||
{
|
{
|
||||||
@ -79,20 +78,17 @@ export default function NavSidebar({ narrow = false }: { narrow?: boolean }) {
|
|||||||
publicKey: s.publicKey,
|
publicKey: s.publicKey,
|
||||||
readonly: s.readonly,
|
readonly: s.readonly,
|
||||||
}));
|
}));
|
||||||
const profile = useUserProfile(publicKey);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { publisher } = useEventPublisher();
|
const { publisher } = useEventPublisher();
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
|
const pageSize = useWindowSize();
|
||||||
|
const isMobile = pageSize.width <= 768; //max-md
|
||||||
|
if (isMobile) return;
|
||||||
|
|
||||||
const className = classNames(
|
const className = classNames(
|
||||||
{ "xl:w-56 xl:gap-2 xl:items-start": !narrow },
|
{ "xl:w-56 xl:gap-2 xl:items-start": !narrow },
|
||||||
"select-none overflow-y-auto hide-scrollbar sticky items-center border-r border-border-color top-0 z-20 h-screen max-h-screen hidden md:flex flex-col px-2 py-4 flex-shrink-0 gap-1",
|
"select-none overflow-y-auto hide-scrollbar sticky items-center border-r border-border-color top-0 z-20 h-screen max-h-screen flex flex-col px-2 py-4 flex-shrink-0 gap-1",
|
||||||
);
|
|
||||||
|
|
||||||
const readOnlyIcon = readonly && (
|
|
||||||
<span style={{ transform: "rotate(135deg)" }} title={formatMessage({ defaultMessage: "Read-only", id: "djNL6D" })}>
|
|
||||||
<Icon name="openeye" className="text-nostr-red" size={20} />
|
|
||||||
</span>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -156,21 +152,7 @@ export default function NavSidebar({ narrow = false }: { narrow?: boolean }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{publicKey && (
|
<ProfileMenu />
|
||||||
<>
|
|
||||||
<ProfileLink pubkey={publicKey} user={profile} className="hover:no-underline">
|
|
||||||
<div className="mt-2 flex flex-row items-center justify-center font-bold text-md p-1 xl:px-4 xl:py-3 hover:bg-secondary rounded-full cursor-pointer">
|
|
||||||
<Avatar pubkey={publicKey} user={profile} size={40} icons={readOnlyIcon} />
|
|
||||||
{!narrow && <span className="hidden xl:inline ml-3">{profile?.name}</span>}
|
|
||||||
</div>
|
|
||||||
</ProfileLink>
|
|
||||||
{readonly && (
|
|
||||||
<div className="hidden xl:block text-nostr-red text-sm m-3">
|
|
||||||
<FormattedMessage defaultMessage="Read-only" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
90
packages/app/src/Pages/Layout/ProfileMenu.tsx
Normal file
90
packages/app/src/Pages/Layout/ProfileMenu.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import AsyncButton from "@/Components/Button/AsyncButton";
|
||||||
|
import Icon from "@/Components/Icons/Icon";
|
||||||
|
import ProfileImage from "@/Components/User/ProfileImage";
|
||||||
|
import ProfilePreview from "@/Components/User/ProfilePreview";
|
||||||
|
import useLogin from "@/Hooks/useLogin";
|
||||||
|
import { useProfileLink } from "@/Hooks/useProfileLink";
|
||||||
|
import useWindowSize from "@/Hooks/useWindowSize";
|
||||||
|
import { LoginStore } from "@/Utils/Login";
|
||||||
|
|
||||||
|
export default function ProfileMenu({ className }: { className?: string }) {
|
||||||
|
const { publicKey, readonly } = useLogin(s => ({
|
||||||
|
publicKey: s.publicKey,
|
||||||
|
readonly: s.readonly,
|
||||||
|
}));
|
||||||
|
const logins = LoginStore.getSessions();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const link = useProfileLink(publicKey);
|
||||||
|
|
||||||
|
const pageSize = useWindowSize();
|
||||||
|
const isNarrow = pageSize.width <= 1280; //xl
|
||||||
|
function profile() {
|
||||||
|
return (
|
||||||
|
<ProfilePreview
|
||||||
|
pubkey={publicKey!}
|
||||||
|
className={isNarrow ? "!justify-center" : ""}
|
||||||
|
actions={<>{!isNarrow && <Icon name="arrowFront" className="rotate-90 align-end" size={14} />}</>}
|
||||||
|
profileImageProps={{
|
||||||
|
size: 40,
|
||||||
|
link: "",
|
||||||
|
showBadges: false,
|
||||||
|
showProfileCard: false,
|
||||||
|
showFollowDistance: false,
|
||||||
|
displayNameClassName: "max-xl:hidden",
|
||||||
|
subHeader: readonly ? (
|
||||||
|
<div className="max-xl:hidden text-nostr-red text-sm">
|
||||||
|
<FormattedMessage defaultMessage="Read Only" />
|
||||||
|
</div>
|
||||||
|
) : undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!publicKey) return;
|
||||||
|
return (
|
||||||
|
<div className={classNames("w-full cursor-pointer", className)}>
|
||||||
|
<Menu menuButton={profile()} menuClassName="ctx-menu no-icons">
|
||||||
|
<div className="close-menu-container">
|
||||||
|
<MenuItem>
|
||||||
|
<div className="close-menu" />
|
||||||
|
</MenuItem>
|
||||||
|
</div>
|
||||||
|
<MenuItem onClick={() => navigate(link)}>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<Icon name="user" />
|
||||||
|
<FormattedMessage defaultMessage="Profile" />
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem className="!uppercase !text-xs !font-medium !text-gray-light">
|
||||||
|
<FormattedMessage defaultMessage="Switch accounts" />
|
||||||
|
</MenuItem>
|
||||||
|
{logins
|
||||||
|
.filter(a => a.pubkey !== publicKey)
|
||||||
|
.map(a => (
|
||||||
|
<MenuItem key={a.id}>
|
||||||
|
<ProfileImage
|
||||||
|
pubkey={a.pubkey}
|
||||||
|
link=""
|
||||||
|
size={24}
|
||||||
|
showBadges={false}
|
||||||
|
showProfileCard={false}
|
||||||
|
showFollowDistance={false}
|
||||||
|
onClick={() => LoginStore.switchAccount(a.id)}
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
<MenuItem>
|
||||||
|
<AsyncButton className="!bg-gray-light !text-white">
|
||||||
|
<FormattedMessage defaultMessage="Add Account" />
|
||||||
|
</AsyncButton>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -12,12 +12,17 @@ import TrendingHashtags from "@/Components/Trending/TrendingHashtags";
|
|||||||
import TrendingNotes from "@/Components/Trending/TrendingPosts";
|
import TrendingNotes from "@/Components/Trending/TrendingPosts";
|
||||||
import TrendingUsers from "@/Components/Trending/TrendingUsers";
|
import TrendingUsers from "@/Components/Trending/TrendingUsers";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
|
import useWindowSize from "@/Hooks/useWindowSize";
|
||||||
|
|
||||||
export default function RightColumn() {
|
export default function RightColumn() {
|
||||||
const { pubkey } = useLogin(s => ({ pubkey: s.publicKey }));
|
const { pubkey } = useLogin(s => ({ pubkey: s.publicKey }));
|
||||||
const hideRightColumnPaths = ["/login", "/new", "/messages"];
|
const hideRightColumnPaths = ["/login", "/new", "/messages"];
|
||||||
const show = !hideRightColumnPaths.some(path => globalThis.location.pathname.startsWith(path));
|
const show = !hideRightColumnPaths.some(path => globalThis.location.pathname.startsWith(path));
|
||||||
|
|
||||||
|
const pageSize = useWindowSize();
|
||||||
|
const isDesktop = pageSize.width >= 1024; //max-xl
|
||||||
|
if (!isDesktop) return;
|
||||||
|
|
||||||
const widgets = pubkey
|
const widgets = pubkey
|
||||||
? [
|
? [
|
||||||
RightColumnWidget.TaskList,
|
RightColumnWidget.TaskList,
|
||||||
|
@ -72,12 +72,32 @@ export function ZapsProfileTab({ id }: { id: HexKey }) {
|
|||||||
|
|
||||||
export function FollowersTab({ id }: { id: HexKey }) {
|
export function FollowersTab({ id }: { id: HexKey }) {
|
||||||
const followers = useFollowersFeed(id);
|
const followers = useFollowersFeed(id);
|
||||||
return <FollowsList pubkeys={followers} showAbout={true} className="p" />;
|
return (
|
||||||
|
<FollowsList
|
||||||
|
pubkeys={followers}
|
||||||
|
className="p"
|
||||||
|
profilePreviewProps={{
|
||||||
|
options: {
|
||||||
|
about: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FollowsTab({ id }: { id: HexKey }) {
|
export function FollowsTab({ id }: { id: HexKey }) {
|
||||||
const follows = useFollowsFeed(id);
|
const follows = useFollowsFeed(id);
|
||||||
return <FollowsList pubkeys={follows} showAbout={true} className="p" />;
|
return (
|
||||||
|
<FollowsList
|
||||||
|
pubkeys={follows}
|
||||||
|
className="p"
|
||||||
|
profilePreviewProps={{
|
||||||
|
options: {
|
||||||
|
about: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RelaysTab({ id }: { id: HexKey }) {
|
export function RelaysTab({ id }: { id: HexKey }) {
|
||||||
|
@ -10,20 +10,20 @@ export default function AccountsPage() {
|
|||||||
const sub = getActiveSubscriptions(LoginStore.allSubscriptions());
|
const sub = getActiveSubscriptions(LoginStore.allSubscriptions());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col g12">
|
<div className="flex flex-col gap-2">
|
||||||
<h3>
|
<h3>
|
||||||
<FormattedMessage defaultMessage="Logins" />
|
<FormattedMessage defaultMessage="Logins" />
|
||||||
</h3>
|
</h3>
|
||||||
{logins.map(a => (
|
{logins.map(a => (
|
||||||
<div className="card flex" key={a.id}>
|
<div className="" key={a.id}>
|
||||||
<ProfilePreview
|
<ProfilePreview
|
||||||
pubkey={a.pubkey}
|
pubkey={a.pubkey}
|
||||||
options={{
|
options={{
|
||||||
about: false,
|
about: false,
|
||||||
}}
|
}}
|
||||||
actions={
|
actions={
|
||||||
<div className="flex-1">
|
<div className="align-end flex gap-2">
|
||||||
<button className="mb10" onClick={() => LoginStore.switchAccount(a.id)}>
|
<button onClick={() => LoginStore.switchAccount(a.id)}>
|
||||||
<FormattedMessage defaultMessage="Switch" />
|
<FormattedMessage defaultMessage="Switch" />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => LoginStore.removeSession(a.id)}>
|
<button onClick={() => LoginStore.removeSession(a.id)}>
|
||||||
|
@ -799,7 +799,6 @@ button.tall {
|
|||||||
|
|
||||||
.ctx-menu li {
|
.ctx-menu li {
|
||||||
background: #1e1e1e !important;
|
background: #1e1e1e !important;
|
||||||
padding: 8px 24px !important;
|
|
||||||
display: grid !important;
|
display: grid !important;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@ -807,6 +806,7 @@ button.tall {
|
|||||||
|
|
||||||
.ctx-menu:not(.no-icons) li {
|
.ctx-menu:not(.no-icons) li {
|
||||||
grid-template-columns: 2rem auto !important;
|
grid-template-columns: 2rem auto !important;
|
||||||
|
padding: 8px 24px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .ctx-menu li {
|
.light .ctx-menu li {
|
||||||
|
@ -348,6 +348,9 @@
|
|||||||
"6uMqL1": {
|
"6uMqL1": {
|
||||||
"defaultMessage": "Unpaid"
|
"defaultMessage": "Unpaid"
|
||||||
},
|
},
|
||||||
|
"6xNr8c": {
|
||||||
|
"defaultMessage": "Switch accounts"
|
||||||
|
},
|
||||||
"6xap9L": {
|
"6xap9L": {
|
||||||
"defaultMessage": "Good"
|
"defaultMessage": "Good"
|
||||||
},
|
},
|
||||||
@ -1611,9 +1614,6 @@
|
|||||||
"djLctd": {
|
"djLctd": {
|
||||||
"defaultMessage": "Amount in sats"
|
"defaultMessage": "Amount in sats"
|
||||||
},
|
},
|
||||||
"djNL6D": {
|
|
||||||
"defaultMessage": "Read-only"
|
|
||||||
},
|
|
||||||
"dmcsBA": {
|
"dmcsBA": {
|
||||||
"defaultMessage": "Classified Listing"
|
"defaultMessage": "Classified Listing"
|
||||||
},
|
},
|
||||||
@ -1983,6 +1983,9 @@
|
|||||||
"mfe8RW": {
|
"mfe8RW": {
|
||||||
"defaultMessage": "Option: {n}"
|
"defaultMessage": "Option: {n}"
|
||||||
},
|
},
|
||||||
|
"mmPSWH": {
|
||||||
|
"defaultMessage": "Read Only"
|
||||||
|
},
|
||||||
"n1Whvj": {
|
"n1Whvj": {
|
||||||
"defaultMessage": "Switch"
|
"defaultMessage": "Switch"
|
||||||
},
|
},
|
||||||
|
@ -19,6 +19,7 @@ export const System = new NostrSystem({
|
|||||||
optimizer: hasWasm ? WasmOptimizer : undefined,
|
optimizer: hasWasm ? WasmOptimizer : undefined,
|
||||||
db: SystemDb,
|
db: SystemDb,
|
||||||
buildFollowGraph: true,
|
buildFollowGraph: true,
|
||||||
|
automaticOutboxModel: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
System.on("auth", async (c, r, cb) => {
|
System.on("auth", async (c, r, cb) => {
|
||||||
|
@ -115,6 +115,7 @@
|
|||||||
"6mr8WU": "Followed by",
|
"6mr8WU": "Followed by",
|
||||||
"6pdxsi": "Extra metadata fields and tags",
|
"6pdxsi": "Extra metadata fields and tags",
|
||||||
"6uMqL1": "Unpaid",
|
"6uMqL1": "Unpaid",
|
||||||
|
"6xNr8c": "Switch accounts",
|
||||||
"6xap9L": "Good",
|
"6xap9L": "Good",
|
||||||
"7+Domh": "Notes",
|
"7+Domh": "Notes",
|
||||||
"712i26": "Proxy uses HODL invoices to forward the payment, which hides the pubkey of your node",
|
"712i26": "Proxy uses HODL invoices to forward the payment, which hides the pubkey of your node",
|
||||||
@ -534,7 +535,6 @@
|
|||||||
"ddd3JX": "Popular Hashtags",
|
"ddd3JX": "Popular Hashtags",
|
||||||
"deEeEI": "Register",
|
"deEeEI": "Register",
|
||||||
"djLctd": "Amount in sats",
|
"djLctd": "Amount in sats",
|
||||||
"djNL6D": "Read-only",
|
|
||||||
"dmcsBA": "Classified Listing",
|
"dmcsBA": "Classified Listing",
|
||||||
"dmsiLv": "A default Zap Pool split of {n} has been configured for {site} developers, you can disable it at any time in {link}",
|
"dmsiLv": "A default Zap Pool split of {n} has been configured for {site} developers, you can disable it at any time in {link}",
|
||||||
"e5x8FT": "Kind",
|
"e5x8FT": "Kind",
|
||||||
@ -658,6 +658,7 @@
|
|||||||
"mKhgP9": "{n,plural,=0{} =1{zapped} other{zapped}}",
|
"mKhgP9": "{n,plural,=0{} =1{zapped} other{zapped}}",
|
||||||
"mOFG3K": "Start",
|
"mOFG3K": "Start",
|
||||||
"mfe8RW": "Option: {n}",
|
"mfe8RW": "Option: {n}",
|
||||||
|
"mmPSWH": "Read Only",
|
||||||
"n1Whvj": "Switch",
|
"n1Whvj": "Switch",
|
||||||
"n5l7tP": "Time-Based Calendar Event",
|
"n5l7tP": "Time-Based Calendar Event",
|
||||||
"n8k1SG": "{n}MiB",
|
"n8k1SG": "{n}MiB",
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
import { ID, STR, UID } from "./UniqueIds";
|
import { ID, STR, UID } from "./UniqueIds";
|
||||||
import { HexKey, NostrEvent } from "..";
|
import { HexKey, NostrEvent } from "..";
|
||||||
|
import EventEmitter from "eventemitter3";
|
||||||
|
import { unixNowMs } from "@snort/shared";
|
||||||
|
import debug from "debug";
|
||||||
|
|
||||||
export default class SocialGraph {
|
export interface SocialGraphEvents {
|
||||||
|
changeRoot: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SocialGraph extends EventEmitter<SocialGraphEvents> {
|
||||||
|
#log = debug("SocialGraph");
|
||||||
root: UID;
|
root: UID;
|
||||||
followDistanceByUser = new Map<UID, number>();
|
followDistanceByUser = new Map<UID, number>();
|
||||||
usersByFollowDistance = new Map<number, Set<UID>>();
|
usersByFollowDistance = new Map<number, Set<UID>>();
|
||||||
@ -10,6 +18,7 @@ export default class SocialGraph {
|
|||||||
latestFollowEventTimestamps = new Map<UID, number>();
|
latestFollowEventTimestamps = new Map<UID, number>();
|
||||||
|
|
||||||
constructor(root: HexKey) {
|
constructor(root: HexKey) {
|
||||||
|
super();
|
||||||
this.root = ID(root);
|
this.root = ID(root);
|
||||||
this.followDistanceByUser.set(this.root, 0);
|
this.followDistanceByUser.set(this.root, 0);
|
||||||
this.usersByFollowDistance.set(0, new Set([this.root]));
|
this.usersByFollowDistance.set(0, new Set([this.root]));
|
||||||
@ -20,6 +29,7 @@ export default class SocialGraph {
|
|||||||
if (rootId === this.root) {
|
if (rootId === this.root) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const start = unixNowMs();
|
||||||
this.root = rootId;
|
this.root = rootId;
|
||||||
this.followDistanceByUser.clear();
|
this.followDistanceByUser.clear();
|
||||||
this.usersByFollowDistance.clear();
|
this.usersByFollowDistance.clear();
|
||||||
@ -45,6 +55,8 @@ export default class SocialGraph {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.emit("changeRoot");
|
||||||
|
this.#log(`Rebuilding root took ${(unixNowMs() - start).toFixed(2)} ms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEvent(evs: NostrEvent | Array<NostrEvent>) {
|
handleEvent(evs: NostrEvent | Array<NostrEvent>) {
|
||||||
|
@ -130,7 +130,7 @@ export class DefaultConnectionPool<T extends ConnectionType = Connection>
|
|||||||
this.#connectionBuilder = builder;
|
this.#connectionBuilder = builder;
|
||||||
} else {
|
} else {
|
||||||
this.#connectionBuilder = (addr, options, ephemeral) => {
|
this.#connectionBuilder = (addr, options, ephemeral) => {
|
||||||
const sync = new DefaultSyncModule(this.#system.config.fallbackSync);
|
const sync = new DefaultSyncModule(this.#system.config.fallbackSync, this.#system);
|
||||||
return new Connection(addr, options, ephemeral, sync) as unknown as T;
|
return new Connection(addr, options, ephemeral, sync) as unknown as T;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -240,7 +240,6 @@ export class Connection extends EventEmitter<ConnectionTypeEvents> implements Co
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.emit("event", msg[1] as string, ev);
|
this.emit("event", msg[1] as string, ev);
|
||||||
// todo: stats events received
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "EOSE": {
|
case "EOSE": {
|
||||||
|
@ -24,7 +24,7 @@ export interface Thread {
|
|||||||
export const enum EventType {
|
export const enum EventType {
|
||||||
Regular,
|
Regular,
|
||||||
Replaceable,
|
Replaceable,
|
||||||
ParameterizedReplaceable,
|
Addressable,
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class EventExt {
|
export abstract class EventExt {
|
||||||
@ -164,7 +164,7 @@ export abstract class EventExt {
|
|||||||
static getType(kind: number) {
|
static getType(kind: number) {
|
||||||
const legacyReplaceable = [0, 3, 41];
|
const legacyReplaceable = [0, 3, 41];
|
||||||
if (kind >= 30_000 && kind < 40_000) {
|
if (kind >= 30_000 && kind < 40_000) {
|
||||||
return EventType.ParameterizedReplaceable;
|
return EventType.Addressable;
|
||||||
} else if (kind >= 10_000 && kind < 20_000) {
|
} else if (kind >= 10_000 && kind < 20_000) {
|
||||||
return EventType.Replaceable;
|
return EventType.Replaceable;
|
||||||
} else if (legacyReplaceable.includes(kind)) {
|
} else if (legacyReplaceable.includes(kind)) {
|
||||||
@ -174,9 +174,14 @@ export abstract class EventExt {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static isReplaceable(kind: number) {
|
||||||
|
const t = EventExt.getType(kind);
|
||||||
|
return t === EventType.Replaceable || t === EventType.Addressable;
|
||||||
|
}
|
||||||
|
|
||||||
static isValid(ev: NostrEvent) {
|
static isValid(ev: NostrEvent) {
|
||||||
const type = EventExt.getType(ev.kind);
|
const type = EventExt.getType(ev.kind);
|
||||||
if (type === EventType.ParameterizedReplaceable) {
|
if (type === EventType.Addressable) {
|
||||||
if (!findTag(ev, "d")) return false;
|
if (!findTag(ev, "d")) return false;
|
||||||
}
|
}
|
||||||
return ev.sig !== undefined;
|
return ev.sig !== undefined;
|
||||||
|
@ -100,7 +100,7 @@ export class NoteCollection extends KeyedReplaceableNoteStore {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super(e => {
|
super(e => {
|
||||||
switch (EventExt.getType(e.kind)) {
|
switch (EventExt.getType(e.kind)) {
|
||||||
case EventType.ParameterizedReplaceable:
|
case EventType.Addressable:
|
||||||
return `${e.kind}:${e.pubkey}:${findTag(e, "d")}`;
|
return `${e.kind}:${e.pubkey}:${findTag(e, "d")}`;
|
||||||
case EventType.Replaceable:
|
case EventType.Replaceable:
|
||||||
return `${e.kind}:${e.pubkey}`;
|
return `${e.kind}:${e.pubkey}`;
|
||||||
|
@ -29,6 +29,7 @@ export interface Optimizer {
|
|||||||
flatMerge(all: Array<FlatReqFilter>): Array<ReqFilter>;
|
flatMerge(all: Array<FlatReqFilter>): Array<ReqFilter>;
|
||||||
compress(all: Array<ReqFilter>): Array<ReqFilter>;
|
compress(all: Array<ReqFilter>): Array<ReqFilter>;
|
||||||
schnorrVerify(ev: NostrEvent): boolean;
|
schnorrVerify(ev: NostrEvent): boolean;
|
||||||
|
batchVerify(evs: Array<NostrEvent>): Array<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DefaultOptimizer = {
|
export const DefaultOptimizer = {
|
||||||
|
@ -3,14 +3,18 @@ import { EventExt, EventType } from "../event-ext";
|
|||||||
import { NoteCollection } from "../note-collection";
|
import { NoteCollection } from "../note-collection";
|
||||||
import { RangeSync } from "./range-sync";
|
import { RangeSync } from "./range-sync";
|
||||||
import { NegentropyFlow } from "../negentropy/negentropy-flow";
|
import { NegentropyFlow } from "../negentropy/negentropy-flow";
|
||||||
import { SystemConfig } from "../system";
|
import { SystemConfig, SystemInterface } from "../system";
|
||||||
|
import { findTag } from "../utils";
|
||||||
|
|
||||||
export interface ConnectionSyncModule {
|
export interface ConnectionSyncModule {
|
||||||
sync: (c: Connection, item: SyncCommand, cb?: () => void) => void;
|
sync: (c: Connection, item: SyncCommand, cb?: () => void) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DefaultSyncModule implements ConnectionSyncModule {
|
export class DefaultSyncModule implements ConnectionSyncModule {
|
||||||
constructor(readonly method: SystemConfig["fallbackSync"]) {}
|
constructor(
|
||||||
|
readonly method: SystemConfig["fallbackSync"],
|
||||||
|
readonly system: SystemInterface,
|
||||||
|
) {}
|
||||||
|
|
||||||
sync(c: Connection, item: SyncCommand, cb?: () => void) {
|
sync(c: Connection, item: SyncCommand, cb?: () => void) {
|
||||||
const [_, id, eventSet, ...filters] = item;
|
const [_, id, eventSet, ...filters] = item;
|
||||||
@ -58,6 +62,36 @@ export class DefaultSyncModule implements ConnectionSyncModule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split a set of filters down into individual filters
|
||||||
|
* which can be used to since request updates to replaceable events
|
||||||
|
*/
|
||||||
|
#breakdownReplaceable(item: SyncCommand) {
|
||||||
|
const [type, id, eventSet, ...filters] = item;
|
||||||
|
|
||||||
|
const flat = filters.flatMap(a => this.system.optimizer.expandFilter(a));
|
||||||
|
const mapped = flat.map(a => {
|
||||||
|
if (!a.kinds || !a.authors) return a;
|
||||||
|
if (EventExt.isReplaceable(a.kinds)) {
|
||||||
|
const latest = eventSet.find(
|
||||||
|
b => b.kind === a.kinds && b.pubkey === a.authors && (!a["#d"] || findTag(b, "d") === a["#d"]),
|
||||||
|
);
|
||||||
|
if (latest) {
|
||||||
|
return {
|
||||||
|
...a,
|
||||||
|
since: latest.created_at + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
});
|
||||||
|
const compressed = this.system.optimizer.flatMerge(mapped);
|
||||||
|
if (compressed.length !== filters.length) {
|
||||||
|
console.debug("COMPRESSED", id, filters, compressed);
|
||||||
|
}
|
||||||
|
return compressed;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Using the latest data, fetch only newer items
|
* Using the latest data, fetch only newer items
|
||||||
*
|
*
|
||||||
@ -66,11 +100,17 @@ export class DefaultSyncModule implements ConnectionSyncModule {
|
|||||||
#syncSince(c: Connection, item: SyncCommand, cb?: () => void) {
|
#syncSince(c: Connection, item: SyncCommand, cb?: () => void) {
|
||||||
const [type, id, eventSet, ...filters] = item;
|
const [type, id, eventSet, ...filters] = item;
|
||||||
if (type !== "SYNC") throw new Error("Must be a SYNC command");
|
if (type !== "SYNC") throw new Error("Must be a SYNC command");
|
||||||
const latest = eventSet.reduce((acc, v) => (acc = v.created_at > acc ? v.created_at : acc), 0);
|
//const broken = this.#breakdownReplaceable(item);
|
||||||
const newFilters = filters.map(a => ({
|
const latest = eventSet
|
||||||
...a,
|
//.filter(a => !EventExt.isReplaceable(a.kind))
|
||||||
since: latest + 1,
|
.reduce((acc, v) => (acc = v.created_at > acc ? v.created_at : acc), 0);
|
||||||
}));
|
const newFilters = filters.map(a => {
|
||||||
|
if (a.since || latest === 0) return a;
|
||||||
|
return {
|
||||||
|
...a,
|
||||||
|
since: latest + 1,
|
||||||
|
};
|
||||||
|
});
|
||||||
c.request(["REQ", id, ...newFilters], cb);
|
c.request(["REQ", id, ...newFilters], cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,10 +129,7 @@ export class SafeSync extends EventEmitter<SafeSyncEvents> {
|
|||||||
if (prevTag && prevTag[1] !== this.#base.id) {
|
if (prevTag && prevTag[1] !== this.#base.id) {
|
||||||
throw new Error("Previous tag does not match our version");
|
throw new Error("Previous tag does not match our version");
|
||||||
}
|
}
|
||||||
if (
|
if (EventExt.getType(ev.kind) !== EventType.Replaceable && EventExt.getType(ev.kind) !== EventType.Addressable) {
|
||||||
EventExt.getType(ev.kind) !== EventType.Replaceable &&
|
|
||||||
EventExt.getType(ev.kind) !== EventType.ParameterizedReplaceable
|
|
||||||
) {
|
|
||||||
throw new Error("Not a replacable event kind");
|
throw new Error("Not a replacable event kind");
|
||||||
}
|
}
|
||||||
if (this.#base.created_at >= ev.created_at) {
|
if (this.#base.created_at >= ev.created_at) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user