Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Add unsubscribe functionality to NewsletterModal #210

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
13 changes: 8 additions & 5 deletions apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,11 +257,14 @@ export function BulkUnsubscribeSection({
</LoadingContent>
)}
</Card>
<NewsletterModal
newsletter={openedNewsletter}
onClose={() => setOpenedNewsletter(undefined)}
refreshInterval={refreshInterval}
/>
{openedNewsletter && (
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you need to add {openedNewsletter && (?
I think it stops the animation when it's done like that. Or was this added to fix a bug?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reason

This is cus the <NewsletterModal /> component now requires (non-optional) the openedNewsletter prop, which was previously optional.

The reason for this change is that the component now uses the existing useUnsubscribeButton hook, which does need a required openedNewsletter prop.

Initially, I tried keeping openedNewsletter as optional (to have less diffs like the one you brought up here), but that would have required introducing a second hook for unsubscribing, which would complicate the logic unnecessarily.

Minor Benefit

Additionally, making openedNewsletter a required prop also ensures that the <NewsletterModal /> is only rendered when all necessary data is present, making it more robust and predictable in terms of when it renders throughout the app.

Quick Demo

Also, below GIF shows that the animations are still intact. No issues with that according to my testing.

CleanShot 2024-08-23 at 12 29 31

<NewsletterModal
newsletter={openedNewsletter}
onClose={() => setOpenedNewsletter(undefined)}
refreshInterval={props.refreshInterval}
mutate={mutate}
/>
)}
<PremiumModal />
</>
);
Expand Down
13 changes: 7 additions & 6 deletions apps/web/app/(app)/cold-email-blocker/ColdEmailList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,13 @@ export function ColdEmailList() {

<TablePagination totalPages={data.totalPages} />

<NewsletterModal
newsletter={
openedRow ? { name: openedRow.fromEmail || "" } : undefined
}
onClose={() => setOpenedRow(undefined)}
/>
{openedRow && (
<NewsletterModal
newsletter={{ name: openedRow.fromEmail || "" }}
onClose={() => setOpenedRow(undefined)}
mutate={mutate}
/>
)}
</div>
) : (
<NoColdEmails />
Expand Down
12 changes: 6 additions & 6 deletions apps/web/app/(app)/cold-email-blocker/ColdEmailRejected.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@ export function ColdEmailRejected() {

<TablePagination totalPages={data.totalPages} />

<NewsletterModal
newsletter={
openedRow ? { name: openedRow.fromEmail || "" } : undefined
}
onClose={() => setOpenedRow(undefined)}
/>
{openedRow && (
<NewsletterModal
newsletter={openedRow && { name: openedRow.fromEmail || "" }}
onClose={() => setOpenedRow(undefined)}
/>
)}
</div>
) : (
<NoRejectedColdEmails />
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(app)/compose/selectors/color-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export const ColorSelector = ({ open, onOpenChange }: any) => {

<PopoverContent
sideOffset={5}
className="my-1 flex max-h-80 w-48 flex-col overflow-hidden overflow-y-auto rounded border p-1 shadow-xl "
className="my-1 flex max-h-80 w-48 flex-col overflow-hidden overflow-y-auto rounded border p-1 shadow-xl"
align="start"
>
<div className="flex flex-col">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(app)/compose/selectors/link-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const LinkSelector = ({ open, onOpenChange }: LinkSelectorProps) => {
const url = getUrlFromString(input.value);
url && editor.chain().focus().setLink({ href: url }).run();
}}
className="flex p-1 "
className="flex p-1"
>
<input
ref={inputRef}
Expand Down
59 changes: 31 additions & 28 deletions apps/web/app/(app)/new-senders/NewSenders.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,25 @@
"use client";

import React, { useState } from "react";
import useSWR from "swr";
import { useSession } from "next-auth/react";
import { usePostHog } from "posthog-js/react";
import { Card, Title } from "@tremor/react";
import groupBy from "lodash/groupBy";
import { FilterIcon, Users2Icon } from "lucide-react";
import { LoadingContent } from "@/components/LoadingContent";
import { Skeleton } from "@/components/ui/skeleton";
import { useExpanded } from "@/app/(app)/stats/useExpanded";
import { NewsletterModal } from "@/app/(app)/stats/NewsletterModal";
import type {
NewSendersQuery,
NewSendersResponse,
} from "@/app/api/user/stats/new-senders/route";
import { formatShortDate } from "@/utils/date";
import { formatStat } from "@/utils/stats";
import { StatsCards } from "@/components/StatsCards";
import { ActionCell, HeaderButton } from "@/app/(app)/bulk-unsubscribe/common";
import {
useNewsletterFilter,
useBulkUnsubscribeShortcuts,
useNewsletterFilter,
} from "@/app/(app)/bulk-unsubscribe/hooks";
import { ShortcutTooltip } from "@/app/(app)/bulk-unsubscribe/ShortcutTooltip";
import type { Row } from "@/app/(app)/bulk-unsubscribe/types";
import { usePremiumModal } from "@/app/(app)/premium/PremiumModal";
import { DetailedStatsFilter } from "@/app/(app)/stats/DetailedStatsFilter";
import { NewsletterModal } from "@/app/(app)/stats/NewsletterModal";
import { useExpanded } from "@/app/(app)/stats/useExpanded";
import type { LabelsResponse } from "@/app/api/google/labels/route";
import type {
NewSendersQuery,
NewSendersResponse,
} from "@/app/api/user/stats/new-senders/route";
import { LoadingContent } from "@/components/LoadingContent";
import { usePremium } from "@/components/PremiumAlert";
import { usePremiumModal } from "@/app/(app)/premium/PremiumModal";
import { useLabels } from "@/hooks/useLabels";
import { ShortcutTooltip } from "@/app/(app)/bulk-unsubscribe/ShortcutTooltip";
import type { Row } from "@/app/(app)/bulk-unsubscribe/types";
import { StatsCards } from "@/components/StatsCards";
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
Expand All @@ -38,6 +28,16 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useLabels } from "@/hooks/useLabels";
import { formatShortDate } from "@/utils/date";
import { formatStat } from "@/utils/stats";
import { Card, Title } from "@tremor/react";
import groupBy from "lodash/groupBy";
import { FilterIcon, Users2Icon } from "lucide-react";
import { useSession } from "next-auth/react";
import { usePostHog } from "posthog-js/react";
import React, { useState } from "react";
import useSWR from "swr";

export function NewSenders({ refreshInterval }: { refreshInterval: number }) {
const session = useSession();
Expand Down Expand Up @@ -211,11 +211,14 @@ export function NewSenders({ refreshInterval }: { refreshInterval: number }) {
<div className="mt-2 px-6 pb-6">{extra}</div>
</LoadingContent>
</Card>
<NewsletterModal
newsletter={openedNewsletter}
onClose={() => setOpenedNewsletter(undefined)}
refreshInterval={refreshInterval}
/>
{openedNewsletter && (
<NewsletterModal
newsletter={openedNewsletter}
onClose={() => setOpenedNewsletter(undefined)}
refreshInterval={refreshInterval}
mutate={mutate}
/>
)}
<PremiumModal />
</>
);
Expand Down
70 changes: 46 additions & 24 deletions apps/web/app/(app)/stats/NewsletterModal.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,64 @@
import useSWR from "swr";
import { BarChart } from "@tremor/react";
import type { DateRange } from "react-day-picker";
import Link from "next/link";
import { ExternalLinkIcon } from "lucide-react";
import { useSession } from "next-auth/react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { MoreDropdown } from "@/app/(app)/bulk-unsubscribe/common";
import { useUnsubscribe } from "@/app/(app)/bulk-unsubscribe/hooks";
import { Row } from "@/app/(app)/bulk-unsubscribe/types";
import { getDateRangeParams } from "@/app/(app)/stats/params";
import type { ThreadsResponse } from "@/app/api/google/threads/controller";
import type {
SenderEmailsQuery,
SenderEmailsResponse,
} from "@/app/api/user/stats/sender-emails/route";
import type { ZodPeriod } from "@inboxzero/tinybird";
import { AlertBasic } from "@/components/Alert";
import { LoadingContent } from "@/components/LoadingContent";
import { usePremium } from "@/components/PremiumAlert";
import { Tooltip } from "@/components/Tooltip";
import { SectionHeader } from "@/components/Typography";
import { EmailList } from "@/components/email-list/EmailList";
import type { ThreadsResponse } from "@/app/api/google/threads/controller";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { getGmailFilterSettingsUrl } from "@/utils/url";
import { Tooltip } from "@/components/Tooltip";
import { AlertBasic } from "@/components/Alert";
import { onAutoArchive } from "@/utils/actions/client";
import { MoreDropdown } from "@/app/(app)/bulk-unsubscribe/common";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useLabels } from "@/hooks/useLabels";
import type { Row } from "@/app/(app)/bulk-unsubscribe/types";
import { onAutoArchive } from "@/utils/actions/client";
import { getGmailFilterSettingsUrl } from "@/utils/url";
import type { ZodPeriod } from "@inboxzero/tinybird";
import { BarChart } from "@tremor/react";
import { ExternalLinkIcon } from "lucide-react";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { usePostHog } from "posthog-js/react";
import type { DateRange } from "react-day-picker";
import useSWR from "swr";

export function NewsletterModal(props: {
newsletter?: Pick<Row, "name" | "lastUnsubscribeLink" | "autoArchived">;
newsletter: Pick<Row, "name" | "lastUnsubscribeLink" | "autoArchived">;
onClose: (isOpen: boolean) => void;
refreshInterval?: number;
mutate?: () => Promise<any>;
Copy link
Contributor

@coderabbitai coderabbitai bot Aug 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid Using any Type

The use of any should be avoided to maintain type safety. Consider specifying a more precise type for the mutate function to enhance type checking.

- mutate?: () => Promise<any>;
+ mutate?: () => Promise<void>;
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mutate?: () => Promise<any>;
mutate?: () => Promise<void>;
Tools
Biome

[error] 42-42: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree bot. I tried my best to make the suggestion of mutate?: () => Promise<void>; work but there were a lot of side effects with it. Would be happy to see a fix for this.

The issue here is that the mutate function passed down to this NewsletterModal component is to mutate multiple newsletters, but this component only needs to mutate a single newsletter.

The only fixes I could think of was to:

  1. Create a new API route to mutate a single newsletter as well, along with the existing API route to mutate multiple newsletters.
  2. Update the useUnsubscribeButton hook's logic, and all of its usages.

I think this will need refactoring in the future regardless. Would be happy to have a look at this later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aryanprince, thanks for sharing your insights and potential solutions. It seems like a refactoring effort could indeed be beneficial to address the scope of the mutate function. To ensure this doesn't get lost, would you like me to create a follow-up issue to track this refactoring task? This way, we can revisit it when the time is right and ensure the NewsletterModal component handles mutations more precisely. Let me know how you'd like to proceed!

Comment on lines +37 to +40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Approve newsletter prop change, but consider specifying mutate type

The change to make the newsletter prop required is good as it ensures the component always receives necessary data. However, for the mutate prop:

The newsletter prop change is approved.

The mutate prop uses the any type, which is generally discouraged as it bypasses TypeScript's type checking. Consider specifying a more precise return type for the Promise. If the exact type is unknown, you could use unknown instead of any:

mutate?: () => Promise<unknown>;

This maintains type safety while allowing flexibility in the return type.

Tools
Biome

[error] 40-40: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

}) {
const { newsletter, refreshInterval, onClose } = props;
const mutate = props.mutate || (() => Promise.resolve()); // Set a default value for mutate if it's not provided

const session = useSession();
const email = session.data?.user.email;

const { userLabels } = useLabels();

const { hasUnsubscribeAccess, mutate: refetchPremium } = usePremium();

const posthog = usePostHog();

const { unsubscribeLoading, onUnsubscribe } = useUnsubscribe({
item: newsletter,
hasUnsubscribeAccess,
mutate,
posthog,
refetchPremium,
});

return (
<Dialog open={!!newsletter} onOpenChange={onClose}>
<DialogContent className="max-h-screen overflow-x-scroll overflow-y-scroll lg:min-w-[880px] xl:min-w-[1280px]">
Expand All @@ -55,13 +69,21 @@ export function NewsletterModal(props: {
</DialogHeader>

<div className="flex space-x-2">
<Button size="sm" variant="outline">
<Button size="sm" variant="outline" disabled={unsubscribeLoading}>
<a
href={newsletter.lastUnsubscribeLink || undefined}
target="_blank"
rel="noreferrer"
onClick={onUnsubscribe}
>
Unsubscribe
{unsubscribeLoading ? (
<div className="flex cursor-not-allowed items-center opacity-50">
<Button loading={true} />
<span>Unsubscribing...</span>
</div>
) : (
<span>Unsubscribe</span>
)}
</a>
</Button>
<Tooltip content="Auto archive emails using Gmail filters">
Expand Down