mirror of
https://github.com/Mail-0/Zero.git
synced 2026-07-01 08:16:28 +00:00
Merge pull request #885 from Mail-0/feat/outlook-provider
adds outlook provider
This commit is contained in:
@@ -4,7 +4,7 @@ import { useEffect, type ReactNode, useState, Suspense } from 'react';
|
||||
import type { EnvVarInfo } from '@zero/server/auth-providers';
|
||||
import ErrorMessage from '@/app/(auth)/login/error-message';
|
||||
import { signIn, useSession } from '@/lib/auth-client';
|
||||
import { Google } from '@/components/icons/icons';
|
||||
import { Google, Microsoft } from '@/components/icons/icons';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TriangleAlert } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -42,6 +42,9 @@ const getProviderIcon = (providerId: string, className?: string): ReactNode => {
|
||||
case 'google':
|
||||
return <Google className={defaultClass} />;
|
||||
|
||||
case 'microsoft':
|
||||
return <Microsoft className={defaultClass} />;
|
||||
|
||||
case 'zero':
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -24,7 +24,6 @@ import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { env } from '@/lib/env';
|
||||
import Image from 'next/image';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -80,119 +79,122 @@ export default function ConnectionsPage() {
|
||||
</div>
|
||||
) : data?.connections?.length ? (
|
||||
<div className="lg: grid gap-4 sm:grid-cols-1 md:grid-cols-2">
|
||||
{data.connections.map((connection) => (
|
||||
<div
|
||||
key={connection.id}
|
||||
className="bg-popover flex items-center justify-between rounded-lg border p-4"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-4">
|
||||
{connection.picture ? (
|
||||
<Image
|
||||
src={connection.picture}
|
||||
alt={connection.id + ' profile picture'}
|
||||
className="h-12 w-12 shrink-0 rounded-lg object-cover"
|
||||
width={48}
|
||||
height={48}
|
||||
/>
|
||||
) : (
|
||||
<div className="bg-primary/10 flex h-12 w-12 shrink-0 items-center justify-center rounded-lg">
|
||||
<svg viewBox="0 0 24 24" className="text-primary h-6 w-6">
|
||||
<path fill="currentColor" d={emailProviders[0]!.icon} />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<span className="truncate text-sm font-medium">{connection.name}</span>
|
||||
<div className="text-muted-foreground flex items-center gap-2 text-xs">
|
||||
<Tooltip
|
||||
delayDuration={0}
|
||||
open={openTooltip === connection.id}
|
||||
onOpenChange={(open) => {
|
||||
if (window.innerWidth <= 768) {
|
||||
setOpenTooltip(open ? connection.id : null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className="max-w-[180px] cursor-default truncate sm:max-w-[240px] md:max-w-[300px]"
|
||||
onClick={() => {
|
||||
if (window.innerWidth <= 768) {
|
||||
setOpenTooltip(
|
||||
openTooltip === connection.id ? null : connection.id,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{connection.email}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="start" className="select-all">
|
||||
<div className="font-mono">{connection.email}</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{data.connections.map((connection) => {
|
||||
const Icon = emailProviders.find(
|
||||
(p) => p.providerId === connection.providerId,
|
||||
)?.icon;
|
||||
return (
|
||||
<div
|
||||
key={connection.id}
|
||||
className="bg-popover flex items-center justify-between rounded-lg border p-4"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-4">
|
||||
{connection.picture ? (
|
||||
<Image
|
||||
src={connection.picture}
|
||||
alt=""
|
||||
className="h-12 w-12 shrink-0 rounded-lg object-cover"
|
||||
width={48}
|
||||
height={48}
|
||||
/>
|
||||
) : (
|
||||
<div className="bg-primary/10 flex h-12 w-12 shrink-0 items-center justify-center rounded-lg">
|
||||
{Icon && <Icon className="size-6" />}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<span className="truncate text-sm font-medium">{connection.name}</span>
|
||||
<div className="text-muted-foreground flex items-center gap-2 text-xs">
|
||||
<Tooltip
|
||||
delayDuration={0}
|
||||
open={openTooltip === connection.id}
|
||||
onOpenChange={(open) => {
|
||||
if (window.innerWidth <= 768) {
|
||||
setOpenTooltip(open ? connection.id : null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className="max-w-[180px] cursor-default truncate sm:max-w-[240px] md:max-w-[300px]"
|
||||
onClick={() => {
|
||||
if (window.innerWidth <= 768) {
|
||||
setOpenTooltip(
|
||||
openTooltip === connection.id ? null : connection.id,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{connection.email}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="start" className="select-all">
|
||||
<div className="font-mono">{connection.email}</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{data.disconnectedIds?.includes(connection.id) ? (
|
||||
<>
|
||||
<div>
|
||||
<Badge variant="destructive">
|
||||
{t('pages.settings.connections.disconnected')}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
await authClient.linkSocial({
|
||||
provider: connection.providerId,
|
||||
callbackURL: `${process.env.NEXT_PUBLIC_APP_URL}/settings/connections`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Unplug className="size-4" />
|
||||
{t('pages.settings.connections.reconnect')}
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground hover:text-primary ml-4 shrink-0"
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent showOverlay>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t('pages.settings.connections.disconnectTitle')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('pages.settings.connections.disconnectDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-end gap-4">
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">
|
||||
{t('pages.settings.connections.cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button onClick={() => disconnectAccount(connection.id)}>
|
||||
{t('pages.settings.connections.remove')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{data.disconnectedIds?.includes(connection.id) ? (
|
||||
<>
|
||||
<div>
|
||||
<Badge variant="destructive">
|
||||
{t('pages.settings.connections.disconnected')}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
await authClient.linkSocial({
|
||||
provider: connection.providerId,
|
||||
callbackURL: `${env.NEXT_PUBLIC_APP_URL}/settings/connections`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Unplug className="size-4" />
|
||||
{t('pages.settings.connections.reconnect')}
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground hover:text-primary ml-4 shrink-0"
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t('pages.settings.connections.disconnectTitle')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('pages.settings.connections.disconnectDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-end gap-4">
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">
|
||||
{t('pages.settings.connections.cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button onClick={() => disconnectAccount(connection.id)}>
|
||||
{t('pages.settings.connections.remove')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import { useTranslations } from 'next-intl';
|
||||
import { Button } from '../ui/button';
|
||||
import { motion } from 'motion/react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { env } from '@/lib/env';
|
||||
import { useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -31,11 +30,11 @@ export const AddConnectionDialog = ({
|
||||
const { connections, attach } = useBilling();
|
||||
const t = useTranslations();
|
||||
|
||||
const pathname = usePathname();
|
||||
const canCreateConnection = useMemo(() => {
|
||||
if (!connections?.remaining && !connections?.unlimited) return false;
|
||||
return (connections?.unlimited && !connections?.remaining) || (connections?.remaining ?? 0) > 0;
|
||||
}, [connections]);
|
||||
const pathname = usePathname();
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
if (attach) {
|
||||
@@ -97,33 +96,34 @@ export const AddConnectionDialog = ({
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{emailProviders.map((provider, index) => (
|
||||
<motion.div
|
||||
key={provider.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1, duration: 0.3 }}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
>
|
||||
<Button
|
||||
disabled={!canCreateConnection}
|
||||
variant="outline"
|
||||
className="h-24 w-full flex-col items-center justify-center gap-2"
|
||||
onClick={async () =>
|
||||
await authClient.linkSocial({
|
||||
provider: provider.providerId,
|
||||
callbackURL: `${env.NEXT_PUBLIC_APP_URL}/${pathname}`,
|
||||
})
|
||||
}
|
||||
{emailProviders.map((provider, index) => {
|
||||
const Icon = provider.icon;
|
||||
return (
|
||||
<motion.div
|
||||
key={provider.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1, duration: 0.3 }}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="h-12 w-12">
|
||||
<path fill="currentColor" d={provider.icon} />
|
||||
</svg>
|
||||
<span className="text-xs">{provider.name}</span>
|
||||
</Button>
|
||||
</motion.div>
|
||||
))}
|
||||
<Button
|
||||
disabled={!canCreateConnection}
|
||||
variant="outline"
|
||||
className="h-24 w-full flex-col items-center justify-center gap-2"
|
||||
onClick={async () =>
|
||||
await authClient.linkSocial({
|
||||
provider: provider.providerId,
|
||||
callbackURL: `${process.env.NEXT_PUBLIC_APP_URL}/${pathname}`,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Icon className="!size-6" />
|
||||
<span className="text-xs">{provider.name}</span>
|
||||
</Button>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
||||
@@ -15,6 +15,26 @@ export const Gmail = ({ className }: { className?: string }) => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const GmailColor = ({ className }: { className?: string }) => (
|
||||
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="52 42 88 66" className={className}>
|
||||
<path fill="#4285f4" d="M58 108h14V74L52 59v43c0 3.32 2.69 6 6 6" />
|
||||
<path fill="#34a853" d="M120 108h14c3.32 0 6-2.69 6-6V59l-20 15" />
|
||||
<path fill="#fbbc04" d="M120 48v26l20-15v-8c0-7.42-8.47-11.65-14.4-7.2" />
|
||||
<path fill="#ea4335" d="M72 74V48l24 18 24-18v26L96 92" />
|
||||
<path fill="#c5221f" d="M52 51v8l20 15V48l-5.6-4.2c-5.94-4.45-14.4-.22-14.4 7.2" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const Microsoft = ({ className }: { className?: string }) => (
|
||||
<svg role="img" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<title>Microsoft</title>
|
||||
<path
|
||||
d="M0 32h214.6v214.6H0V32zm233.4 0H448v214.6H233.4V32zM0 265.4h214.6V480H0V265.4zm233.4 0H448V480H233.4V265.4z"
|
||||
fill="var(--icon-color)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const Outlook = ({ className }: { className?: string }) => (
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<title>Outlook</title>
|
||||
@@ -25,6 +45,108 @@ export const Outlook = ({ className }: { className?: string }) => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const OutlookColor = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1831.085 1703.335"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
fill="#0A2767"
|
||||
d="M1831.083,894.25c0.1-14.318-7.298-27.644-19.503-35.131h-0.213l-0.767-0.426l-634.492-375.585 c-2.74-1.851-5.583-3.543-8.517-5.067c-24.498-12.639-53.599-12.639-78.098,0c-2.934,1.525-5.777,3.216-8.517,5.067L446.486,858.693 l-0.766,0.426c-19.392,12.059-25.337,37.556-13.278,56.948c3.553,5.714,8.447,10.474,14.257,13.868l634.492,375.585 c2.749,1.835,5.592,3.527,8.517,5.068c24.498,12.639,53.599,12.639,78.098,0c2.925-1.541,5.767-3.232,8.517-5.068l634.492-375.585 C1823.49,922.545,1831.228,908.923,1831.083,894.25z"
|
||||
/>
|
||||
<path
|
||||
fill="#0364B8"
|
||||
d="M520.453,643.477h416.38v381.674h-416.38V643.477z M1745.917,255.5V80.908 c1-43.652-33.552-79.862-77.203-80.908H588.204C544.552,1.046,510,37.256,511,80.908V255.5l638.75,170.333L1745.917,255.5z"
|
||||
/>
|
||||
<path fill="#0078D4" d="M511,255.5h425.833v383.25H511V255.5z" />
|
||||
<path
|
||||
fill="#28A8EA"
|
||||
d="M1362.667,255.5H936.833v383.25L1362.667,1022h383.25V638.75L1362.667,255.5z"
|
||||
/>
|
||||
<path fill="#0078D4" d="M936.833,638.75h425.833V1022H936.833V638.75z" />
|
||||
<path fill="#0364B8" d="M936.833,1022h425.833v383.25H936.833V1022z" />
|
||||
<path fill="#14447D" d="M520.453,1025.151h416.38v346.969h-416.38V1025.151z" />
|
||||
<path fill="#0078D4" d="M1362.667,1022h383.25v383.25h-383.25V1022z" />
|
||||
<linearGradient
|
||||
id="SVGID_1_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="1128.4584"
|
||||
y1="811.0833"
|
||||
x2="1128.4584"
|
||||
y2="1.9982"
|
||||
gradientTransform="matrix(1 0 0 -1 0 1705.3334)"
|
||||
>
|
||||
<stop offset="0" style={{ stopColor: '#35B8F1' }} />
|
||||
<stop offset="1" style={{ stopColor: '#28A8EA' }} />
|
||||
</linearGradient>
|
||||
<path
|
||||
fill="url(#SVGID_1_)"
|
||||
d="M1811.58,927.593l-0.809,0.426l-634.492,356.848c-2.768,1.703-5.578,3.321-8.517,4.769 c-10.777,5.132-22.481,8.029-34.407,8.517l-34.663-20.27c-2.929-1.47-5.773-3.105-8.517-4.897L447.167,906.003h-0.298 l-21.036-11.753v722.384c0.328,48.196,39.653,87.006,87.849,86.7h1230.914c0.724,0,1.363-0.341,2.129-0.341 c10.18-0.651,20.216-2.745,29.808-6.217c4.145-1.756,8.146-3.835,11.966-6.217c2.853-1.618,7.75-5.152,7.75-5.152 c21.814-16.142,34.726-41.635,34.833-68.772V894.25C1831.068,908.067,1823.616,920.807,1811.58,927.593z"
|
||||
/>
|
||||
<path
|
||||
opacity="0.5"
|
||||
fill="#0A2767"
|
||||
d="M1797.017,891.397v44.287l-663.448,456.791L446.699,906.301 c0-0.235-0.191-0.426-0.426-0.426l0,0l-63.023-37.899v-31.938l25.976-0.426l54.932,31.512l1.277,0.426l4.684,2.981 c0,0,645.563,368.346,647.267,369.197l24.698,14.478c2.129-0.852,4.258-1.703,6.813-2.555 c1.278-0.852,640.879-360.681,640.879-360.681L1797.017,891.397z"
|
||||
/>
|
||||
<path
|
||||
fill="#1490DF"
|
||||
d="M1811.58,927.593l-0.809,0.468l-634.492,356.848c-2.768,1.703-5.578,3.321-8.517,4.769 c-24.641,12.038-53.457,12.038-78.098,0c-2.918-1.445-5.76-3.037-8.517-4.769L446.657,928.061l-0.766-0.468 c-12.25-6.642-19.93-19.409-20.057-33.343v722.384c0.305,48.188,39.616,87.004,87.803,86.7c0.001,0,0.002,0,0.004,0h1229.636 c48.188,0.307,87.5-38.509,87.807-86.696c0-0.001,0-0.002,0-0.004V894.25C1831.068,908.067,1823.616,920.807,1811.58,927.593z"
|
||||
/>
|
||||
<path
|
||||
opacity="0.1"
|
||||
d="M1185.52,1279.629l-9.496,5.323c-2.752,1.752-5.595,3.359-8.517,4.812 c-10.462,5.135-21.838,8.146-33.47,8.857l241.405,285.479l421.107,101.476c11.539-8.716,20.717-20.178,26.7-33.343L1185.52,1279.629 z"
|
||||
/>
|
||||
<path
|
||||
opacity="0.05"
|
||||
d="M1228.529,1255.442l-52.505,29.51c-2.752,1.752-5.595,3.359-8.517,4.812 c-10.462,5.135-21.838,8.146-33.47,8.857l113.101,311.838l549.538,74.989c21.649-16.254,34.394-41.743,34.407-68.815v-9.326 L1228.529,1255.442z"
|
||||
/>
|
||||
<path
|
||||
fill="#28A8EA"
|
||||
d="M514.833,1703.333h1228.316c18.901,0.096,37.335-5.874,52.59-17.033l-697.089-408.331 c-2.929-1.47-5.773-3.105-8.517-4.897L447.125,906.088h-0.298l-20.993-11.838v719.914 C425.786,1663.364,465.632,1703.286,514.833,1703.333C514.832,1703.333,514.832,1703.333,514.833,1703.333z"
|
||||
/>
|
||||
<path
|
||||
opacity="0.1"
|
||||
d="M1022,418.722v908.303c-0.076,31.846-19.44,60.471-48.971,72.392 c-9.148,3.931-19,5.96-28.957,5.962H425.833V383.25H511v-42.583h433.073C987.092,340.83,1021.907,375.702,1022,418.722z"
|
||||
/>
|
||||
<path
|
||||
opacity="0.2"
|
||||
d="M979.417,461.305v908.302c0.107,10.287-2.074,20.469-6.388,29.808 c-11.826,29.149-40.083,48.273-71.54,48.417H425.833V383.25h475.656c12.356-0.124,24.533,2.958,35.344,8.943 C962.937,405.344,979.407,432.076,979.417,461.305z"
|
||||
/>
|
||||
<path
|
||||
opacity="0.2"
|
||||
d="M979.417,461.305v823.136c-0.208,43-34.928,77.853-77.927,78.225H425.833V383.25 h475.656c12.356-0.124,24.533,2.958,35.344,8.943C962.937,405.344,979.407,432.076,979.417,461.305z"
|
||||
/>
|
||||
<path
|
||||
opacity="0.2"
|
||||
d="M936.833,461.305v823.136c-0.046,43.067-34.861,78.015-77.927,78.225H425.833 V383.25h433.072c43.062,0.023,77.951,34.951,77.927,78.013C936.833,461.277,936.833,461.291,936.833,461.305z"
|
||||
/>
|
||||
<linearGradient
|
||||
id="SVGID_2_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="162.7469"
|
||||
y1="1383.0741"
|
||||
x2="774.0864"
|
||||
y2="324.2592"
|
||||
gradientTransform="matrix(1 0 0 -1 0 1705.3334)"
|
||||
>
|
||||
<stop offset="0" style={{ stopColor: '#1784D9' }} />
|
||||
<stop offset="0.5" style={{ stopColor: '#107AD5' }} />
|
||||
<stop offset="1" style={{ stopColor: '#0A63C9' }} />
|
||||
</linearGradient>
|
||||
<path
|
||||
fill="url(#SVGID_2_)"
|
||||
d="M78.055,383.25h780.723c43.109,0,78.055,34.947,78.055,78.055v780.723 c0,43.109-34.946,78.055-78.055,78.055H78.055c-43.109,0-78.055-34.947-78.055-78.055V461.305 C0,418.197,34.947,383.25,78.055,383.25z"
|
||||
/>
|
||||
<path
|
||||
fill="#FFFFFF"
|
||||
d="M243.96,710.631c19.238-40.988,50.29-75.289,89.17-98.495c43.057-24.651,92.081-36.94,141.675-35.515 c45.965-0.997,91.321,10.655,131.114,33.683c37.414,22.312,67.547,55.004,86.742,94.109c20.904,43.09,31.322,90.512,30.405,138.396 c1.013,50.043-9.706,99.628-31.299,144.783c-19.652,40.503-50.741,74.36-89.425,97.388c-41.327,23.734-88.367,35.692-136.011,34.578 c-46.947,1.133-93.303-10.651-134.01-34.067c-37.738-22.341-68.249-55.07-87.892-94.28c-21.028-42.467-31.57-89.355-30.745-136.735 C212.808,804.859,223.158,755.686,243.96,710.631z M339.006,941.858c10.257,25.912,27.651,48.385,50.163,64.812 c22.93,16.026,50.387,24.294,78.353,23.591c29.783,1.178,59.14-7.372,83.634-24.358c22.227-16.375,39.164-38.909,48.715-64.812 c10.677-28.928,15.946-59.572,15.543-90.404c0.33-31.127-4.623-62.084-14.649-91.554c-8.855-26.607-25.246-50.069-47.182-67.537 c-23.88-17.79-53.158-26.813-82.91-25.55c-28.572-0.74-56.644,7.593-80.184,23.804c-22.893,16.496-40.617,39.168-51.1,65.365 c-23.255,60.049-23.376,126.595-0.341,186.728L339.006,941.858z"
|
||||
/>
|
||||
<path fill="#50D9FF" d="M1362.667,255.5h383.25v383.25h-383.25V255.5z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const Discord = ({ className }: { className?: string }) => (
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<title>Discord</title>
|
||||
|
||||
@@ -35,6 +35,7 @@ import { backgroundQueueAtom } from '@/store/backgroundQueue';
|
||||
import { handleUnsubscribe } from '@/lib/email-utils.client';
|
||||
import { useMediaQuery } from '../../hooks/use-media-query';
|
||||
import { useSearchValue } from '@/hooks/use-search-value';
|
||||
import { useConnections } from '@/hooks/use-connections';
|
||||
import { MailList } from '@/components/mail/mail-list';
|
||||
import { useHotkeysContext } from 'react-hotkeys-hook';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
@@ -319,10 +320,18 @@ export function MailLayout() {
|
||||
const isMobile = useIsMobile();
|
||||
const router = useRouter();
|
||||
const { data: session, isPending } = useSession();
|
||||
const { data: connections } = useConnections();
|
||||
const t = useTranslations();
|
||||
const prevFolderRef = useRef(folder);
|
||||
const { enableScope, disableScope } = useHotkeysContext();
|
||||
|
||||
const activeAccount = useMemo(() => {
|
||||
if (!session?.activeConnection?.id || !connections?.connections) return null;
|
||||
return connections.connections.find(
|
||||
(connection) => connection.id === session.activeConnection?.id,
|
||||
);
|
||||
}, [session?.activeConnection?.id, connections?.connections]);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevFolderRef.current !== folder && mail.bulkSelected.length > 0) {
|
||||
clearBulkSelection();
|
||||
@@ -454,7 +463,7 @@ export function MailLayout() {
|
||||
<div className="p-2 px-[22px]">
|
||||
<SearchBar />
|
||||
<div className="mt-2">
|
||||
{folder === 'inbox' && (
|
||||
{activeAccount?.providerId === 'google' && folder === 'inbox' && (
|
||||
<CategorySelect isMultiSelectMode={mail.bulkSelected.length > 0} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -51,8 +51,7 @@ import { toast } from 'sonner';
|
||||
import Link from 'next/link';
|
||||
|
||||
export function NavUser() {
|
||||
const { data: session, refetch } = useSession();
|
||||
const router = useRouter();
|
||||
const { data: session, refetch: refetchSession } = useSession();
|
||||
const { data, refetch: refetchConnections } = useConnections();
|
||||
const [isRendered, setIsRendered] = useState(false);
|
||||
const [showPricing, setShowPricing] = useState(false);
|
||||
@@ -60,9 +59,6 @@ export function NavUser() {
|
||||
const t = useTranslations();
|
||||
const { state } = useSidebar();
|
||||
const trpc = useTRPC();
|
||||
const { refetch: refetchStats } = useStats();
|
||||
const [{ refetch: refetchThreads }] = useThreads();
|
||||
const { refetch: refetchLabels } = useLabels();
|
||||
const { mutateAsync: setDefaultConnection } = useMutation(
|
||||
trpc.connections.setDefault.mutationOptions(),
|
||||
);
|
||||
@@ -98,20 +94,13 @@ export function NavUser() {
|
||||
|
||||
useEffect(() => setIsRendered(true), []);
|
||||
|
||||
const refetchBrainLabels = useCallback(() => {
|
||||
queryClient.invalidateQueries({ queryKey: trpc.brain.getLabels.queryKey() });
|
||||
}, [queryClient]);
|
||||
|
||||
const handleAccountSwitch = (connectionId: string) => async () => {
|
||||
if (connectionId === session?.connectionId) return;
|
||||
await setDefaultConnection({ connectionId });
|
||||
refetch();
|
||||
refetchConnections();
|
||||
refetchThreads();
|
||||
refetchLabels();
|
||||
refetchStats();
|
||||
refetchBrainState();
|
||||
refetchBrainLabels();
|
||||
await refetchConnections();
|
||||
refetchSession();
|
||||
// TODO: fix this cache issue, for now this is a quick fix to hard refresh the page
|
||||
window.location.href = pathname;
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { GmailColor, OutlookColor } from '../components/icons/icons';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
export const I18N_LOCALE_COOKIE_NAME = 'i18n:locale';
|
||||
@@ -14,15 +15,15 @@ export const CACHE_BURST_KEY = 'cache-burst:v0.0.2';
|
||||
|
||||
export const emailProviders = [
|
||||
{
|
||||
name: 'Google',
|
||||
icon: 'M11.99 13.9v-3.72h9.36c.14.63.25 1.22.25 2.05c0 5.71-3.83 9.77-9.6 9.77c-5.52 0-10-4.48-10-10S6.48 2 12 2c2.7 0 4.96.99 6.69 2.61l-2.84 2.76c-.72-.68-1.98-1.48-3.85-1.48c-3.31 0-6.01 2.75-6.01 6.12s2.7 6.12 6.01 6.12c3.83 0 5.24-2.65 5.5-4.22h-5.51z',
|
||||
name: 'Gmail',
|
||||
icon: GmailColor,
|
||||
providerId: 'google',
|
||||
},
|
||||
// {
|
||||
// name: 'Microsoft',
|
||||
// icon: 'M11.99 13.9v-3.72h9.36c.14.63.25 1.22.25 2.05c0 5.71-3.83 9.77-9.6 9.77c-5.52 0-10-4.48-10-10S6.48 2 12 2c2.7 0 4.96.99 6.69 2.61l-2.84 2.76c-.72-.68-1.98-1.48-3.85-1.48c-3.31 0-6.01 2.75-6.01 6.12s2.7 6.12 6.01 6.12c3.83 0 5.24-2.65 5.5-4.22h-5.51z',
|
||||
// providerId: 'microsoft',
|
||||
// },
|
||||
{
|
||||
name: 'Outlook',
|
||||
icon: OutlookColor,
|
||||
providerId: 'microsoft',
|
||||
},
|
||||
] as const;
|
||||
|
||||
interface GmailColor {
|
||||
@@ -86,7 +86,7 @@
|
||||
"@zero/server": "workspace:*",
|
||||
"ai": "^4.3.9",
|
||||
"autumn-js": "0.0.22",
|
||||
"better-auth": "1.2.7",
|
||||
"better-auth": "1.2.8-beta.7",
|
||||
"canvas-confetti": "1.9.3",
|
||||
"cheerio": "1.0.0",
|
||||
"class-variance-authority": "0.7.1",
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^1.3.21",
|
||||
"@hono/trpc-server": "^0.3.4",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||
"@react-email/components": "^0.0.38",
|
||||
"@react-email/render": "^1.1.0",
|
||||
"@trpc/client": "^11.1.2",
|
||||
@@ -32,6 +34,7 @@
|
||||
"googleapis": "^148.0.0",
|
||||
"he": "^1.2.0",
|
||||
"hono": "^4.7.8",
|
||||
"hono-party": "^0.0.12",
|
||||
"jsonrepair": "^3.12.0",
|
||||
"mimetext": "^3.0.27",
|
||||
"partyserver": "^0.0.71",
|
||||
|
||||
@@ -50,25 +50,31 @@ export const authProviders = (env: Record<string, string>): ProviderConfig[] =>
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
// {
|
||||
// id: 'microsoft',
|
||||
// name: 'Microsoft',
|
||||
// requiredEnvVars: ['MICROSOFT_CLIENT_ID', 'MICROSOFT_CLIENT_SECRET'],
|
||||
// envVarInfo: [
|
||||
// { name: 'MICROSOFT_CLIENT_ID', source: 'Microsoft Azure App ID' },
|
||||
// { name: 'MICROSOFT_CLIENT_SECRET', source: 'Microsoft Azure App Password' },
|
||||
// ],
|
||||
// config: {
|
||||
// clientId: env.MICROSOFT_CLIENT_ID!,
|
||||
// clientSecret: env.MICROSOFT_CLIENT_SECRET!,
|
||||
// redirectUri: env.MICROSOFT_REDIRECT_URI!,
|
||||
// scope: ['https://graph.microsoft.com/User.Read', 'offline_access'],
|
||||
// authority: 'https://login.microsoftonline.com/common',
|
||||
// responseType: 'code',
|
||||
// prompt: 'consent',
|
||||
// loginHint: 'email',
|
||||
// },
|
||||
// },
|
||||
{
|
||||
id: 'microsoft',
|
||||
name: 'Microsoft',
|
||||
requiredEnvVars: ['MICROSOFT_CLIENT_ID', 'MICROSOFT_CLIENT_SECRET'],
|
||||
envVarInfo: [
|
||||
{ name: 'MICROSOFT_CLIENT_ID', source: 'Microsoft Azure App ID' },
|
||||
{ name: 'MICROSOFT_CLIENT_SECRET', source: 'Microsoft Azure App Password' },
|
||||
],
|
||||
config: {
|
||||
clientId: env.MICROSOFT_CLIENT_ID,
|
||||
clientSecret: env.MICROSOFT_CLIENT_SECRET,
|
||||
redirectUri: env.MICROSOFT_REDIRECT_URI,
|
||||
scope: [
|
||||
'https://graph.microsoft.com/User.Read',
|
||||
'https://graph.microsoft.com/Mail.ReadWrite',
|
||||
'https://graph.microsoft.com/Mail.Send',
|
||||
'offline_access',
|
||||
],
|
||||
authority: 'https://login.microsoftonline.com/common',
|
||||
responseType: 'code',
|
||||
prompt: 'consent',
|
||||
loginHint: 'email',
|
||||
disableProfilePhoto: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function isProviderEnabled(provider: ProviderConfig, env: Record<string, string>): boolean {
|
||||
|
||||
@@ -31,7 +31,12 @@ const connectionHandlerHook = async (account: Account) => {
|
||||
}
|
||||
|
||||
const driver = createDriver(account.providerId, {
|
||||
auth: { accessToken: account.accessToken, refreshToken: account.refreshToken, email: '' },
|
||||
auth: {
|
||||
accessToken: account.accessToken,
|
||||
refreshToken: account.refreshToken,
|
||||
userId: account.userId,
|
||||
email: '',
|
||||
},
|
||||
});
|
||||
|
||||
const userInfo = await driver.getUserInfo().catch(() => {
|
||||
|
||||
@@ -34,7 +34,6 @@ export class GoogleMailManager implements MailManager {
|
||||
|
||||
this.gmail = google.gmail({ version: 'v1', auth: this.auth });
|
||||
}
|
||||
|
||||
public getScope(): string {
|
||||
return [
|
||||
'https://www.googleapis.com/auth/gmail.modify',
|
||||
@@ -42,7 +41,6 @@ export class GoogleMailManager implements MailManager {
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
public getAttachment(messageId: string, attachmentId: string) {
|
||||
return this.withErrorHandler(
|
||||
'getAttachment',
|
||||
@@ -62,7 +60,6 @@ export class GoogleMailManager implements MailManager {
|
||||
{ messageId, attachmentId },
|
||||
);
|
||||
}
|
||||
|
||||
public getEmailAliases() {
|
||||
return this.withErrorHandler('getEmailAliases', async () => {
|
||||
const profile = await this.gmail.users.getProfile({
|
||||
@@ -95,7 +92,6 @@ export class GoogleMailManager implements MailManager {
|
||||
return aliases;
|
||||
});
|
||||
}
|
||||
|
||||
public markAsRead(threadIds: string[]) {
|
||||
return this.withErrorHandler(
|
||||
'markAsRead',
|
||||
@@ -105,7 +101,6 @@ export class GoogleMailManager implements MailManager {
|
||||
{ threadIds },
|
||||
);
|
||||
}
|
||||
|
||||
public markAsUnread(threadIds: string[]) {
|
||||
return this.withErrorHandler(
|
||||
'markAsUnread',
|
||||
@@ -115,7 +110,6 @@ export class GoogleMailManager implements MailManager {
|
||||
{ threadIds },
|
||||
);
|
||||
}
|
||||
|
||||
public getUserInfo() {
|
||||
return this.withErrorHandler(
|
||||
'getUserInfo',
|
||||
@@ -132,7 +126,6 @@ export class GoogleMailManager implements MailManager {
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
public getTokens<T>(code: string) {
|
||||
return this.withErrorHandler(
|
||||
'getTokens',
|
||||
@@ -143,7 +136,6 @@ export class GoogleMailManager implements MailManager {
|
||||
{ code },
|
||||
);
|
||||
}
|
||||
|
||||
public count() {
|
||||
return this.withErrorHandler(
|
||||
'count',
|
||||
@@ -174,7 +166,6 @@ export class GoogleMailManager implements MailManager {
|
||||
{ email: this.config.auth?.email },
|
||||
);
|
||||
}
|
||||
|
||||
public list(params: {
|
||||
folder: string;
|
||||
query?: string;
|
||||
@@ -209,7 +200,6 @@ export class GoogleMailManager implements MailManager {
|
||||
{ folder, q, maxResults, _labelIds, pageToken, email: this.config.auth?.email },
|
||||
);
|
||||
}
|
||||
|
||||
public get(id: string) {
|
||||
return this.withErrorHandler(
|
||||
'get',
|
||||
@@ -353,7 +343,6 @@ export class GoogleMailManager implements MailManager {
|
||||
{ id, email: this.config.auth?.email },
|
||||
);
|
||||
}
|
||||
|
||||
public create(data: IOutgoingMessage) {
|
||||
return this.withErrorHandler(
|
||||
'create',
|
||||
@@ -371,7 +360,6 @@ export class GoogleMailManager implements MailManager {
|
||||
{ data, email: this.config.auth?.email },
|
||||
);
|
||||
}
|
||||
|
||||
public delete(id: string) {
|
||||
return this.withErrorHandler(
|
||||
'delete',
|
||||
@@ -382,7 +370,6 @@ export class GoogleMailManager implements MailManager {
|
||||
{ id },
|
||||
);
|
||||
}
|
||||
|
||||
public normalizeIds(ids: string[]) {
|
||||
return this.withSyncErrorHandler(
|
||||
'normalizeIds',
|
||||
@@ -395,7 +382,6 @@ export class GoogleMailManager implements MailManager {
|
||||
{ ids },
|
||||
);
|
||||
}
|
||||
|
||||
public modifyLabels(
|
||||
threadIds: string[],
|
||||
options: { addLabels: string[]; removeLabels: string[] },
|
||||
@@ -411,7 +397,6 @@ export class GoogleMailManager implements MailManager {
|
||||
{ threadIds, options },
|
||||
);
|
||||
}
|
||||
|
||||
public sendDraft(draftId: string, data: IOutgoingMessage) {
|
||||
return this.withErrorHandler(
|
||||
'sendDraft',
|
||||
@@ -663,9 +648,6 @@ export class GoogleMailManager implements MailManager {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================================
|
||||
|
||||
private async modifyThreadLabels(
|
||||
threadIds: string[],
|
||||
requestBody: gmail_v1.Schema$ModifyThreadRequest,
|
||||
@@ -990,7 +972,6 @@ export class GoogleMailManager implements MailManager {
|
||||
rawMessage: draft.message,
|
||||
};
|
||||
}
|
||||
|
||||
private async withErrorHandler<T>(
|
||||
operation: string,
|
||||
fn: () => Promise<T> | T,
|
||||
@@ -1037,16 +1018,15 @@ export class GoogleMailManager implements MailManager {
|
||||
}
|
||||
}
|
||||
|
||||
private findAttachments(parts: any[]): any[] {
|
||||
let results: any[] = [];
|
||||
private findAttachments(parts: gmail_v1.Schema$MessagePart[]): gmail_v1.Schema$MessagePart[] {
|
||||
let results: gmail_v1.Schema$MessagePart[] = [];
|
||||
|
||||
for (const part of parts) {
|
||||
if (part.filename && part.filename.length > 0) {
|
||||
const contentDisposition =
|
||||
part.headers?.find((h: any) => h.name?.toLowerCase() === 'content-disposition')?.value ||
|
||||
'';
|
||||
part.headers?.find((h) => h.name?.toLowerCase() === 'content-disposition')?.value || '';
|
||||
const isInline = contentDisposition.toLowerCase().includes('inline');
|
||||
const hasContentId = part.headers?.some((h: any) => h.name?.toLowerCase() === 'content-id');
|
||||
const hasContentId = part.headers?.some((h) => h.name?.toLowerCase() === 'content-id');
|
||||
|
||||
if (!isInline || (isInline && !hasContentId)) {
|
||||
results.push(part);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { MailManager, ManagerConfig } from './types';
|
||||
import { OutlookMailManager } from './microsoft';
|
||||
import { GoogleMailManager } from './google';
|
||||
|
||||
const supportedProviders = {
|
||||
google: GoogleMailManager,
|
||||
// microsoft: microsoftDriver,
|
||||
microsoft: OutlookMailManager,
|
||||
};
|
||||
|
||||
export const createDriver = (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,8 @@ export interface IConfig {
|
||||
|
||||
export type ManagerConfig = {
|
||||
auth: {
|
||||
userId: string;
|
||||
// accountId: string;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
email: string;
|
||||
|
||||
@@ -38,6 +38,7 @@ export const getActiveDriver = async () => {
|
||||
auth: {
|
||||
accessToken: activeConnection.accessToken,
|
||||
refreshToken: activeConnection.refreshToken,
|
||||
userId: activeConnection.userId,
|
||||
email: activeConnection.email,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user