Skip to content

Commit

Permalink
Merge branch 'preview'
Browse files Browse the repository at this point in the history
  • Loading branch information
pdelfan committed Feb 12, 2024
2 parents 46e62b1 + f2e5fb4 commit eaeca99
Show file tree
Hide file tree
Showing 11 changed files with 212 additions and 105 deletions.
1 change: 0 additions & 1 deletion src/app/dashboard/feeds/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import SavedFeedListSkeleton from "@/components/contentDisplay/savedFeedList/Sav
import Search from "@/components/filter/search/Search";
import Link from "next/link";
import { Suspense } from "react";
import { BiCog } from "react-icons/bi";
import { FaSlidersH } from "react-icons/fa";

interface Props {
Expand Down
4 changes: 3 additions & 1 deletion src/app/dashboard/notifications/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import NotificationSkeleton from "@/components/contentDisplay/notification/NotificationSkeleton";
import { TabsSkeleton } from "@/components/contentDisplay/profileHeader/ProfileHeaderSkeleton";

export default function Loading() {
return (
<section className="flex flex-col gap-5">
<section>
<h2 className="text-2xl font-semibold mx-3 md:mx-0 mb-2">
<h2 className="mx-3 mb-2 text-2xl font-semibold md:mx-0">
Notifications
</h2>
<TabsSkeleton />
<NotificationSkeleton />
</section>
</section>
Expand Down
30 changes: 8 additions & 22 deletions src/components/contentDisplay/notification/NotificationItem.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { getNotificationLabel } from "@/lib/utils/text";
import Avatar from "@/components/dataDisplay/avatar/Avatar";
import { BiSolidHeart } from "react-icons/bi";
import { BiRepost } from "react-icons/bi";
import { BiSolidUserPlus } from "react-icons/bi";
import { getRelativeTime } from "@/lib/utils/time";
import { GROUPABLE_NOTIFICATIONS } from "@/lib/consts/notification";
import { getNotificationIcon } from "@/lib/utils/icon";
import {
GROUPABLE_NOTIFICATIONS,
MAX_AUTHORS_SHOWN,
} from "@/lib/consts/notification";
import Avatar from "@/components/dataDisplay/avatar/Avatar";
import Link from "next/link";
import {
ContentFilterResult,
Expand All @@ -26,25 +27,10 @@ const NotificationItem = memo(function NotificationItem(props: Props) {
const subjectUri =
notification.reasonSubject as AppBskyNotificationListNotifications.Notification["reasonSubject"];

const MAX_AUTHORS_SHOWN = 6;

const getNotificationIcon = (reason: string) => {
switch (reason) {
case "like":
return <BiSolidHeart className="shrink-0 text-2xl text-red-500" />;
case "repost":
return <BiRepost className="shrink-0 text-2xl text-green-600" />;
case "follow":
return <BiSolidUserPlus className="text-primary shrink-0 text-2xl" />;
default:
return null;
}
};

if (GROUPABLE_NOTIFICATIONS.includes(reason)) {
return (
<article
className={`flex flex-col justify-between border border-x-0 p-3 first:border-t last:border-b md:border-x md:first:rounded-t-2xl odd:[&:not(:last-child)]:border-b-0 even:[&:not(:last-child)]:border-b-0 ${
className={`flex flex-col justify-between border border-x-0 p-3 first:border-t last:border-b md:border-x odd:[&:not(:last-child)]:border-b-0 even:[&:not(:last-child)]:border-b-0 ${
!isRead && "bg-neutral-100"
}`}
>
Expand Down Expand Up @@ -114,7 +100,7 @@ const NotificationItem = memo(function NotificationItem(props: Props) {
} else {
return (
<div
className={`flex flex-col justify-between border border-x-0 p-3 first:border-t last:border-b md:border-x first:md:rounded-t-2xl odd:[&:not(:last-child)]:border-b-0 even:[&:not(:last-child)]:border-b-0 ${
className={`flex flex-col justify-between border border-x-0 p-3 first:border-t last:border-b md:border-x odd:[&:not(:last-child)]:border-b-0 even:[&:not(:last-child)]:border-b-0 ${
!isRead && "bg-neutral-100"
}`}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export function Skeleton() {
return (
<article className="flex flex-col justify-between border border-x-0 p-3 first:border-t last:border-b md:border-x md:first:rounded-t-2xl odd:[&:not(:last-child)]:border-b-0 even:[&:not(:last-child)]:border-b-0">
<article className="flex flex-col justify-between border border-x-0 p-3 first:border-t last:border-b md:border-x odd:[&:not(:last-child)]:border-b-0 even:[&:not(:last-child)]:border-b-0">
<div className="flex flex-wrap gap-2">
<div className="h-5 w-5 rounded bg-neutral-200" />
<div className="flex grow flex-col gap-1">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function Skeleton() {
);
}

function TabsSkeleton() {
export function TabsSkeleton() {
return (
<div className="no-scrollbar mt-5 flex flex-nowrap gap-6 overflow-auto px-3">
<Skeleton />
Expand Down
124 changes: 124 additions & 0 deletions src/containers/notifications/FilteredNotificationsContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"use client";

import NotificationItem from "@/components/contentDisplay/notification/NotificationItem";
import NotificationSkeleton from "@/components/contentDisplay/notification/NotificationSkeleton";
import EndOfFeed from "@/components/feedback/endOfFeed/EndOfFeed";
import FeedAlert from "@/components/feedback/feedAlert/FeedAlert";
import LoadingSpinner from "@/components/status/loadingSpinner/LoadingSpinner";
import usePreferences from "@/lib/hooks/bsky/actor/usePreferences";
import useNotification from "@/lib/hooks/bsky/notification/useNotification";
import { Fragment, useEffect, useState } from "react";
import InfiniteScroll from "react-infinite-scroll-component";

interface Props {
filter: NotificationReason | "all";
}

export default function FilteredNotificationsContainer(props: Props) {
const { filter } = props;
const [isFetchingMore, setIsFetchingMore] = useState(false);
const {
notificationStatus,
notificationData,
notificationError,
isLoadingNotification,
isFetchingNotification,
fetchNotificationNextPage,
isFetchingNotificationNextPage,
notificationHasNextPage,
} = useNotification({ notificationType: filter });

// load next page if there are no filtered notifications on the current page
useEffect(() => {
if (
notificationData &&
notificationData.pages
.flatMap((page) => page?.data.notifications)
.filter((item) => (filter === "all" ? true : item.reason === filter))
.length < 10 &&
notificationHasNextPage
) {
fetchNotificationNextPage();
setIsFetchingMore(true);
} else {
setIsFetchingMore(false);
}
}, [
fetchNotificationNextPage,
filter,
notificationData,
notificationHasNextPage,
]);

const { preferences } = usePreferences();
const contentFilter = preferences?.contentFilter;

const dataLength = notificationData?.pages.reduce(
(acc, page) => acc + (page?.data.notifications.length ?? 0),
0,
);

const isEmpty =
!isFetchingNotification &&
!isFetchingNotificationNextPage &&
notificationData &&
notificationData.pages
.flatMap((page) => page?.data.notifications)
.filter((item) => (filter === "all" ? true : item.reason === filter))
.length === 0;

return (
<section>
<InfiniteScroll
dataLength={dataLength ?? 0}
next={fetchNotificationNextPage}
hasMore={notificationHasNextPage}
loader={!isFetchingMore && <LoadingSpinner />}
scrollThreshold={0.95}
className="no-scrollbar flex flex-col"
>
{notificationData &&
contentFilter &&
notificationData.pages
.flatMap((page) => page?.data.notifications)
.filter((item) =>
filter === "all" ? true : item.reason === filter,
)
.map((notification, i) => (
<Fragment key={i}>
{notification && (
<NotificationItem
key={notification.uri + i}
notification={notification}
filter={contentFilter}
/>
)}
</Fragment>
))}
</InfiniteScroll>

{isFetchingNotification && !isFetchingNotificationNextPage && (
<NotificationSkeleton />
)}
{notificationError && (
<div className="border-t">
<FeedAlert variant="badResponse" message="Something went wrong" />
</div>
)}
{isFetchingMore && <LoadingSpinner />}
{isEmpty && !notificationHasNextPage && !isFetchingMore && (
<div className="border-t">
<FeedAlert
variant="empty"
message="There are no notifications to show... yet"
/>
</div>
)}
{!isEmpty &&
!notificationError &&
!isFetchingNotification &&
!notificationHasNextPage &&
!isFetchingNotificationNextPage && <EndOfFeed />}
</section>
);
}
104 changes: 28 additions & 76 deletions src/containers/notifications/NotificationsContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,89 +1,41 @@
"use client";

import NotificationItem from "@/components/contentDisplay/notification/NotificationItem";
import NotificationSkeleton from "@/components/contentDisplay/notification/NotificationSkeleton";
import EndOfFeed from "@/components/feedback/endOfFeed/EndOfFeed";
import FeedAlert from "@/components/feedback/feedAlert/FeedAlert";
import LoadingSpinner from "@/components/status/loadingSpinner/LoadingSpinner";
import usePreferences from "@/lib/hooks/bsky/actor/usePreferences";
import useNotification from "@/lib/hooks/bsky/notification/useNotification";
import { Fragment } from "react";
import InfiniteScroll from "react-infinite-scroll-component";
import { useState } from "react";
import FilteredNotificationsContainer from "./FilteredNotificationsContainer";
import { NOTIFICATION_FILTER } from "@/lib/consts/notification";

export default function NotificationsContainer() {
const {
notificationStatus,
notificationData,
notificationError,
isLoadingNotification,
isFetchingNotification,
fetchNotificationNextPage,
isFetchingNotificationNextPage,
notificationHasNextPage,
} = useNotification();

const { preferences } = usePreferences();
const contentFilter = preferences?.contentFilter;

const dataLength = notificationData?.pages.reduce(
(acc, page) => acc + (page?.data.notifications.length ?? 0),
0,
const [currentTab, setCurrentTab] = useState<"all" | NotificationReason>(
"all",
);

const isEmpty =
!isFetchingNotification &&
!isFetchingNotificationNextPage &&
dataLength === 0;
const handleTabChange = (tab: "all" | NotificationReason) => {
setCurrentTab(tab);
};

return (
<section>
<InfiniteScroll
dataLength={dataLength ?? 0}
next={fetchNotificationNextPage}
hasMore={notificationHasNextPage}
loader={<LoadingSpinner />}
scrollThreshold={0.95}
className="no-scrollbar flex flex-col"
<div
role="tablist"
aria-orientation="horizontal"
className="no-scrollbar mt-5 flex flex-nowrap gap-3 overflow-auto px-3 md:px-0"
>
{notificationData &&
contentFilter &&
notificationData.pages
.flatMap((page) => page?.data.notifications)
.map((notification, i) => (
<Fragment key={i}>
{notification && (
<NotificationItem
key={notification.uri + i}
notification={notification}
filter={contentFilter}
/>
)}
</Fragment>
))}
</InfiniteScroll>

{isFetchingNotification && !isFetchingNotificationNextPage && (
<NotificationSkeleton />
)}
{notificationError && (
<FeedAlert
variant="badResponse"
message="Something went wrong"
standalone
/>
)}
{isEmpty && !notificationHasNextPage && (
<FeedAlert
variant="empty"
message="There are no notifications to show... yet"
standalone
/>
)}
{!isEmpty &&
!notificationError &&
!isFetchingNotification &&
!notificationHasNextPage &&
!isFetchingNotificationNextPage && <EndOfFeed />}
{NOTIFICATION_FILTER.map((type) => (
<button
key={type.label}
role="tab"
onClick={() => handleTabChange(type.value)}
className={`border-b-3 hover:text-primary shrink-0 cursor-pointer px-3 pb-2 font-semibold ${
currentTab === type.value
? "border-primary-600 text-primary border-primary"
: "border-transparent text-neutral-500"
}`}
>
{type.label}
</button>
))}
</div>
<FilteredNotificationsContainer filter={currentTab} />
</section>
);
}
14 changes: 14 additions & 0 deletions src/lib/consts/notification.ts
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
export const GROUPABLE_NOTIFICATIONS = ["like", "follow", "repost"];

export const MAX_AUTHORS_SHOWN = 6;

export const NOTIFICATION_FILTER: {
label: string;
value: NotificationReason | "all";
}[] = [
{ label: "All", value: "all" },
{ label: "Like", value: "like" },
{ label: "Follow", value: "follow" },
{ label: "Repost", value: "repost" },
{ label: "Quote", value: "quote" },
{ label: "Reply", value: "reply" },
];
9 changes: 7 additions & 2 deletions src/lib/hooks/bsky/notification/useNotification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import {
import { GroupedNotification } from "../../../../../types/feed";
import { Notification } from "@atproto/api/dist/client/types/app/bsky/notification/listNotifications";

export default function useNotification() {
interface Props {
notificationType: NotificationReason | "all";
}

export default function useNotification(props: Props) {
const { notificationType } = props;
const agent = useAgent();
const groupNotifications = (
notifications: Notification[],
Expand Down Expand Up @@ -43,7 +48,7 @@ export default function useNotification() {
fetchNextPage,
hasNextPage,
} = useInfiniteQuery({
queryKey: ["notifications"],
queryKey: ["notifications", notificationType],
queryFn: async ({ pageParam }) => {
const res = await getNotifications(agent, pageParam);
await updateSeenNotifications(agent);
Expand Down
Loading

0 comments on commit eaeca99

Please sign in to comment.