Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ApiNotificationCause } from '@/generated/models/ApiNotificationCause';
jest.mock('@/components/brain/notifications/drop-quoted/NotificationDropQuoted', () => ({ __esModule: true, default: () => <div data-testid="quoted" /> }));
jest.mock('@/components/brain/notifications/drop-replied/NotificationDropReplied', () => ({ __esModule: true, default: () => <div data-testid="replied" /> }));
jest.mock('@/components/brain/notifications/priority-alert/NotificationPriorityAlert', () => ({ __esModule: true, default: () => <div data-testid="priority-alert" /> }));
jest.mock('@/components/brain/notifications/identity-rating/NotificationIdentityRating', () => ({ __esModule: true, default: () => <div data-testid="identity-rating" /> }));

describe('NotificationItem', () => {
const base = { id: '1' } as any;
Expand All @@ -23,4 +24,14 @@ describe('NotificationItem', () => {
render(<NotificationItem notification={{ ...base, cause: ApiNotificationCause.PriorityAlert }} activeDrop={null} onReply={jest.fn()} onQuote={jest.fn()} />);
expect(screen.getByTestId('priority-alert')).toBeInTheDocument();
});

it('renders identity rating component for IdentityRep', () => {
render(<NotificationItem notification={{ ...base, cause: ApiNotificationCause.IdentityRep }} activeDrop={null} onReply={jest.fn()} onQuote={jest.fn()} />);
expect(screen.getByTestId('identity-rating')).toBeInTheDocument();
});

it('renders identity rating component for IdentityNic', () => {
render(<NotificationItem notification={{ ...base, cause: ApiNotificationCause.IdentityNic }} activeDrop={null} onReply={jest.fn()} onQuote={jest.fn()} />);
expect(screen.getByTestId('identity-rating')).toBeInTheDocument();
});
});
16 changes: 14 additions & 2 deletions components/brain/notifications/NotificationItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import type { DropInteractionParams } from "@/components/waves/drops/Drop";
import { ApiNotificationCause } from "@/generated/models/ApiNotificationCause";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import type { ActiveDropState } from "@/types/dropInteractionTypes";
import type { TypedNotification } from "@/types/feed.types";
import type {
INotificationGeneric,
TypedNotification,
} from "@/types/feed.types";
import { memo } from "react";
import NotificationAllDrops from "./all-drops/NotificationAllDrops";
import NotificationDropQuoted from "./drop-quoted/NotificationDropQuoted";
import NotificationDropReplied from "./drop-replied/NotificationDropReplied";
import NotificationGeneric from "./generic/NotificationGeneric";
import NotificationIdentityMentioned from "./identity-mentioned/NotificationIdentityMentioned";
import NotificationIdentityRating from "./identity-rating/NotificationIdentityRating";
import NotificationIdentitySubscribed from "./identity-subscribed/NotificationIdentitySubscribed";
import NotificationPriorityAlert from "./priority-alert/NotificationPriorityAlert";
import NotificationWaveCreated from "./wave-created/NotificationWaveCreated";
Expand Down Expand Up @@ -74,6 +79,9 @@ function NotificationItemComponent({
);
case ApiNotificationCause.IdentitySubscribed:
return <NotificationIdentitySubscribed notification={notification} />;
case ApiNotificationCause.IdentityRep:
case ApiNotificationCause.IdentityNic:
return <NotificationIdentityRating notification={notification} />;
case ApiNotificationCause.WaveCreated:
return <NotificationWaveCreated notification={notification} />;
case ApiNotificationCause.AllDrops:
Expand All @@ -97,7 +105,11 @@ function NotificationItemComponent({
/>
);
default:
return <div />;
return (
<NotificationGeneric
notification={notification as unknown as INotificationGeneric}
/>
);
}
};

Expand Down
2 changes: 1 addition & 1 deletion components/brain/notifications/NotificationItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function NotificationItemsComponent({
);

return (
<div className="tw-flex tw-flex-col tw-space-y-3 tw-pb-3 lg:tw-pr-2">
<div className="tw-flex tw-flex-col tw-space-y-3 tw-pb-3">
{keyedNotifications.map(({ notification, key, domId }) => (
<div key={key} id={domId}>
<NotificationItem
Expand Down
107 changes: 102 additions & 5 deletions components/brain/notifications/NotificationsCauseFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,19 @@
import { AuthContext } from "@/components/auth/Auth";
import { ApiNotificationCause } from "@/generated/models/ApiNotificationCause";
import { usePrefetchNotifications } from "@/hooks/useNotificationsQuery";
import { useContext, useLayoutEffect, useRef } from "react";
import {
faChevronLeft,
faChevronRight,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
useCallback,
useContext,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";

export interface NotificationFilter {
cause: ApiNotificationCause[];
Expand All @@ -20,7 +32,14 @@ const NotificationFilters: NotificationFilter[] = [
title: "Mentions",
},
{ cause: [ApiNotificationCause.DropReplied], title: "Replies" },
{ cause: [ApiNotificationCause.IdentitySubscribed], title: "Follows" },
{
cause: [
ApiNotificationCause.IdentitySubscribed,
ApiNotificationCause.IdentityRep,
ApiNotificationCause.IdentityNic,
],
title: "Identity",
},
{
cause: [
ApiNotificationCause.DropVoted,
Expand All @@ -43,15 +62,61 @@ export default function NotificationsCauseFilter({
const buttonRefs = useRef<HTMLButtonElement[]>([]);
const highlightRef = useRef<HTMLDivElement>(null);
const activeIndexRef = useRef<number>(0);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(false);

const { connectedProfile } = useContext(AuthContext);
const prefetchNotifications = usePrefetchNotifications();

const checkScroll = useCallback(() => {
const container = containerRef.current;
if (!container) return;

const { scrollLeft, scrollWidth, clientWidth } = container;
setCanScrollLeft(scrollLeft > 0);
setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 1);
}, []);

useEffect(() => {
const container = containerRef.current;
if (!container) return;

checkScroll();
container.addEventListener("scroll", checkScroll);
window.addEventListener("resize", checkScroll);

let resizeObserver: ResizeObserver | null = null;
if (typeof ResizeObserver !== "undefined") {
resizeObserver = new ResizeObserver(() => {
checkScroll();
});
resizeObserver.observe(container);
}

return () => {
container.removeEventListener("scroll", checkScroll);
window.removeEventListener("resize", checkScroll);
resizeObserver?.disconnect();
};
}, [checkScroll]);

const scrollLeft = () => {
const container = containerRef.current;
if (!container) return;
container.scrollBy({ left: -150, behavior: "smooth" });
};

const scrollRight = () => {
const container = containerRef.current;
if (!container) return;
container.scrollBy({ left: 150, behavior: "smooth" });
};

const handleHover = (filter: NotificationFilter) => {
if (!connectedProfile) return;
prefetchNotifications({
identity: connectedProfile.handle,
cause: filter.cause,
cause: filter.cause.length > 0 ? filter.cause : null,
pages: 1,
});
};
Expand Down Expand Up @@ -130,10 +195,10 @@ export default function NotificationsCauseFilter({
const isActive = (filter: NotificationFilter) => activeFilter === filter;

return (
<div className="tw-w-full tw-pb-2 tw-pt-2 lg:tw-pt-4">
<div className="tw-relative tw-w-full tw-pb-2 tw-pt-2 lg:tw-pt-4">
<div
ref={containerRef}
className="tw-nowrap tw-relative tw-flex tw-h-10 tw-items-center tw-gap-1 tw-overflow-x-auto tw-rounded-lg tw-border tw-border-solid tw-border-iron-800 tw-bg-iron-950"
className="tw-nowrap tw-relative tw-flex tw-h-10 tw-items-center tw-gap-1 tw-overflow-x-auto tw-rounded-lg tw-border tw-border-solid tw-border-iron-800 tw-bg-iron-950 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:tw-hidden"
>
<div
ref={highlightRef}
Expand All @@ -150,6 +215,38 @@ export default function NotificationsCauseFilter({
/>
))}
</div>
{canScrollLeft && (
<>
<div className="tw-pointer-events-none tw-absolute tw-bottom-2 tw-left-0 tw-top-2 tw-z-10 tw-w-16 tw-rounded-l-lg tw-bg-gradient-to-r tw-from-iron-950 tw-via-iron-950/40 tw-to-iron-950/0 lg:tw-top-4" />
<button
type="button"
onClick={scrollLeft}
aria-label="Scroll filters left"
className="tw-group tw-absolute tw-left-0 tw-top-1/2 tw-z-20 tw-inline-flex tw-h-10 tw-w-10 tw--translate-y-1/2 tw-items-center tw-justify-start tw-border-none tw-bg-transparent tw-p-0 tw-outline-none"
>
<FontAwesomeIcon
icon={faChevronLeft}
className="tw-ml-1 tw-h-4 tw-w-4 tw-text-iron-400 tw-transition tw-duration-300 tw-ease-out group-hover:tw-text-iron-300"
/>
</button>
</>
)}
{canScrollRight && (
<>
<div className="tw-pointer-events-none tw-absolute tw-bottom-2 tw-right-0 tw-top-2 tw-z-10 tw-w-16 tw-rounded-r-lg tw-bg-gradient-to-l tw-from-iron-950 tw-via-iron-950/40 tw-to-iron-950/0 lg:tw-top-4" />
<button
type="button"
onClick={scrollRight}
aria-label="Scroll filters right"
className="tw-group tw-absolute tw-right-0 tw-top-1/2 tw-z-20 tw-inline-flex tw-h-10 tw-w-10 tw--translate-y-1/2 tw-items-center tw-justify-end tw-border-none tw-bg-transparent tw-p-0 tw-outline-none"
>
<FontAwesomeIcon
icon={faChevronRight}
className="tw-mr-1 tw-h-4 tw-w-4 tw-text-iron-400 tw-transition tw-duration-300 tw-ease-out group-hover:tw-text-iron-300"
/>
</button>
</>
)}
Comment thread
prxt6529 marked this conversation as resolved.
</div>
);
}
Expand Down
102 changes: 102 additions & 0 deletions components/brain/notifications/generic/NotificationGeneric.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { INotificationGeneric } from "@/types/feed.types";
import NotificationHeader from "../subcomponents/NotificationHeader";
import NotificationTimestamp from "../subcomponents/NotificationTimestamp";

function formatCause(cause: string): string {
return cause
.replaceAll("_", " ")
.toLowerCase()
.replaceAll(/\b\w/g, (c) => c.toUpperCase());
}

function formatContextValue(value: unknown): string | null {
if (value === null || value === undefined) return null;
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
return null;
}

function ContextDetails({
context,
}: {
readonly context: Record<string, unknown> | undefined;
}) {
if (!context || Object.keys(context).length === 0) return null;

const displayableEntries = Object.entries(context)
.map(([key, value]) => [key, formatContextValue(value)] as const)
.filter((entry): entry is [string, string] => entry[1] !== null);

if (displayableEntries.length === 0) return null;

return (
<>
<span className="tw-mr-1 tw-text-xs tw-font-bold tw-text-iron-400">
&#8226;
</span>
<div className="tw-flex tw-flex-wrap tw-gap-x-3 tw-gap-y-1">
{displayableEntries.map(([key, value]) => (
<span key={key} className="tw-text-xs tw-text-iron-500">
<span className="tw-text-iron-600">{key}:</span> {value}
</span>
))}
</div>
</>
);
}

function NotificationContent({
causeLabel,
createdAt,
context,
}: {
readonly causeLabel: string;
readonly createdAt: number;
readonly context: Record<string, unknown> | undefined;
}) {
return (
<>
<span className="tw-text-sm tw-font-normal tw-text-iron-400">
{causeLabel}
</span>
<NotificationTimestamp createdAt={createdAt} />
<ContextDetails context={context} />
</>
);
}

export default function NotificationGeneric({
notification,
}: {
readonly notification: INotificationGeneric;
}) {
const causeLabel = formatCause(notification.cause);

if (notification.related_identity) {
return (
<div className="tw-w-full">
<NotificationHeader author={notification.related_identity}>
<NotificationContent
causeLabel={causeLabel}
createdAt={notification.created_at}
context={notification.additional_context}
/>
</NotificationHeader>
</div>
);
}

return (
<div className="tw-w-full tw-py-2">
<div className="tw-flex tw-flex-wrap tw-items-center tw-gap-x-2">
<span className="tw-text-sm tw-font-medium tw-text-iron-300">
{causeLabel}
</span>
<NotificationTimestamp createdAt={notification.created_at} />
</div>
<ContextDetails context={notification.additional_context} />
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export const useNotificationsController =
activeProfileProxy: !!activeProfileProxy,
limit: "30",
reverse: true,
cause: activeFilter?.cause,
cause: activeFilter?.cause?.length ? activeFilter.cause : null,
});

useEffect(() => {
Expand Down
Loading