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
62 changes: 46 additions & 16 deletions components/brain/notifications/NotificationItems.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import type { DropInteractionParams } from "@/components/waves/drops/Drop";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import type { ActiveDropState } from "@/types/dropInteractionTypes";
import type { TypedNotification } from "@/types/feed.types";
import {
type NotificationDisplayItem,
isGroupedReactionsItem,
} from "@/types/feed.types";
import { memo, useMemo } from "react";
import NotificationDropReactedGroup from "./drop-reacted/NotificationDropReactedGroup";
import NotificationItem from "./NotificationItem";

interface NotificationItemsProps {
readonly items: TypedNotification[];
readonly items: NotificationDisplayItem[];
readonly activeDrop: ActiveDropState | null;
readonly onReply: (param: DropInteractionParams) => void;
readonly onQuote: (param: DropInteractionParams) => void;
readonly onDropContentClick?: ((drop: ExtendedDrop) => void) | undefined;
readonly onMarkGroupAsRead?: ((ids: number[]) => Promise<void>) | undefined;
}

function NotificationItemsComponent({
Expand All @@ -19,14 +24,16 @@ function NotificationItemsComponent({
onReply,
onQuote,
onDropContentClick,
onMarkGroupAsRead,
}: NotificationItemsProps) {
const keyedNotifications = useMemo(
const keyedItems = useMemo(
() =>
items.map((notification, index) => {
const keySuffix = notification.id ?? `fallback-${index}`;

items.map((item, index) => {
const keySuffix = isGroupedReactionsItem(item)
? `group-${item.drop.id}`
: item.id ?? `fallback-${index}`;
return {
notification,
item,
key: `notification-${keySuffix}`,
domId: `feed-item-${keySuffix}`,
};
Expand All @@ -36,15 +43,37 @@ function NotificationItemsComponent({

return (
<div className="tw-flex tw-flex-col tw-space-y-3 tw-pb-3">
{keyedNotifications.map(({ notification, key, domId }) => (
{keyedItems.map(({ item, key, domId }) => (
<div key={key} id={domId}>
<NotificationItem
notification={notification}
activeDrop={activeDrop}
onReply={onReply}
onQuote={onQuote}
onDropContentClick={onDropContentClick}
/>
{isGroupedReactionsItem(item) ? (
<div className="tw-flex">
<div className="tw-relative lg:tw-hidden">
<div className="tw-h-full tw-w-[1px] -tw-translate-x-8 tw-bg-iron-800" />
</div>
<div className="tw-w-full">
<NotificationDropReactedGroup
group={item}
activeDrop={activeDrop}
onReply={onReply}
onQuote={onQuote}
onDropContentClick={onDropContentClick}
onMarkAsRead={
onMarkGroupAsRead
? (ids) => onMarkGroupAsRead(ids)
: undefined
}
/>
</div>
</div>
) : (
<NotificationItem
notification={item}
activeDrop={activeDrop}
onReply={onReply}
onQuote={onQuote}
onDropContentClick={onDropContentClick}
/>
)}
</div>
))}
</div>
Expand All @@ -59,7 +88,8 @@ const NotificationItems = memo(
prevProps.activeDrop === nextProps.activeDrop &&
prevProps.onReply === nextProps.onReply &&
prevProps.onQuote === nextProps.onQuote &&
prevProps.onDropContentClick === nextProps.onDropContentClick
prevProps.onDropContentClick === nextProps.onDropContentClick &&
prevProps.onMarkGroupAsRead === nextProps.onMarkGroupAsRead
);
}
);
Expand Down
104 changes: 104 additions & 0 deletions components/brain/notifications/NotificationsFollowAllBtn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"use client";

import { AuthContext } from "@/components/auth/Auth";
import CircleLoader from "@/components/distribution-plan-tool/common/CircleLoader";
import { ReactQueryWrapperContext } from "@/components/react-query-wrapper/ReactQueryWrapper";
import {
FOLLOW_BTN_BUTTON_CLASSES,
FOLLOW_BTN_LOADER_SIZES,
UserFollowBtnSize,
} from "@/components/user/utils/UserFollowBtn";
import type { ApiIdentitySubscriptionActions } from "@/generated/models/ApiIdentitySubscriptionActions";
import type { ApiProfileMin } from "@/generated/models/ApiProfileMin";
import { commonApiPost } from "@/services/api/common-api";
import type { FC } from "react";
import { useContext, useState } from "react";
import {
DEFAULT_SUBSCRIPTION_BODY,
FollowBtnCheckIcon,
FollowBtnPlusIcon,
} from "./notificationsFollowShared";

interface NotificationsFollowAllBtnProps {
readonly profiles: readonly ApiProfileMin[];
readonly size?: UserFollowBtnSize | undefined;
}

const NotificationsFollowAllBtn: FC<NotificationsFollowAllBtnProps> = ({
profiles,
size = UserFollowBtnSize.SMALL,
}) => {
const { onIdentityFollowChange } = useContext(ReactQueryWrapperContext);
const { setToast, requestAuth } = useContext(AuthContext);
const [mutating, setMutating] = useState(false);

const toFollow = profiles.filter(
(p) => p.handle && (p.subscribed_actions?.length ?? 0) === 0
);
const allFollowed = toFollow.length === 0;
const label = allFollowed ? "Following All" : "Follow All";

const onFollowAll = async (): Promise<void> => {
if (allFollowed) return;
setMutating(true);
const { success } = await requestAuth();
if (!success) {
setMutating(false);
return;
}
try {
const results = await Promise.allSettled(
toFollow.map((profile) =>
commonApiPost<
ApiIdentitySubscriptionActions,
ApiIdentitySubscriptionActions
>({
endpoint: `identities/${profile.handle}/subscriptions`,
body: DEFAULT_SUBSCRIPTION_BODY,
})
)
);
const hasFulfilled = results.some((r) => r.status === "fulfilled");
const rejected = results.filter(
(r): r is PromiseRejectedResult => r.status === "rejected"
);
if (hasFulfilled) onIdentityFollowChange();
if (rejected.length > 0) {
const messages = rejected.map((r) =>
r.reason instanceof Error ? r.reason.message : String(r.reason)
);
setToast({
message: messages.join("; "),
type: "error",
});
}
} finally {
setMutating(false);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

return (
<div className="tw-flex tw-items-center tw-gap-x-2">
<button
onClick={onFollowAll}
disabled={mutating || allFollowed}
type="button"
className={`${FOLLOW_BTN_BUTTON_CLASSES[size]} ${
allFollowed
? "tw-cursor-default tw-bg-iron-800 tw-text-iron-300 tw-ring-iron-800"
: "tw-cursor-pointer tw-bg-primary-500 tw-text-white tw-ring-primary-500 hover:tw-bg-primary-600 hover:tw-ring-primary-600"
} tw-flex tw-items-center tw-rounded-lg tw-border-0 tw-font-semibold tw-ring-1 tw-ring-inset tw-transition tw-duration-300 tw-ease-out disabled:tw-opacity-70`}
>
{(() => {
if (mutating)
return <CircleLoader size={FOLLOW_BTN_LOADER_SIZES[size]} />;
if (allFollowed) return <FollowBtnCheckIcon />;
return <FollowBtnPlusIcon size={size} />;
})()}
<span>{label}</span>
</button>
</div>
);
};

export default NotificationsFollowAllBtn;
71 changes: 20 additions & 51 deletions components/brain/notifications/NotificationsFollowBtn.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
"use client";

import type { FC} from "react";
import { useState, useContext } from "react";
import type { ApiProfileMin } from "@/generated/models/ApiProfileMin";
import { AuthContext } from "@/components/auth/Auth";
import CircleLoader from "@/components/distribution-plan-tool/common/CircleLoader";
import { ReactQueryWrapperContext } from "@/components/react-query-wrapper/ReactQueryWrapper";
import {
FOLLOW_BTN_BUTTON_CLASSES,
FOLLOW_BTN_LOADER_SIZES,
FOLLOW_BTN_SVG_CLASSES,
UserFollowBtnSize,
} from "@/components/user/utils/UserFollowBtn";
import { useMutation } from "@tanstack/react-query";
import CircleLoader from "@/components/distribution-plan-tool/common/CircleLoader";
import { ReactQueryWrapperContext } from "@/components/react-query-wrapper/ReactQueryWrapper";
import { AuthContext } from "@/components/auth/Auth";
import type { ApiIdentitySubscriptionActions } from "@/generated/models/ApiIdentitySubscriptionActions";
import type { ApiProfileMin } from "@/generated/models/ApiProfileMin";
import {
commonApiDeleteWithBody,
commonApiPost,
} from "@/services/api/common-api";
import { ApiIdentitySubscriptionTargetAction } from "@/generated/models/ApiIdentitySubscriptionTargetAction";
import { useMutation } from "@tanstack/react-query";
import type { FC } from "react";
import { useContext, useState } from "react";
import {
DEFAULT_SUBSCRIPTION_BODY,
FollowBtnCheckIcon,
FollowBtnPlusIcon,
} from "./notificationsFollowShared";

interface NotificationsFollowBtnProps {
readonly profile: ApiProfileMin;
Expand All @@ -43,11 +46,7 @@ const NotificationsFollowBtn: FC<NotificationsFollowBtnProps> = ({
ApiIdentitySubscriptionActions
>({
endpoint: `identities/${profile.handle}/subscriptions`,
body: {
actions: Object.values(ApiIdentitySubscriptionTargetAction).filter(
(i) => i !== ApiIdentitySubscriptionTargetAction.DropVoted
),
},
body: DEFAULT_SUBSCRIPTION_BODY,
});
},
onSuccess: () => {
Expand All @@ -71,11 +70,7 @@ const NotificationsFollowBtn: FC<NotificationsFollowBtnProps> = ({
ApiIdentitySubscriptionActions
>({
endpoint: `identities/${profile.handle}/subscriptions`,
body: {
actions: Object.values(ApiIdentitySubscriptionTargetAction).filter(
(i) => i !== ApiIdentitySubscriptionTargetAction.DropVoted
),
},
body: DEFAULT_SUBSCRIPTION_BODY,
});
},
onSuccess: () => {
Expand Down Expand Up @@ -114,42 +109,16 @@ const NotificationsFollowBtn: FC<NotificationsFollowBtnProps> = ({
type="button"
className={`${FOLLOW_BTN_BUTTON_CLASSES[size]} ${
following
? "tw-bg-iron-800 tw-ring-iron-800 tw-text-iron-300 hover:tw-bg-iron-700 hover:tw-ring-iron-700"
: "tw-bg-primary-500 tw-ring-primary-500 hover:tw-bg-primary-600 hover:tw-ring-primary-600 tw-text-white"
} tw-flex tw-items-center tw-cursor-pointer tw-rounded-lg tw-font-semibold tw-border-0 tw-ring-1 tw-ring-inset tw-transition tw-duration-300 tw-ease-out`}>
? "tw-bg-iron-800 tw-text-iron-300 tw-ring-iron-800 hover:tw-bg-iron-700 hover:tw-ring-iron-700"
: "tw-bg-primary-500 tw-text-white tw-ring-primary-500 hover:tw-bg-primary-600 hover:tw-ring-primary-600"
} tw-flex tw-cursor-pointer tw-items-center tw-rounded-lg tw-border-0 tw-font-semibold tw-ring-1 tw-ring-inset tw-transition tw-duration-300 tw-ease-out`}
>
{mutating ? (
<CircleLoader size={FOLLOW_BTN_LOADER_SIZES[size]} />
) : following ? (
<svg
className="tw-h-3 tw-w-3"
width="17"
height="15"
viewBox="0 0 17 15"
fill="none"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.7953 0.853403L5.24867 10.0667L2.71534 7.36007C2.24867 6.92007 1.51534 6.8934 0.982005 7.26674C0.462005 7.6534 0.315338 8.3334 0.635338 8.88007L3.63534 13.7601C3.92867 14.2134 4.43534 14.4934 5.00867 14.4934C5.55534 14.4934 6.07534 14.2134 6.36867 13.7601C6.84867 13.1334 16.0087 2.2134 16.0087 2.2134C17.2087 0.986737 15.7553 -0.093263 14.7953 0.84007V0.853403Z"
fill="currentColor"
/>
</svg>
<FollowBtnCheckIcon />
) : (
<svg
className={FOLLOW_BTN_SVG_CLASSES[size]}
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg">
<path
d="M12 5V19M5 12H19"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<FollowBtnPlusIcon size={size} />
)}
<span>{label}</span>
</button>
Expand Down
7 changes: 5 additions & 2 deletions components/brain/notifications/NotificationsWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useCallback } from "react";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import type { TypedNotification } from "@/types/feed.types";
import type { NotificationDisplayItem } from "@/types/feed.types";
import type {
ActiveDropState} from "@/types/dropInteractionTypes";
import {
Expand Down Expand Up @@ -34,17 +34,19 @@ const hasChatScope = (wave: ExtendedDrop["wave"]): wave is WaveWithChatScope =>
typeof wave === "object" && wave !== null && "chat" in wave;

interface NotificationsWrapperProps {
readonly items: TypedNotification[];
readonly items: NotificationDisplayItem[];
readonly loadingOlder: boolean;
readonly activeDrop: ActiveDropState | null;
readonly setActiveDrop: (drop: ActiveDropState | null) => void;
readonly markNotificationIdsAsRead?: (ids: number[]) => Promise<void>;
}

export default function NotificationsWrapper({
items,
loadingOlder,
activeDrop,
setActiveDrop,
markNotificationIdsAsRead,
}: NotificationsWrapperProps) {
const router = useRouter();
const { isApp } = useDeviceInfo();
Expand Down Expand Up @@ -109,6 +111,7 @@ export default function NotificationsWrapper({
onReply={onReply}
onQuote={onQuote}
onDropContentClick={onDropContentClick}
onMarkGroupAsRead={markNotificationIdsAsRead}
/>
</div>
);
Expand Down
Loading