Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
16 changes: 13 additions & 3 deletions __tests__/components/brain/NotificationsWrapper.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,25 @@ describe('NotificationsWrapper', () => {
it('shows loading spinner and handles actions', () => {
const setActive = jest.fn();
render(
<NotificationsWrapper items={[]} loading={true} activeDrop={null} setActiveDrop={setActive} />
<NotificationsWrapper
items={[]}
loadingOlder={true}
activeDrop={null}
setActiveDrop={setActive}
/>
);
expect(screen.getByText(/Loading notifications/, { selector: 'div' })).toBeInTheDocument();
expect(screen.getByText(/Loading older notifications/, { selector: 'div' })).toBeInTheDocument();
});

it('delegates callbacks to router and state setter', () => {
const setActive = jest.fn();
render(
<NotificationsWrapper items={[]} loading={false} activeDrop={null} setActiveDrop={setActive} />
<NotificationsWrapper
items={[]}
loadingOlder={false}
activeDrop={null}
setActiveDrop={setActive}
/>
);
screen.getByTestId('items').click();
expect(setActive).toHaveBeenCalledTimes(2);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { render } from '@testing-library/react';
const NotificationItem = jest.fn(() => <div data-testid="item" />);
const CommonChangeAnimation = jest.fn(({ children }) => <div data-testid="anim">{children}</div>);

jest.mock('@/components/brain/notifications/NotificationItem', () => ({ __esModule: true, default: NotificationItem }));
jest.mock('@/components/utils/animation/CommonChangeAnimation', () => ({ __esModule: true, default: CommonChangeAnimation }));

import NotificationItems from '@/components/brain/notifications/NotificationItems';
import React from 'react';
Expand All @@ -26,7 +24,6 @@ describe('NotificationItems', () => {
/>
);

expect(CommonChangeAnimation).toHaveBeenCalledTimes(2);
expect(NotificationItem).toHaveBeenCalledTimes(2);
expect(NotificationItem.mock.calls[0][0]).toEqual(
expect.objectContaining({
Expand Down
12 changes: 3 additions & 9 deletions __tests__/components/brain/notifications/Notifications.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,6 @@ jest.mock('@/components/brain/notifications/NotificationsCauseFilter', () => ({
default: () => <div data-testid="filter" />,
}));

jest.mock('@/components/brain/feed/FeedScrollContainer', () => ({
FeedScrollContainer: React.forwardRef((props: any, ref) => (
<div data-testid="scroll" ref={ref} {...props} />
)),
}));

jest.mock('@/components/brain/content/input/BrainContentInput', () => ({
__esModule: true,
default: () => <div data-testid="input" />,
Expand Down Expand Up @@ -109,7 +103,7 @@ describe('Notifications component', () => {
isInitialQueryDone: false,
});

render(<Notifications />);
render(<Notifications activeDrop={null} setActiveDrop={jest.fn()} />);

expect(screen.getByText('Loading notifications...', { selector: 'div' })).toBeInTheDocument();
expect(mutateAsyncMock).toHaveBeenCalled();
Expand All @@ -127,7 +121,7 @@ describe('Notifications component', () => {
isInitialQueryDone: true,
});

render(<Notifications />);
render(<Notifications activeDrop={null} setActiveDrop={jest.fn()} />);

expect(screen.getByTestId('wrapper')).toBeInTheDocument();
});
Expand All @@ -143,7 +137,7 @@ describe('Notifications component', () => {
isInitialQueryDone: true,
});

render(<Notifications />);
render(<Notifications activeDrop={null} setActiveDrop={jest.fn()} />);

expect(screen.getByTestId('no-items')).toBeInTheDocument();
});
Expand Down
38 changes: 24 additions & 14 deletions components/brain/feed/FeedScrollContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface FeedScrollContainerProps {
}

const MIN_OUT_OF_VIEW_COUNT = 30;
const TOP_SCROLL_THRESHOLD_PX = 200;

export const FeedScrollContainer = forwardRef<
HTMLDivElement,
Expand Down Expand Up @@ -80,31 +81,40 @@ export const FeedScrollContainer = forwardRef<

const currentTarget = event.currentTarget;
const currentScrollTop = currentTarget.scrollTop;
const isNearTop = currentScrollTop <= TOP_SCROLL_THRESHOLD_PX;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

throttleTimeoutRef.current = setTimeout(() => {
const direction = currentScrollTop > lastScrollTop ? "down" : "up";
setLastScrollTop(currentScrollTop);

if (direction === "up" && onScrollUpNearTop) {
const dropElements =
contentRef.current?.querySelectorAll("[id^='feed-item-']");
if (!dropElements) {
if (onScrollUpNearTop) {
if (isNearTop) {
onScrollUpNearTop();
throttleTimeoutRef.current = null;
return;
}

const containerRect = currentTarget.getBoundingClientRect();
let outOfViewCount = 0;

dropElements.forEach((el) => {
const rect = el.getBoundingClientRect();
if (rect.bottom < containerRect.top) {
outOfViewCount++;
if (direction === "up") {
const dropElements =
contentRef.current?.querySelectorAll("[id^='feed-item-']");
if (!dropElements) {
throttleTimeoutRef.current = null;
return;
}
});

if (outOfViewCount <= MIN_OUT_OF_VIEW_COUNT) {
onScrollUpNearTop();
const containerRect = currentTarget.getBoundingClientRect();
let outOfViewCount = 0;

dropElements.forEach((el) => {
const rect = el.getBoundingClientRect();
if (rect.bottom < containerRect.top) {
outOfViewCount++;
}
});

if (outOfViewCount <= MIN_OUT_OF_VIEW_COUNT) {
onScrollUpNearTop();
}
}
}

Expand Down
20 changes: 19 additions & 1 deletion components/brain/notifications/NotificationItem.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { memo } from "react";
import { ApiNotificationCause } from "@/generated/models/ApiNotificationCause";
import { assertUnreachable } from "@/helpers/AllowlistToolHelpers";
import { ExtendedDrop } from "@/helpers/waves/drop.helpers";
Expand All @@ -14,7 +15,7 @@ import NotificationAllDrops from "./all-drops/NotificationAllDrops";
import type { JSX } from "react";
import NotificationDropReacted from "./drop-reacted/NotificationDropReacted";

export default function NotificationItem({
function NotificationItemComponent({
notification,
activeDrop,
onReply,
Expand Down Expand Up @@ -99,3 +100,20 @@ export default function NotificationItem({
</div>
);
}

const NotificationItem = memo(
NotificationItemComponent,
(prevProps, nextProps) => {
return (
prevProps.notification === nextProps.notification &&
prevProps.activeDrop === nextProps.activeDrop &&
prevProps.onReply === nextProps.onReply &&
prevProps.onQuote === nextProps.onQuote &&
prevProps.onDropContentClick === nextProps.onDropContentClick
);
}
);

NotificationItem.displayName = "NotificationItem";

export default NotificationItem;
52 changes: 37 additions & 15 deletions components/brain/notifications/NotificationItems.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { memo, useMemo } from "react";
import { TypedNotification } from "@/types/feed.types";
import NotificationItem from "./NotificationItem";
import { ActiveDropState } from "@/types/dropInteractionTypes";
import { DropInteractionParams } from "@/components/waves/drops/Drop";
import { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import CommonChangeAnimation from "@/components/utils/animation/CommonChangeAnimation";

interface NotificationItemsProps {
readonly items: TypedNotification[];
Expand All @@ -13,30 +13,52 @@ interface NotificationItemsProps {
readonly onDropContentClick?: (drop: ExtendedDrop) => void;
}

export default function NotificationItems({
function NotificationItemsComponent({
items,
activeDrop,
onReply,
onQuote,
onDropContentClick,
}: NotificationItemsProps) {
const keyedNotifications = useMemo(
() =>
items.map((notification, index) => ({
notification,
key: `notification-${notification.id}-${index}`,
})),
[items]
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return (
<div className="tw-flex tw-flex-col tw-space-y-3 tw-pb-3 lg:tw-pr-2">
{items.map((notification, i) => (
<div
key={`notification-${notification.id}-${i}`}
id={`feed-item-${notification.id}`}>
<CommonChangeAnimation>
<NotificationItem
notification={notification}
activeDrop={activeDrop}
onReply={onReply}
onQuote={onQuote}
onDropContentClick={onDropContentClick}
/>
</CommonChangeAnimation>
{keyedNotifications.map(({ notification, key }) => (
<div key={key} id={`feed-item-${notification.id}`}>
<NotificationItem
Comment thread
coderabbitai[bot] marked this conversation as resolved.
notification={notification}
activeDrop={activeDrop}
onReply={onReply}
onQuote={onQuote}
onDropContentClick={onDropContentClick}
/>
</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
))}
</div>
);
}

const NotificationItems = memo(
NotificationItemsComponent,
(prevProps, nextProps) => {
return (
prevProps.items === nextProps.items &&
prevProps.activeDrop === nextProps.activeDrop &&
prevProps.onReply === nextProps.onReply &&
prevProps.onQuote === nextProps.onQuote &&
prevProps.onDropContentClick === nextProps.onDropContentClick
);
}
);

NotificationItems.displayName = "NotificationItems";

export default NotificationItems;
Loading