diff --git a/apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeDesktop.tsx b/apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeDesktop.tsx index 5aa261a6c6..7e54577caa 100644 --- a/apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeDesktop.tsx +++ b/apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeDesktop.tsx @@ -21,7 +21,7 @@ export function BulkUnsubscribeDesktop(props: { const { tableRows, sortColumn, setSortColumn } = props; return ( - +
@@ -67,7 +67,7 @@ export function BulkUnsubscribeRowDesktop({ onDoubleClick, hasUnsubscribeAccess, mutate, - setOpenedNewsletter, + onOpenNewsletter, userGmailLabels, openPremiumModal, userEmail, @@ -121,7 +121,7 @@ export function BulkUnsubscribeRowDesktop({ hasUnsubscribeAccess={hasUnsubscribeAccess} mutate={mutate} refetchPremium={refetchPremium} - setOpenedNewsletter={setOpenedNewsletter} + onOpenNewsletter={onOpenNewsletter} selected={selected} userGmailLabels={userGmailLabels} openPremiumModal={openPremiumModal} diff --git a/apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeMobile.tsx b/apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeMobile.tsx index b3d35ff162..3af98c4f64 100644 --- a/apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeMobile.tsx +++ b/apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeMobile.tsx @@ -42,7 +42,7 @@ export function BulkUnsubscribeRowMobile({ refetchPremium, mutate, hasUnsubscribeAccess, - setOpenedNewsletter, + onOpenNewsletter, }: RowProps) { const readPercentage = (item.readEmails / item.value) * 100; const archivedEmails = item.value - item.inboxEmails; @@ -117,19 +117,12 @@ export function BulkUnsubscribeRowMobile({ variant={ item.status === NewsletterStatus.UNSUBSCRIBED ? "red" : "default" } - disabled={!item.lastUnsubscribeLink} asChild={!!item.lastUnsubscribeLink} > setOpenedNewsletter(item)} + onClick={() => onOpenNewsletter(item)} > More diff --git a/apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeSection.tsx b/apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeSection.tsx index 1b70adc7f5..3903020f7b 100644 --- a/apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeSection.tsx +++ b/apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeSection.tsx @@ -3,6 +3,7 @@ import React, { useState } from "react"; import { useSession } from "next-auth/react"; import useSWR from "swr"; +import { usePostHog } from "posthog-js/react"; import { FilterIcon } from "lucide-react"; import { Title } from "@tremor/react"; import type { DateRange } from "react-day-picker"; @@ -25,7 +26,6 @@ import { import BulkUnsubscribeSummary from "@/app/(app)/bulk-unsubscribe/BulkUnsubscribeSummary"; import { useStatLoader } from "@/providers/StatLoaderProvider"; import { usePremiumModal } from "@/app/(app)/premium/PremiumModal"; -import { Toggle } from "@/components/Toggle"; import { useLabels } from "@/hooks/useLabels"; import { BulkUnsubscribeMobile, @@ -37,6 +37,7 @@ import { } from "@/app/(app)/bulk-unsubscribe/BulkUnsubscribeDesktop"; import { Card } from "@/components/ui/card"; import { ShortcutTooltip } from "@/app/(app)/bulk-unsubscribe/ShortcutTooltip"; +import { SearchBar } from "@/app/(app)/bulk-unsubscribe/SearchBar"; type Newsletter = NewsletterStatsResponse["newsletters"][number]; @@ -58,15 +59,14 @@ export function BulkUnsubscribeSection({ const { typesArray } = useEmailsToIncludeFilter(); const { filtersArray, filters, setFilters } = useNewsletterFilter(); - const [includeMissingUnsubscribe, setIncludeMissingUnsubscribe] = - useState(false); + const posthog = usePostHog(); const params: NewsletterStatsQuery = { types: typesArray, filters: filtersArray, orderBy: sortColumn, limit: 100, - includeMissingUnsubscribe, + includeMissingUnsubscribe: true, ...getDateRangeParams(dateRange), }; const urlParams = new URLSearchParams(params as any); @@ -83,6 +83,11 @@ export function BulkUnsubscribeSection({ const { expanded, extra } = useExpanded(); const [openedNewsletter, setOpenedNewsletter] = React.useState(); + const onOpenNewsletter = (newsletter: Newsletter) => { + setOpenedNewsletter(newsletter); + posthog?.capture("Clicked Expand Sender"); + }; + const [selectedRow, setSelectedRow] = React.useState< Newsletter | undefined >(); @@ -90,13 +95,15 @@ export function BulkUnsubscribeSection({ useBulkUnsubscribeShortcuts({ newsletters: data?.newsletters, selectedRow, - setOpenedNewsletter, + onOpenNewsletter, setSelectedRow, refetchPremium, hasUnsubscribeAccess, mutate, }); + const [search, setSearch] = useState(""); + const { isLoading: isStatsLoading } = useStatLoader(); const { userLabels } = useLabels(); @@ -108,20 +115,29 @@ export function BulkUnsubscribeSection({ : BulkUnsubscribeRowDesktop; const tableRows = data?.newsletters + .filter( + search + ? (item) => + item.name.toLowerCase().includes(search.toLowerCase()) || + item.lastUnsubscribeLink + ?.toLowerCase() + .includes(search.toLowerCase()) + : Boolean, + ) .slice(0, expanded ? undefined : 50) .map((item) => ( { setSelectedRow(item); }} - onDoubleClick={() => setOpenedNewsletter(item)} + onDoubleClick={() => onOpenNewsletter(item)} hasUnsubscribeAccess={hasUnsubscribeAccess} refetchPremium={refetchPremium} openPremiumModal={openModal} @@ -132,21 +148,16 @@ export function BulkUnsubscribeSection({ <> {!isMobile && } -
- Bulk Unsubscribe -
+
+ + Bulk unsubscribe from emails + +
- { - setIncludeMissingUnsubscribe(!includeMissingUnsubscribe); - }} - /> + setFilters({ ...filters, - ["autoArchived"]: !filters.autoArchived, + ["unsubscribed"]: !filters.unsubscribed, }), }, { - label: "Unsubscribed", - checked: filters.unsubscribed, + label: "Auto Archived", + checked: filters.autoArchived, setChecked: () => setFilters({ ...filters, - ["unsubscribed"]: !filters.unsubscribed, + ["autoArchived"]: !filters.autoArchived, }), }, { diff --git a/apps/web/app/(app)/bulk-unsubscribe/SearchBar.tsx b/apps/web/app/(app)/bulk-unsubscribe/SearchBar.tsx new file mode 100644 index 0000000000..0428d2dc8a --- /dev/null +++ b/apps/web/app/(app)/bulk-unsubscribe/SearchBar.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { z } from "zod"; +import { SearchIcon } from "lucide-react"; +import { useCallback, useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import throttle from "lodash/throttle"; +import { Input } from "@/components/Input"; +import { Button } from "@/components/ui/button"; + +const searchSchema = z.object({ search: z.string() }); + +export function SearchBar({ + onSearch, +}: { + onSearch: (search: string) => void; +}) { + const [showSearch, setShowSearch] = useState(false); + + const { + register, + formState: { errors }, + watch, + } = useForm>({ + resolver: zodResolver(searchSchema), + defaultValues: { search: "" }, + }); + + const throttledSearch = useCallback( + throttle((value: string) => { + onSearch(value.trim()); + }, 300), + [onSearch], + ); + + watch((data) => { + if (data.search !== undefined) { + throttledSearch(data.search); + } + }); + return ( + <> + + {showSearch && ( +
+ + + )} + + ); +} diff --git a/apps/web/app/(app)/bulk-unsubscribe/ShortcutTooltip.tsx b/apps/web/app/(app)/bulk-unsubscribe/ShortcutTooltip.tsx index 46afa52f88..b95e2bdeea 100644 --- a/apps/web/app/(app)/bulk-unsubscribe/ShortcutTooltip.tsx +++ b/apps/web/app/(app)/bulk-unsubscribe/ShortcutTooltip.tsx @@ -18,8 +18,8 @@ export function ShortcutTooltip() {
} > - ); diff --git a/apps/web/app/(app)/bulk-unsubscribe/common.tsx b/apps/web/app/(app)/bulk-unsubscribe/common.tsx index bda1e42612..38f64b2f7b 100644 --- a/apps/web/app/(app)/bulk-unsubscribe/common.tsx +++ b/apps/web/app/(app)/bulk-unsubscribe/common.tsx @@ -53,7 +53,7 @@ import type { GroupsResponse } from "@/app/api/user/group/route"; import { addGroupItemAction } from "@/utils/actions/group"; import { toastError, toastSuccess } from "@/components/Toast"; import { createFilterAction } from "@/utils/actions/mail"; -import { isActionError, isErrorMessage } from "@/utils/error"; +import { captureException, isActionError, isErrorMessage } from "@/utils/error"; import type { GetThreadsResponse } from "@/app/api/google/threads/basic/route"; import { archiveEmails, deleteEmails } from "@/providers/QueueProvider"; import { isDefined } from "@/utils/types"; @@ -65,7 +65,7 @@ export function ActionCell({ hasUnsubscribeAccess, mutate, refetchPremium, - setOpenedNewsletter, + onOpenNewsletter, userGmailLabels, openPremiumModal, userEmail, @@ -74,7 +74,7 @@ export function ActionCell({ hasUnsubscribeAccess: boolean; mutate: () => Promise; refetchPremium: () => Promise; - setOpenedNewsletter: React.Dispatch>; + onOpenNewsletter: (row: T) => void; selected: boolean; userGmailLabels: LabelsResponse["labels"]; openPremiumModal: () => void; @@ -137,7 +137,7 @@ export function ActionCell({ /> ({ setUnsubscribeLoading(true); - await setNewsletterStatusAction({ - newsletterEmail: item.name, - status: NewsletterStatus.UNSUBSCRIBED, - }); - await mutate(); - await decrementUnsubscribeCreditAction(); - await refetchPremium(); - - posthog.capture("Clicked Unsubscribe"); + try { + posthog.capture("Clicked Unsubscribe"); + + await setNewsletterStatusAction({ + newsletterEmail: item.name, + status: + item.status === NewsletterStatus.UNSUBSCRIBED + ? null + : NewsletterStatus.UNSUBSCRIBED, + }); + await mutate(); + await decrementUnsubscribeCreditAction(); + await refetchPremium(); + } catch (error) { + captureException(error); + console.error(error); + } setUnsubscribeLoading(false); }, [hasUnsubscribeAccess, item.name, mutate, posthog, refetchPremium]); @@ -311,25 +319,23 @@ function UnsubscribeButton({ refetchPremium, }); + const isLink = hasUnsubscribeAccess && item.lastUnsubscribeLink; + return ( ); } @@ -526,13 +532,13 @@ function ApproveButton({ } export function MoreDropdown({ - setOpenedNewsletter, + onOpenNewsletter, item, userEmail, userGmailLabels, posthog, }: { - setOpenedNewsletter?: (row: T) => void; + onOpenNewsletter?: (row: T) => void; item: T; userEmail: string; userGmailLabels: LabelsResponse["labels"]; @@ -552,13 +558,8 @@ export function MoreDropdown({ - {!!setOpenedNewsletter && ( - { - setOpenedNewsletter(item); - posthog?.capture("Clicked Expand Sender"); - }} - > + {!!onOpenNewsletter && ( + onOpenNewsletter(item)}> View stats @@ -667,7 +668,7 @@ export function HeaderButton(props: { export function useBulkUnsubscribeShortcuts({ newsletters, selectedRow, - setOpenedNewsletter, + onOpenNewsletter, setSelectedRow, refetchPremium, hasUnsubscribeAccess, @@ -676,7 +677,7 @@ export function useBulkUnsubscribeShortcuts({ newsletters?: T[]; selectedRow?: T; setSelectedRow: (row: T) => void; - setOpenedNewsletter: (row: T) => void; + onOpenNewsletter: (row: T) => void; refetchPremium: () => Promise; hasUnsubscribeAccess: boolean; mutate: () => Promise; @@ -704,7 +705,7 @@ export function useBulkUnsubscribeShortcuts({ } else if (e.key === "Enter") { // open modal e.preventDefault(); - setOpenedNewsletter(item); + onOpenNewsletter(item); return; } @@ -755,7 +756,7 @@ export function useBulkUnsubscribeShortcuts({ hasUnsubscribeAccess, refetchPremium, setSelectedRow, - setOpenedNewsletter, + onOpenNewsletter, ]); } diff --git a/apps/web/app/(app)/bulk-unsubscribe/types.ts b/apps/web/app/(app)/bulk-unsubscribe/types.ts index cec0facba4..c27aafb279 100644 --- a/apps/web/app/(app)/bulk-unsubscribe/types.ts +++ b/apps/web/app/(app)/bulk-unsubscribe/types.ts @@ -13,9 +13,7 @@ type Newsletter = NewsletterStatsResponse["newsletters"][number]; export interface RowProps { item: Newsletter; - setOpenedNewsletter: React.Dispatch< - React.SetStateAction - >; + onOpenNewsletter: (row: Newsletter) => void; userGmailLabels: LabelsResponse["labels"]; userEmail: string; mutate: () => Promise; diff --git a/apps/web/app/(app)/new-senders/NewSenders.tsx b/apps/web/app/(app)/new-senders/NewSenders.tsx index 44c687126f..cb3f30d4a8 100644 --- a/apps/web/app/(app)/new-senders/NewSenders.tsx +++ b/apps/web/app/(app)/new-senders/NewSenders.tsx @@ -3,6 +3,7 @@ import React, { useState } from "react"; import useSWR from "swr"; import { useSession } from "next-auth/react"; +import { usePostHog } from "posthog-js/react"; import { Card, Table, @@ -35,16 +36,12 @@ import { import { DetailedStatsFilter } from "@/app/(app)/stats/DetailedStatsFilter"; import type { LabelsResponse } from "@/app/api/google/labels/route"; import { usePremium } from "@/components/PremiumAlert"; -import type { DateRange } from "react-day-picker"; import { usePremiumModal } from "@/app/(app)/premium/PremiumModal"; import { useLabels } from "@/hooks/useLabels"; import { ShortcutTooltip } from "@/app/(app)/bulk-unsubscribe/ShortcutTooltip"; import { Row } from "@/app/(app)/bulk-unsubscribe/types"; -export function NewSenders(props: { - dateRange?: DateRange | undefined; - refreshInterval: number; -}) { +export function NewSenders({ refreshInterval }: { refreshInterval: number }) { const session = useSession(); const userEmail = session.data?.user.email || ""; @@ -71,7 +68,12 @@ export function NewSenders(props: { const { userLabels } = useLabels(); const { expanded, extra } = useExpanded(); + const posthog = usePostHog(); const [openedNewsletter, setOpenedNewsletter] = React.useState(); + const onOpenNewsletter = (newsletter: Row) => { + setOpenedNewsletter(newsletter); + posthog?.capture("Clicked Expand Sender"); + }; const [selectedRow, setSelectedRow] = React.useState(); @@ -94,7 +96,7 @@ export function NewSenders(props: { useBulkUnsubscribeShortcuts({ newsletters: rows, selectedRow, - setOpenedNewsletter, + onOpenNewsletter, setSelectedRow, refetchPremium, hasUnsubscribeAccess, @@ -122,7 +124,7 @@ export function NewSenders(props: { /> -
+
New Senders
@@ -193,7 +195,7 @@ export function NewSenders(props: { userEmail={userEmail} firstEmail={item.firstEmail} numberOfEmails={item.numberOfEmails} - setOpenedNewsletter={setOpenedNewsletter} + onOpenNewsletter={onOpenNewsletter} userGmailLabels={userLabels} mutate={mutate} selected={selectedRow?.name === item.name} @@ -214,20 +216,22 @@ export function NewSenders(props: { setOpenedNewsletter(undefined)} - refreshInterval={props.refreshInterval} + refreshInterval={refreshInterval} /> ); } -function NewSendersTable(props: { +function NewSendersTable({ + tableRows, + sortColumn, + setSortColumn, +}: { tableRows?: React.ReactNode; sortColumn: "subject" | "date" | "numberOfEmails"; setSortColumn: (sortColumn: "subject" | "date" | "numberOfEmails") => void; }) { - const { tableRows, sortColumn, setSortColumn } = props; - return (
@@ -267,12 +271,25 @@ function NewSendersTable(props: { ); } -function NewSenderRow(props: { +function NewSenderRow({ + item, + firstEmail, + numberOfEmails, + refetchPremium, + openPremiumModal, + onOpenNewsletter, + selected, + onSelectRow, + hasUnsubscribeAccess, + mutate, + userEmail, + userGmailLabels, +}: { item: Row; firstEmail: { from: string; subject: string; timestamp: number }; userEmail: string; numberOfEmails: number; - setOpenedNewsletter: React.Dispatch>; + onOpenNewsletter: (row: Row) => void; userGmailLabels: LabelsResponse["labels"]; mutate: () => Promise; selected: boolean; @@ -281,16 +298,13 @@ function NewSenderRow(props: { refetchPremium: () => Promise; openPremiumModal: () => void; }) { - const { item, firstEmail, numberOfEmails, refetchPremium, openPremiumModal } = - props; - return ( {firstEmail.from} @@ -303,13 +317,13 @@ function NewSenderRow(props: { diff --git a/apps/web/app/(app)/stats/DetailedStatsFilter.tsx b/apps/web/app/(app)/stats/DetailedStatsFilter.tsx index ceeb8d523d..cc51f2ccfe 100644 --- a/apps/web/app/(app)/stats/DetailedStatsFilter.tsx +++ b/apps/web/app/(app)/stats/DetailedStatsFilter.tsx @@ -40,7 +40,7 @@ export function DetailedStatsFilter(props: { >