mirror of
https://github.com/Mail-0/Zero.git
synced 2026-06-28 06:46:15 +00:00
Optimize thread counting and add license headers to workflow files (#1949)
# READ CAREFULLY THEN REMOVE Remove bullet points that are not relevant. PLEASE REFRAIN FROM USING AI TO WRITE YOUR CODE AND PR DESCRIPTION. IF YOU DO USE AI TO WRITE YOUR CODE PLEASE PROVIDE A DESCRIPTION AND REVIEW IT CAREFULLY. MAKE SURE YOU UNDERSTAND THE CODE YOU ARE SUBMITTING USING AI. - Pull requests that do not follow these guidelines will be closed without review or comment. - If you use AI to write your PR description your pr will be close without review or comment. - If you are unsure about anything, feel free to ask for clarification. ## Description Please provide a clear description of your changes. --- ## Type of Change Please delete options that are not relevant. - [ ] 🐛 Bug fix (non-breaking change which fixes an issue) - [ ] ✨ New feature (non-breaking change which adds functionality) - [ ] 💥 Breaking change (fix or feature with breaking changes) - [ ] 📝 Documentation update - [ ] 🎨 UI/UX improvement - [ ] 🔒 Security enhancement - [ ] ⚡ Performance improvement ## Areas Affected Please check all that apply: - [ ] Email Integration (Gmail, IMAP, etc.) - [ ] User Interface/Experience - [ ] Authentication/Authorization - [ ] Data Storage/Management - [ ] API Endpoints - [ ] Documentation - [ ] Testing Infrastructure - [ ] Development Workflow - [ ] Deployment/Infrastructure ## Testing Done Describe the tests you've done: - [ ] Unit tests added/updated - [ ] Integration tests added/updated - [ ] Manual testing performed - [ ] Cross-browser testing (if UI changes) - [ ] Mobile responsiveness verified (if UI changes) ## Security Considerations For changes involving data or authentication: - [ ] No sensitive data is exposed - [ ] Authentication checks are in place - [ ] Input validation is implemented - [ ] Rate limiting is considered (if applicable) ## Checklist - [ ] I have read the [CONTRIBUTING](https://github.com/Mail-0/Zero/blob/staging/.github/CONTRIBUTING.md) document - [ ] My code follows the project's style guidelines - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in complex areas - [ ] I have updated the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix/feature works - [ ] All tests pass locally - [ ] Any dependent changes are merged and published ## Additional Notes Add any other context about the pull request here. ## Screenshots/Recordings Add screenshots or recordings here if applicable. --- _By submitting this pull request, I confirm that my contribution is made under the terms of the project's license._ <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Improved thread counting performance by batching label queries and added Apache license headers to workflow files. - **Refactors** - Replaced individual thread count queries with a single batched query for all labels. - Added license headers to workflow-related source files. <!-- End of auto-generated description by cubic. --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Improved thread counting performance by enabling batch counting of threads across multiple folders or labels. * **Refactor** * Streamlined folder synchronization logic and removed unused or commented-out code for better maintainability. * **Documentation** * Added Apache 2.0 license headers to several files to clarify usage and licensing terms. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -201,13 +201,13 @@ function LoginClientContent({ providers, isProd }: LoginClientProps) {
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ease-in-out ${expandedProviders[provider.id] ? 'max-h-[500px] opacity-100' : 'max-h-0 opacity-0'}`}
|
||||
className={`overflow-hidden duration-300 ease-in-out ${expandedProviders[provider.id] ? 'max-h-[500px] opacity-100' : 'max-h-0 opacity-0'}`}
|
||||
>
|
||||
<div className="bg-black/3 p-4 font-mono text-sm dark:bg-white/3">
|
||||
<div className="bg-black/3 dark:bg-white/3 p-4 font-mono text-sm">
|
||||
{provider.envVarStatus.map((envVar) => (
|
||||
<div
|
||||
key={envVar.name}
|
||||
className={`mb-3 flex items-start transition-all duration-300 ease-in-out last:mb-0 ${expandedProviders[provider.id] ? 'translate-y-0 opacity-100' : 'translate-y-2 opacity-0'}`}
|
||||
className={`mb-3 flex items-start duration-300 ease-in-out last:mb-0 ${expandedProviders[provider.id] ? 'translate-y-0 opacity-100' : 'translate-y-2 opacity-0'}`}
|
||||
style={{
|
||||
transitionDelay: expandedProviders[provider.id]
|
||||
? `${provider.envVarStatus.indexOf(envVar) * 50}ms`
|
||||
|
||||
@@ -380,7 +380,7 @@ export default function OpenPage() {
|
||||
</div>
|
||||
|
||||
{/* Project Stats */}
|
||||
<div className="mb-8 overflow-hidden rounded-xl border bg-linear-to-b from-white/50 to-white/10 p-6 backdrop-blur-sm dark:border-neutral-700 dark:from-neutral-900/50 dark:to-neutral-900/30">
|
||||
<div className="bg-linear-to-b mb-8 overflow-hidden rounded-xl border from-white/50 to-white/10 p-6 backdrop-blur-sm dark:border-neutral-700 dark:from-neutral-900/50 dark:to-neutral-900/30">
|
||||
<div className="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -484,7 +484,7 @@ export default function OpenPage() {
|
||||
|
||||
<div className="mt-6 grid gap-4 lg:grid-cols-3">
|
||||
{/* Repository Growth */}
|
||||
<Card className="col-span-full border-neutral-100 bg-white/50 p-4 transition-all hover:bg-white/60 lg:col-span-2 dark:border-neutral-800 dark:bg-neutral-900/50 dark:hover:bg-neutral-900/60">
|
||||
<Card className="col-span-full border-neutral-100 bg-white/50 p-4 hover:bg-white/60 lg:col-span-2 dark:border-neutral-800 dark:bg-neutral-900/50 dark:hover:bg-neutral-900/60">
|
||||
<h3 className="mb-4 text-sm font-medium text-neutral-600 dark:text-neutral-400">
|
||||
Repository Growth
|
||||
</h3>
|
||||
@@ -576,7 +576,7 @@ export default function OpenPage() {
|
||||
</Card>
|
||||
|
||||
{/* Activity Chart */}
|
||||
<Card className="col-span-full border-neutral-200 bg-white/50 p-4 transition-all hover:bg-white/60 lg:col-span-1 dark:border-neutral-800 dark:bg-neutral-900/50 dark:hover:bg-neutral-900/60">
|
||||
<Card className="col-span-full border-neutral-200 bg-white/50 p-4 hover:bg-white/60 lg:col-span-1 dark:border-neutral-800 dark:bg-neutral-900/50 dark:hover:bg-neutral-900/60">
|
||||
<h3 className="mb-4 text-sm font-medium text-neutral-600 dark:text-neutral-400">
|
||||
Recent Activity
|
||||
</h3>
|
||||
@@ -672,7 +672,7 @@ export default function OpenPage() {
|
||||
{filteredCoreTeam?.map((member, index) => (
|
||||
<div
|
||||
key={member.login}
|
||||
className="group relative flex items-center gap-4 rounded-xl border bg-white/50 p-4 transition-all hover:-translate-y-1 hover:bg-white hover:shadow-lg dark:border-neutral-800 dark:bg-neutral-900/50 dark:hover:bg-neutral-900 dark:hover:shadow-neutral-900/50"
|
||||
className="group relative flex items-center gap-4 rounded-xl border bg-white/50 p-4 hover:-translate-y-1 hover:bg-white hover:shadow-lg dark:border-neutral-800 dark:bg-neutral-900/50 dark:hover:bg-neutral-900 dark:hover:shadow-neutral-900/50"
|
||||
style={{
|
||||
animationDelay: `${index * 100}ms`,
|
||||
animation: 'fadeInUp 0.5s ease-out forwards',
|
||||
@@ -783,7 +783,7 @@ export default function OpenPage() {
|
||||
key={contributor.login}
|
||||
href={contributor.html_url}
|
||||
target="_blank"
|
||||
className="group relative flex flex-col items-center rounded-xl border bg-white/50 p-4 transition-all hover:-translate-y-1 hover:bg-white hover:shadow-lg dark:border-neutral-800 dark:bg-neutral-900/50 dark:hover:bg-neutral-900 dark:hover:shadow-neutral-900/50"
|
||||
className="group relative flex flex-col items-center rounded-xl border bg-white/50 p-4 hover:-translate-y-1 hover:bg-white hover:shadow-lg dark:border-neutral-800 dark:bg-neutral-900/50 dark:hover:bg-neutral-900 dark:hover:shadow-neutral-900/50"
|
||||
style={{
|
||||
animationDelay: `${index * 50}ms`,
|
||||
animation: 'fadeInUp 0.5s ease-out forwards',
|
||||
@@ -908,7 +908,7 @@ export default function OpenPage() {
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="relative overflow-hidden rounded-xl border bg-linear-to-br from-neutral-50 to-white shadow-sm dark:border-neutral-800 dark:from-neutral-900/80 dark:to-neutral-900/30">
|
||||
<div className="bg-linear-to-br relative overflow-hidden rounded-xl border from-neutral-50 to-white shadow-sm dark:border-neutral-800 dark:from-neutral-900/80 dark:to-neutral-900/30">
|
||||
<div className="absolute inset-0 opacity-20 dark:opacity-20"></div>
|
||||
|
||||
<div className="relative p-6">
|
||||
@@ -928,7 +928,7 @@ export default function OpenPage() {
|
||||
<div className="mt-5 flex flex-wrap gap-3">
|
||||
<Button
|
||||
asChild
|
||||
className="relative overflow-hidden bg-neutral-900 text-white transition-all hover:bg-neutral-800 dark:bg-white dark:text-neutral-900 dark:hover:bg-neutral-100"
|
||||
className="relative overflow-hidden bg-neutral-900 text-white hover:bg-neutral-800 dark:bg-white dark:text-neutral-900 dark:hover:bg-neutral-100"
|
||||
>
|
||||
<a
|
||||
href={`https://github.com/${REPOSITORY}/blob/main/.github/CONTRIBUTING.md`}
|
||||
|
||||
@@ -61,7 +61,7 @@ export default function PrivacyPolicy() {
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => handleCopyLink(sectionId)}
|
||||
className="text-gray-400 transition-all hover:text-gray-700 dark:text-white/60 dark:hover:text-white/80"
|
||||
className="text-gray-400 hover:text-gray-700 dark:text-white/60 dark:hover:text-white/80"
|
||||
aria-label={`Copy link to ${section.title} section`}
|
||||
>
|
||||
<Link2
|
||||
|
||||
@@ -6,14 +6,12 @@ import { Button } from '@/components/ui/button';
|
||||
import Footer from '@/components/home/footer';
|
||||
import { createSectionId } from '@/lib/utils';
|
||||
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const LAST_UPDATED = 'February 13, 2025';
|
||||
|
||||
export default function TermsOfService() {
|
||||
const { copiedValue: copiedSection, copyToClipboard } = useCopyToClipboard();
|
||||
|
||||
|
||||
const handleCopyLink = (sectionId: string) => {
|
||||
const url = `${window.location.origin}${window.location.pathname}#${sectionId}`;
|
||||
@@ -25,7 +23,7 @@ export default function TermsOfService() {
|
||||
<Navigation />
|
||||
<div className="relative z-10 flex grow flex-col">
|
||||
{/* Back Button */}
|
||||
<div className="absolute right-4 top-6 md:left-8 md:top-8 md:right-auto">
|
||||
<div className="absolute right-4 top-6 md:left-8 md:right-auto md:top-8">
|
||||
<a href="/">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -64,7 +62,7 @@ export default function TermsOfService() {
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => handleCopyLink(sectionId)}
|
||||
className="text-gray-400 transition-all hover:text-gray-700 dark:text-white/60 dark:hover:text-white/80"
|
||||
className="text-gray-400 hover:text-gray-700 dark:text-white/60 dark:hover:text-white/80"
|
||||
aria-label={`Copy link to ${section.title} section`}
|
||||
>
|
||||
<Link2
|
||||
|
||||
@@ -94,7 +94,7 @@ export default function DeveloperPage() {
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 pb-4 sm:gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{developerResources.map((resource) => (
|
||||
<Card key={resource.title} className="transition-all hover:shadow-md">
|
||||
<Card key={resource.title} className="hover:shadow-md">
|
||||
<CardHeader className="sm:p-6">
|
||||
<div className="flex items-start gap-4 sm:items-center">
|
||||
<div className={`shrink-0 rounded-lg ${resource.bgColor} p-2.5`}>
|
||||
|
||||
@@ -164,7 +164,7 @@ export default function ConnectionsPage() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground hover:text-primary ml-4 shrink-0"
|
||||
className="text-muted-foreground hover:text-primary ml-4 shrink-0"
|
||||
disabled={data.connections.length === 1}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
@@ -205,7 +205,7 @@ export default function ConnectionsPage() {
|
||||
<AddConnectionDialog>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="group relative w-9 overflow-hidden transition-all duration-200 hover:w-full sm:hover:w-[32.5%]"
|
||||
className="group relative w-9 overflow-hidden duration-200 hover:w-full sm:hover:w-[32.5%]"
|
||||
>
|
||||
<Plus className="absolute left-2 h-4 w-4" />
|
||||
<span className="whitespace-nowrap pl-7 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
||||
@@ -217,7 +217,7 @@ export default function ConnectionsPage() {
|
||||
<Button
|
||||
onClick={() => setPricingDialog('true')}
|
||||
variant="outline"
|
||||
className="group relative w-9 overflow-hidden transition-all duration-200 hover:w-full sm:hover:w-[32.5%]"
|
||||
className="group relative w-9 overflow-hidden duration-200 hover:w-full sm:hover:w-[32.5%]"
|
||||
>
|
||||
<Plus className="absolute left-2 h-4 w-4" />
|
||||
<span className="whitespace-nowrap pl-7 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
||||
|
||||
@@ -316,7 +316,7 @@ export default function Editor({
|
||||
>
|
||||
{/* Make sure the command palette doesn't cause a refresh */}
|
||||
<EditorCommand
|
||||
className="border-muted bg-background z-50 h-auto max-h-[330px] overflow-y-auto rounded-md border px-1 py-2 shadow-md transition-all"
|
||||
className="border-muted bg-background z-50 h-auto max-h-[330px] overflow-y-auto rounded-md border px-1 py-2 shadow-md"
|
||||
onKeyDown={(e) => {
|
||||
// Prevent form submission on any key that might trigger it
|
||||
if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
|
||||
|
||||
@@ -131,7 +131,7 @@ export function LabelDialog({
|
||||
<button
|
||||
key={color.backgroundColor}
|
||||
type="button"
|
||||
className={`h-10 w-10 rounded-[4px] border-[0.5px] border-white/10 transition-all ${
|
||||
className={`h-10 w-10 rounded-[4px] border-[0.5px] border-white/10 ${
|
||||
formColor?.backgroundColor.toString() === color.backgroundColor &&
|
||||
formColor.textColor.toString() === color.textColor
|
||||
? 'scale-110 ring-2 ring-blue-500 ring-offset-1'
|
||||
|
||||
@@ -1313,10 +1313,7 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }:
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="flex cursor-pointer flex-col pb-2 transition-all duration-200"
|
||||
onClick={toggleCollapse}
|
||||
>
|
||||
<div className="flex cursor-pointer flex-col pb-2 duration-200" onClick={toggleCollapse}>
|
||||
<div className="mt-3 flex w-full items-start justify-between gap-4 px-4">
|
||||
<div className="flex w-full justify-center gap-4">
|
||||
<BimiAvatar
|
||||
@@ -1635,16 +1632,11 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'h-0 overflow-hidden transition-all duration-200',
|
||||
!isCollapsed && 'h-px',
|
||||
)}
|
||||
></div>
|
||||
<div className={cn('h-0 overflow-hidden duration-200', !isCollapsed && 'h-px')}></div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'grid overflow-hidden transition-all duration-200',
|
||||
'grid overflow-hidden duration-200',
|
||||
isCollapsed ? 'grid-rows-[0fr]' : 'grid-rows-[1fr]',
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
@@ -223,7 +223,7 @@ const Thread = memo(
|
||||
data-thread-id={idToUse}
|
||||
key={idToUse}
|
||||
className={cn(
|
||||
'hover:bg-offsetLight dark:hover:bg-primary/5 group relative mx-1 flex cursor-pointer flex-col items-start rounded-lg py-2 text-left text-sm transition-all hover:opacity-100',
|
||||
'hover:bg-offsetLight dark:hover:bg-primary/5 group relative mx-1 flex cursor-pointer flex-col items-start rounded-lg py-2 text-left text-sm hover:opacity-100',
|
||||
(isMailSelected || isMailBulkSelected || isKeyboardFocused) &&
|
||||
'border-border bg-primary/5 opacity-100',
|
||||
isKeyboardFocused && 'ring-primary/50',
|
||||
@@ -572,7 +572,7 @@ const Draft = memo(({ message }: { message: { id: string } }) => {
|
||||
<div
|
||||
key={message.id}
|
||||
className={cn(
|
||||
'group relative mx-[8px] flex cursor-pointer flex-col items-start overflow-clip rounded-[10px] border-transparent py-3 text-left text-sm transition-all',
|
||||
'group relative mx-[8px] flex cursor-pointer flex-col items-start overflow-clip rounded-[10px] border-transparent py-3 text-left text-sm',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
@@ -604,7 +604,7 @@ const Draft = memo(({ message }: { message: { id: string } }) => {
|
||||
<div
|
||||
key={message.id}
|
||||
className={cn(
|
||||
'hover:bg-offsetLight dark:hover:bg-primary/5 group relative mx-[8px] flex cursor-pointer flex-col items-start overflow-clip rounded-[10px] border-transparent py-3 text-left text-sm transition-all hover:opacity-100',
|
||||
'hover:bg-offsetLight dark:hover:bg-primary/5 group relative mx-[8px] flex cursor-pointer flex-col items-start overflow-clip rounded-[10px] border-transparent py-3 text-left text-sm hover:opacity-100',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -526,7 +526,7 @@ export function NotesPanel({ threadId }: NotesPanelProps) {
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className="animate-in fade-in-20 zoom-in-95 dark:bg-panelDark fixed top-20 z-50 h-[calc(100dvh-5rem)] max-h-[calc(100dvh-5rem)] w-full max-w-screen overflow-hidden rounded-t-lg border border-t bg-[#FAFAFA] shadow-lg duration-100 sm:absolute sm:right-0 sm:top-full sm:mt-2 sm:h-auto sm:max-h-[80vh] sm:w-[350px] sm:max-w-[90vw] sm:rounded-xl sm:border lg:left-[-200px] xl:left-[-300px] dark:border-[#252525]"
|
||||
className="animate-in fade-in-20 zoom-in-95 dark:bg-panelDark max-w-screen fixed top-20 z-50 h-[calc(100dvh-5rem)] max-h-[calc(100dvh-5rem)] w-full overflow-hidden rounded-t-lg border border-t bg-[#FAFAFA] shadow-lg duration-100 sm:absolute sm:right-0 sm:top-full sm:mt-2 sm:h-auto sm:max-h-[80vh] sm:w-[350px] sm:max-w-[90vw] sm:rounded-xl sm:border lg:left-[-200px] xl:left-[-300px] dark:border-[#252525]"
|
||||
onClick={handlePanelClick}
|
||||
>
|
||||
<div className="sticky top-0 z-10 flex items-center justify-between border-b border-[#E7E7E7] p-3 dark:border-[#252525]">
|
||||
@@ -706,7 +706,7 @@ export function NotesPanel({ threadId }: NotesPanelProps) {
|
||||
<button
|
||||
onClick={() => setSelectedColor(color.value)}
|
||||
className={cn(
|
||||
'h-5 w-5 rounded-full transition-all',
|
||||
'h-5 w-5 rounded-full',
|
||||
color.value === 'default' ? 'bg-background border' : '',
|
||||
color.value === 'red' ? 'bg-red-500' : '',
|
||||
color.value === 'orange' ? 'bg-orange-500' : '',
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
Archive,
|
||||
ArchiveX,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Folders,
|
||||
Lightning,
|
||||
Mail,
|
||||
@@ -20,41 +18,37 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { EmptyStateIcon } from '../icons/empty-state-svg';
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useOptimisticThreadState } from '@/components/mail/optimistic-thread-state';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useOptimisticActions } from '@/hooks/use-optimistic-actions';
|
||||
import { focusedIndexAtom } from '@/hooks/use-mail-navigation';
|
||||
|
||||
import { type ThreadDestination } from '@/lib/thread-actions';
|
||||
import { handleUnsubscribe } from '@/lib/email-utils.client';
|
||||
import { useThread, useThreads } from '@/hooks/use-threads';
|
||||
import { useAISidebar } from '@/components/ui/ai-sidebar';
|
||||
import { EmptyStateIcon } from '../icons/empty-state-svg';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import type { ParsedMessage, Attachment } from '@/types';
|
||||
import { useAnimations } from '@/hooks/use-animations';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { MailDisplaySkeleton } from './mail-skeleton';
|
||||
import { useTRPC } from '@/providers/query-provider';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cleanHtml } from '@/lib/email-utils';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import ReplyCompose from './reply-composer';
|
||||
import { NotesPanel } from './note-panel';
|
||||
import { cn, FOLDERS } from '@/lib/utils';
|
||||
import { m } from '@/paraglide/messages';
|
||||
import MailDisplay from './mail-display';
|
||||
|
||||
import { useParams } from 'react-router';
|
||||
import { Inbox } from 'lucide-react';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { format } from 'date-fns';
|
||||
import { useAtom } from 'jotai';
|
||||
import { toast } from 'sonner';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { useAnimations } from '@/hooks/use-animations';
|
||||
|
||||
const formatFileSize = (size: number) => {
|
||||
const sizeInMB = (size / (1024 * 1024)).toFixed(2);
|
||||
@@ -85,7 +79,7 @@ export function ThreadDemo({ messages, isMobile }: ThreadDisplayProps) {
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'bg-offsetLight dark:bg-offsetDark relative flex flex-col overflow-hidden transition-all duration-300',
|
||||
'bg-offsetLight dark:bg-offsetDark relative flex flex-col overflow-hidden duration-300',
|
||||
isMobile ? 'h-full' : 'h-full',
|
||||
!isMobile && !isFullscreen && 'rounded-r-lg',
|
||||
isFullscreen ? 'fixed inset-0 z-50' : '',
|
||||
@@ -97,10 +91,7 @@ export function ThreadDemo({ messages, isMobile }: ThreadDisplayProps) {
|
||||
{[...(messages || [])].reverse().map((message, index) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={cn(
|
||||
'transition-all duration-200',
|
||||
index > 0 && 'border-border border-t',
|
||||
)}
|
||||
className={cn('duration-200', index > 0 && 'border-border border-t')}
|
||||
>
|
||||
<MailDisplay
|
||||
demo
|
||||
@@ -171,9 +162,9 @@ export function ThreadDisplay() {
|
||||
const [, items] = useThreads();
|
||||
const [isStarred, setIsStarred] = useState(false);
|
||||
const [isImportant, setIsImportant] = useState(false);
|
||||
|
||||
|
||||
const [navigationDirection, setNavigationDirection] = useState<'previous' | 'next' | null>(null);
|
||||
|
||||
|
||||
const animationsEnabled = useAnimations();
|
||||
|
||||
// Collect all attachments from all messages in the thread
|
||||
@@ -199,34 +190,6 @@ export function ThreadDisplay() {
|
||||
// Get optimistic state for this thread
|
||||
const optimisticState = useOptimisticThreadState(id ?? '');
|
||||
|
||||
const handlePrevious = useCallback(() => {
|
||||
if (!id || !items.length || focusedIndex === null) return;
|
||||
if (focusedIndex > 0) {
|
||||
const prevThread = items[focusedIndex - 1];
|
||||
if (prevThread) {
|
||||
// Clear draft and reply state when navigating to previous thread
|
||||
setMode(null);
|
||||
setActiveReplyId(null);
|
||||
setDraftId(null);
|
||||
setThreadId(prevThread.id);
|
||||
setFocusedIndex(focusedIndex - 1);
|
||||
if (animationsEnabled) {
|
||||
setNavigationDirection('previous');
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
items,
|
||||
id,
|
||||
focusedIndex,
|
||||
setThreadId,
|
||||
setFocusedIndex,
|
||||
setMode,
|
||||
setActiveReplyId,
|
||||
setDraftId,
|
||||
animationsEnabled,
|
||||
]);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
if (!id || !items.length || focusedIndex === null) return setThreadId(null);
|
||||
if (focusedIndex < items.length - 1) {
|
||||
@@ -751,7 +714,7 @@ export function ThreadDisplay() {
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'bg-panelLight dark:bg-panelDark relative flex flex-col overflow-hidden rounded-xl transition-all duration-300',
|
||||
'bg-panelLight dark:bg-panelDark relative flex flex-col overflow-hidden rounded-xl duration-300',
|
||||
isMobile ? 'h-full' : 'h-full',
|
||||
!isMobile && !isFullscreen && 'rounded-r-lg',
|
||||
isFullscreen ? 'fixed inset-0 z-50' : '',
|
||||
@@ -831,52 +794,6 @@ export function ThreadDisplay() {
|
||||
onClick={handleClose}
|
||||
className="hidden md:flex"
|
||||
/>
|
||||
{/* <ThreadSubject subject={emailData.latest?.subject} /> */}
|
||||
<div className="dark:bg-iconDark/20 relative h-3 w-0.5 rounded-full bg-[#E7E7E7]" />{' '}
|
||||
<div className="flex items-center gap-1">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={handlePrevious}
|
||||
className="inline-flex h-7 w-7 items-center justify-center gap-1 overflow-hidden rounded-md hover:bg-white md:hidden dark:hover:bg-[#313131]"
|
||||
>
|
||||
<ChevronLeft className="fill-iconLight dark:fill-iconDark h-3.5 w-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="bg-white dark:bg-[#313131]">
|
||||
Previous email
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<ThreadActionButton
|
||||
icon={ChevronLeft}
|
||||
label="Previous email"
|
||||
onClick={handlePrevious}
|
||||
className="hidden md:flex"
|
||||
/>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="inline-flex h-7 w-7 items-center justify-center gap-1 overflow-hidden rounded-md hover:bg-white md:hidden dark:hover:bg-[#313131]"
|
||||
>
|
||||
<ChevronRight className="fill-iconLight dark:fill-iconDark h-3.5 w-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="bg-white dark:bg-[#313131]">
|
||||
Next email
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<ThreadActionButton
|
||||
icon={ChevronRight}
|
||||
label="Next email"
|
||||
onClick={handleNext}
|
||||
className="hidden md:flex"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
@@ -1013,10 +930,15 @@ export function ThreadDisplay() {
|
||||
{animationsEnabled ? (
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
<motion.div
|
||||
key={id}
|
||||
key={id}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
x: navigationDirection === 'previous' ? -25 : navigationDirection === 'next' ? 25 : 0,
|
||||
x:
|
||||
navigationDirection === 'previous'
|
||||
? -25
|
||||
: navigationDirection === 'next'
|
||||
? 25
|
||||
: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
@@ -1024,10 +946,15 @@ export function ThreadDisplay() {
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
x: navigationDirection === 'previous' ? 25 : navigationDirection === 'next' ? -25 : 0,
|
||||
x:
|
||||
navigationDirection === 'previous'
|
||||
? 25
|
||||
: navigationDirection === 'next'
|
||||
? -25
|
||||
: 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.08,
|
||||
duration: 0.08,
|
||||
ease: [0.4, 0, 0.2, 1],
|
||||
}}
|
||||
onAnimationComplete={handleAnimationComplete}
|
||||
@@ -1084,19 +1011,16 @@ interface MessageListProps {
|
||||
isMobile: boolean;
|
||||
}
|
||||
|
||||
const MessageList = ({
|
||||
messages,
|
||||
isFullscreen,
|
||||
totalReplies,
|
||||
allThreadAttachments,
|
||||
mode,
|
||||
const MessageList = ({
|
||||
messages,
|
||||
isFullscreen,
|
||||
totalReplies,
|
||||
allThreadAttachments,
|
||||
mode,
|
||||
activeReplyId,
|
||||
isMobile
|
||||
isMobile,
|
||||
}: MessageListProps) => (
|
||||
<ScrollArea
|
||||
className={cn('flex-1', isMobile ? 'h-[calc(100%-1px)]' : 'h-full')}
|
||||
type="auto"
|
||||
>
|
||||
<ScrollArea className={cn('flex-1', isMobile ? 'h-[calc(100%-1px)]' : 'h-full')} type="auto">
|
||||
<div className="pb-4">
|
||||
{(messages || []).map((message, index) => {
|
||||
const isLastMessage = index === messages.length - 1;
|
||||
@@ -1105,10 +1029,7 @@ const MessageList = ({
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
className={cn(
|
||||
'transition-all duration-200',
|
||||
index > 0 && 'border-border border-t',
|
||||
)}
|
||||
className={cn('duration-200', index > 0 && 'border-border border-t')}
|
||||
>
|
||||
<MailDisplay
|
||||
emailData={message}
|
||||
|
||||
@@ -186,7 +186,7 @@ export function Navigation() {
|
||||
<span className="ml-1 hidden lg:inline">GitHub</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<Star className="relative top-px size-4 fill-gray-400 transition-all duration-300 group-hover:fill-yellow-400 group-hover:drop-shadow-[0_0_8px_rgba(250,204,21,0.6)]" />
|
||||
<Star className="relative top-px size-4 fill-gray-400 duration-300 group-hover:fill-yellow-400 group-hover:drop-shadow-[0_0_8px_rgba(250,204,21,0.6)]" />
|
||||
<AnimatedNumber value={stars} className="font-medium text-white" />
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -35,7 +35,7 @@ export function ThemeToggle({ className = '', showLabel = false }: ThemeTogglePr
|
||||
return (
|
||||
<button
|
||||
onClick={handleThemeToggle}
|
||||
className={`flex items-center rounded-md p-2 text-gray-600 transition-all hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white ${className}`}
|
||||
className={`flex items-center rounded-md p-2 text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white ${className}`}
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{theme === 'dark' ? <MoonIcon className="opacity-60" /> : <SunIcon className="opacity-60" />}
|
||||
|
||||
@@ -22,7 +22,7 @@ const AccordionTrigger = React.forwardRef<
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||
'flex flex-1 items-center justify-between py-4 font-medium hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -40,7 +40,7 @@ const AccordionContent = React.forwardRef<
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm transition-all"
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn('pb-4 pt-0', className)}>{children}</div>
|
||||
|
||||
@@ -7,27 +7,31 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader } from '@/components/ui/sidebar';
|
||||
import { navigationConfig, bottomNavItems } from '@/config/navigation';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useSession } from '@/lib/auth-client';
|
||||
|
||||
import { useTRPC } from '@/providers/query-provider';
|
||||
import { useSidebar } from '@/components/ui/sidebar';
|
||||
import { CreateEmail } from '../create/create-email';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { PencilCompose, X } from '../icons/icons';
|
||||
import { useBilling } from '@/hooks/use-billing';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useSession } from '@/lib/auth-client';
|
||||
import { useAIFullScreen } from './ai-sidebar';
|
||||
import { useStats } from '@/hooks/use-stats';
|
||||
import { useLocation } from 'react-router';
|
||||
|
||||
import { m } from '@/paraglide/messages';
|
||||
import { FOLDERS } from '@/lib/utils';
|
||||
import { Video } from 'lucide-react';
|
||||
import { NavUser } from './nav-user';
|
||||
import { NavMain } from './nav-main';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const { isPro, isLoading } = useBilling();
|
||||
const trpc = useTRPC();
|
||||
const { mutateAsync: createMeet } = useMutation(trpc.meet.create.mutationOptions());
|
||||
const [showUpgrade, setShowUpgrade] = useState(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('hideUpgradeCard') !== 'true';
|
||||
@@ -75,6 +79,19 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const showComposeButton = currentSection === 'mail';
|
||||
const { state } = useSidebar();
|
||||
|
||||
const handleCreateMeet = async () => {
|
||||
try {
|
||||
const {
|
||||
data: { id },
|
||||
} = await createMeet();
|
||||
navigator.clipboard.writeText(`https://meet.0.email/${id}`);
|
||||
toast.success('Meeting linked copied to clipboard');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error('Failed to create meeting');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!isFullScreen && (
|
||||
@@ -89,8 +106,16 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
{session && <NavUser />}
|
||||
|
||||
{showComposeButton && (
|
||||
<div>
|
||||
<ComposeButton />
|
||||
<div className="flex gap-1">
|
||||
<div className="w-[80%]">
|
||||
<ComposeButton />
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCreateMeet}
|
||||
className="hover:bg-muted-foreground/10 inline-flex h-8 w-[20%] items-center justify-center gap-1 overflow-hidden rounded-lg border bg-white px-1.5 dark:border-none dark:bg-[#313131]"
|
||||
>
|
||||
<Video className="text-muted-foreground h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</SidebarHeader>
|
||||
|
||||
@@ -10,10 +10,7 @@ const InputOTP = React.forwardRef<
|
||||
>(({ className, containerClassName, ...props }, ref) => (
|
||||
<OTPInput
|
||||
ref={ref}
|
||||
containerClassName={cn(
|
||||
'flex items-center gap-2 has-disabled:opacity-50',
|
||||
containerClassName,
|
||||
)}
|
||||
containerClassName={cn('flex items-center gap-2 has-disabled:opacity-50', containerClassName)}
|
||||
className={cn('disabled:cursor-not-allowed', className)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -39,7 +36,7 @@ const InputOTPSlot = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-input relative flex h-10 w-10 items-center justify-center border-y border-r text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md',
|
||||
'border-input relative flex h-10 w-10 items-center justify-center border-y border-r text-sm first:rounded-l-md first:border-l last:rounded-r-md',
|
||||
isActive && 'ring-ring ring-offset-background z-10 ring-2',
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -13,7 +13,7 @@ const Progress = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="bg-primary h-full w-full flex-1 transition-all"
|
||||
className="bg-primary h-full w-full flex-1"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
|
||||
@@ -39,7 +39,7 @@ const Sidebar = React.forwardRef<
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'text-sidebar-foreground bg-sidebar dark:bg-sidebar flex h-full w-(--sidebar-width) flex-col',
|
||||
'text-sidebar-foreground bg-sidebar dark:bg-sidebar w-(--sidebar-width) flex h-full flex-col',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
@@ -86,7 +86,7 @@ const Sidebar = React.forwardRef<
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
className={cn(
|
||||
'relative h-svh w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-in-out',
|
||||
'w-(--sidebar-width) relative h-svh bg-transparent transition-[width] duration-200 ease-in-out',
|
||||
'group-data-[collapsible=offcanvas]:w-0',
|
||||
'group-data-[side=right]:rotate-180',
|
||||
variant === 'floating' || variant === 'inset'
|
||||
@@ -96,7 +96,7 @@ const Sidebar = React.forwardRef<
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-in-out md:flex',
|
||||
'w-(--sidebar-width) fixed inset-y-0 z-10 hidden h-svh transition-[left,right,width] duration-200 ease-in-out md:flex',
|
||||
side === 'left'
|
||||
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
|
||||
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
||||
@@ -110,7 +110,7 @@ const Sidebar = React.forwardRef<
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
className="group-data-[variant=floating]:border-sidebar-border bg-sidebar dark:bg-sidebar flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow transition-all duration-300 ease-in-out overflow-hidden"
|
||||
className="group-data-[variant=floating]:border-sidebar-border bg-sidebar dark:bg-sidebar flex h-full w-full flex-col overflow-hidden duration-300 ease-in-out group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
@@ -160,7 +160,7 @@ const SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<'bu
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-in-out after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
|
||||
'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 ease-in-out after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
|
||||
'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
|
||||
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
||||
'group-data-[collapsible=offcanvas]:hover:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
|
||||
@@ -276,7 +276,7 @@ const SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="group"
|
||||
className={cn('relative flex w-full min-w-0 flex-col pt-0! md:p-2', className)}
|
||||
className={cn('pt-0! relative flex w-full min-w-0 flex-col md:p-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -524,7 +524,7 @@ const SidebarMenuSkeleton = React.forwardRef<
|
||||
>
|
||||
{showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
|
||||
<Skeleton
|
||||
className="h-4 max-w-(--skeleton-width) flex-1"
|
||||
className="max-w-(--skeleton-width) h-4 flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
|
||||
@@ -27,7 +27,7 @@ const TabsTrigger = React.forwardRef<
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center whitespace-nowrap rounded-sm px-2 py-px text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm',
|
||||
'ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center whitespace-nowrap rounded-sm px-2 py-px text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -91,6 +91,8 @@ export type ZeroEnv = {
|
||||
VECTORIZE: VectorizeIndex;
|
||||
VECTORIZE_MESSAGE: VectorizeIndex;
|
||||
DEV_PROXY: string;
|
||||
MEET_AUTH_HEADER: string;
|
||||
MEET_API_URL: string;
|
||||
};
|
||||
|
||||
const env = _env as ZeroEnv;
|
||||
|
||||
@@ -507,17 +507,19 @@ const getCounts = async (connectionId: string): Promise<CountResult[]> => {
|
||||
*/
|
||||
export const sendDoState = async (connectionId: string) => {
|
||||
try {
|
||||
const registry = await getRegistryClient(connectionId);
|
||||
const agent = await getZeroSocketAgent(connectionId);
|
||||
const size = await getDatabaseSize(connectionId);
|
||||
const counts = await getCounts(connectionId);
|
||||
const [registry, agent, size, counts] = await Promise.all([
|
||||
getRegistryClient(connectionId),
|
||||
getZeroSocketAgent(connectionId),
|
||||
getDatabaseSize(connectionId),
|
||||
getCounts(connectionId),
|
||||
]);
|
||||
const shards = await listShards(registry);
|
||||
return await agent.broadcastChatMessage({
|
||||
return agent.broadcastChatMessage({
|
||||
type: OutgoingMessageType.Do_State,
|
||||
isSyncing: false,
|
||||
syncingFolders: ['inbox'],
|
||||
storageSize: size,
|
||||
counts: counts,
|
||||
counts,
|
||||
shards: shards.length,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -143,14 +143,19 @@ export async function countThreads(db: DB): Promise<number> {
|
||||
return result.count;
|
||||
}
|
||||
|
||||
export async function countThreadsByLabel(db: DB, labelId: string): Promise<number> {
|
||||
const [result] = await db
|
||||
.select({ count: count() })
|
||||
.from(threads)
|
||||
.innerJoin(threadLabels, eq(threads.id, threadLabels.threadId))
|
||||
.where(eq(threadLabels.labelId, labelId));
|
||||
export async function countThreadsByLabels(
|
||||
db: DB,
|
||||
labelIds: string[],
|
||||
): Promise<{ labelId: string; count: number }[]> {
|
||||
if (labelIds.length === 0) return [];
|
||||
|
||||
return result.count;
|
||||
const results = await db
|
||||
.select({ labelId: threadLabels.labelId, count: count() })
|
||||
.from(threadLabels)
|
||||
.where(inArray(threadLabels.labelId, labelIds))
|
||||
.groupBy(threadLabels.labelId);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function createThreadLabel(
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import {
|
||||
countThreads,
|
||||
countThreadsByLabel,
|
||||
countThreadsByLabels,
|
||||
deleteSpamThreads,
|
||||
get,
|
||||
getThreadLabels,
|
||||
@@ -323,7 +323,6 @@ export class ZeroDriver extends DurableObject<ZeroEnv> {
|
||||
transfer = new Transfer(this);
|
||||
sql: SqlStorage;
|
||||
private db: DB;
|
||||
// private foldersInSync: Map<string, boolean> = new Map();
|
||||
private syncThreadsInProgress: Map<string, boolean> = new Map();
|
||||
private driver: MailManager | null = null;
|
||||
private agent: DurableObjectStub<ZeroAgent> | null = null;
|
||||
@@ -339,7 +338,6 @@ export class ZeroDriver extends DurableObject<ZeroEnv> {
|
||||
this.name = name;
|
||||
await this.ctx.blockConcurrencyWhile(async () => {
|
||||
await this.setupAuth();
|
||||
// await this.syncFolders();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -349,22 +347,6 @@ export class ZeroDriver extends DurableObject<ZeroEnv> {
|
||||
|
||||
async isSyncing(): Promise<boolean> {
|
||||
return false;
|
||||
// try {
|
||||
// const coordinatorInstance = await this.env.SYNC_THREADS_COORDINATOR_WORKFLOW.get(`${this.name}-inbox-coordinator`);
|
||||
// const coordinatorStatus = (await coordinatorInstance.status()).status;
|
||||
// if (['running', 'queued', 'waiting'].includes(coordinatorStatus)) {
|
||||
// return true;
|
||||
// }
|
||||
// } catch {
|
||||
// }
|
||||
|
||||
// try {
|
||||
// const workflowInstance = await this.env.SYNC_THREADS_WORKFLOW.get(`${this.name}-inbox`);
|
||||
// const status = (await workflowInstance.status()).status;
|
||||
// return ['running', 'queued', 'waiting'].includes(status);
|
||||
// } catch {
|
||||
// return false;
|
||||
// }
|
||||
}
|
||||
|
||||
async getAllSubjects() {
|
||||
@@ -846,13 +828,14 @@ export class ZeroDriver extends DurableObject<ZeroEnv> {
|
||||
// Additional mail operations
|
||||
async count() {
|
||||
const folders = ['inbox', 'sent', 'spam', 'archive', 'trash'];
|
||||
const counts = await Promise.all(
|
||||
folders.map(async (folder) => {
|
||||
const count = await this.getFolderThreadCount(folder.toUpperCase());
|
||||
return { label: folder, count };
|
||||
}),
|
||||
const results = await countThreadsByLabels(
|
||||
this.db,
|
||||
folders.map((f) => f.toUpperCase()),
|
||||
);
|
||||
return counts;
|
||||
const resultMap = new Map(
|
||||
results.map((r: { labelId: string; count: number }) => [r.labelId, r.count]),
|
||||
);
|
||||
return folders.map((f) => ({ label: f, count: resultMap.get(f.toUpperCase()) ?? 0 }));
|
||||
}
|
||||
|
||||
private getThreadKey(threadId: string) {
|
||||
@@ -1002,22 +985,6 @@ export class ZeroDriver extends DurableObject<ZeroEnv> {
|
||||
return count || 0;
|
||||
}
|
||||
|
||||
async getFolderThreadCount(folder: string) {
|
||||
const count = await countThreadsByLabel(this.db, folder);
|
||||
return count || 0;
|
||||
}
|
||||
|
||||
// async sendDoState() {
|
||||
// const isSyncing = await this.isSyncing();
|
||||
// return this.agent?.broadcastChatMessage({
|
||||
// type: OutgoingMessageType.Do_State,
|
||||
// isSyncing,
|
||||
// syncingFolders: isSyncing ? ['inbox'] : [],
|
||||
// storageSize: this.getDatabaseSize(),
|
||||
// counts: await this.count(),
|
||||
// });
|
||||
// }
|
||||
|
||||
async inboxRag(query: string) {
|
||||
if (!this.env.AUTORAG_ID) {
|
||||
console.warn('[inboxRag] AUTORAG_ID not configured - RAG search disabled');
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
* Licensed to Zero Email Inc. under one or more contributor license agreements.
|
||||
* You may not use this file except in compliance with the Apache License, Version 2.0 (the "License").
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* Reuse or distribution of this file requires a license from Zero Email Inc.
|
||||
*/
|
||||
|
||||
import type { IGetThreadResponse } from '../lib/driver/types';
|
||||
import { workflowFunctions } from './workflow-functions';
|
||||
import { shouldGenerateDraft } from './index';
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
* Licensed to Zero Email Inc. under one or more contributor license agreements.
|
||||
* You may not use this file except in compliance with the Apache License, Version 2.0 (the "License").
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* Reuse or distribution of this file requires a license from Zero Email Inc.
|
||||
*/
|
||||
import {
|
||||
SummarizeMessage,
|
||||
ReSummarizeThread,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { type inferRouterInputs, type inferRouterOutputs } from '@trpc/server';
|
||||
import { cookiePreferencesRouter } from './routes/cookies';
|
||||
import { connectionsRouter } from './routes/connections';
|
||||
import { categoriesRouter } from './routes/categories';
|
||||
import { templatesRouter } from './routes/templates';
|
||||
import { shortcutRouter } from './routes/shortcut';
|
||||
import { settingsRouter } from './routes/settings';
|
||||
import { getContext } from 'hono/context-storage';
|
||||
@@ -9,13 +11,12 @@ import { labelsRouter } from './routes/label';
|
||||
import { notesRouter } from './routes/notes';
|
||||
import { brainRouter } from './routes/brain';
|
||||
import { userRouter } from './routes/user';
|
||||
import { meetRouter } from './routes/meet';
|
||||
import { mailRouter } from './routes/mail';
|
||||
import { bimiRouter } from './routes/bimi';
|
||||
import type { HonoContext } from '../ctx';
|
||||
import { aiRouter } from './routes/ai';
|
||||
import { router } from './trpc';
|
||||
import { categoriesRouter } from './routes/categories';
|
||||
import { templatesRouter } from './routes/templates';
|
||||
|
||||
export const appRouter = router({
|
||||
ai: aiRouter,
|
||||
@@ -32,6 +33,7 @@ export const appRouter = router({
|
||||
settings: settingsRouter,
|
||||
user: userRouter,
|
||||
templates: templatesRouter,
|
||||
meet: meetRouter,
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
||||
39
apps/server/src/trpc/routes/meet.ts
Normal file
39
apps/server/src/trpc/routes/meet.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { activeDriverProcedure, router } from '../trpc';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { env } from '../../env';
|
||||
|
||||
type MeetResponse = {
|
||||
success: boolean;
|
||||
data: {
|
||||
created_at: string;
|
||||
id: string;
|
||||
is_large: boolean;
|
||||
live_stream_on_start: boolean;
|
||||
persist_chat: boolean;
|
||||
record_on_start: boolean;
|
||||
status: string;
|
||||
summarize_on_end: boolean;
|
||||
updated_at: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const meetRouter = router({
|
||||
create: activeDriverProcedure.mutation(async () => {
|
||||
const AuthHeader = env.MEET_AUTH_HEADER;
|
||||
const response = await fetch(env.MEET_API_URL + '/meetings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: AuthHeader,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(await response.text());
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to create meeting' });
|
||||
}
|
||||
|
||||
const data = await response.json<MeetResponse>();
|
||||
return data;
|
||||
}),
|
||||
});
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
* Licensed to Zero Email Inc. under one or more contributor license agreements.
|
||||
* You may not use this file except in compliance with the Apache License, Version 2.0 (the "License").
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* Reuse or distribution of this file requires a license from Zero Email Inc.
|
||||
*/
|
||||
import { WorkflowEntrypoint, WorkflowStep } from 'cloudflare:workers';
|
||||
import { connectionToDriver } from '../lib/server-utils';
|
||||
import type { WorkflowEvent } from 'cloudflare:workers';
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
* Licensed to Zero Email Inc. under one or more contributor license agreements.
|
||||
* You may not use this file except in compliance with the Apache License, Version 2.0 (the "License").
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* Reuse or distribution of this file requires a license from Zero Email Inc.
|
||||
*/
|
||||
import { getZeroAgent, connectionToDriver, sendDoState } from '../lib/server-utils';
|
||||
import { WorkflowEntrypoint, WorkflowStep } from 'cloudflare:workers';
|
||||
import type { WorkflowEvent } from 'cloudflare:workers';
|
||||
|
||||
@@ -172,11 +172,12 @@
|
||||
"DROP_AGENT_TABLES": "false",
|
||||
"THREAD_SYNC_MAX_COUNT": "60",
|
||||
"THREAD_SYNC_LOOP": "false",
|
||||
"DISABLE_WORKFLOWS": "false",
|
||||
"DISABLE_WORKFLOWS": "true",
|
||||
"AUTORAG_ID": "",
|
||||
"USE_OPENAI": "true",
|
||||
"CLOUDFLARE_ACCOUNT_ID": "",
|
||||
"CLOUDFLARE_API_TOKEN": "",
|
||||
"MEET_AUTH_HEADER": "",
|
||||
},
|
||||
"kv_namespaces": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user