Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 60 additions & 39 deletions apps/desktop/src/components/finder/views/contact-view.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { RiCornerDownLeftLine } from "@remixicon/react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Building2, CircleMinus, FileText, Pencil, Plus, SearchIcon, TrashIcon, User } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";

import { commands as dbCommands } from "@hypr/plugin-db";
import { type Human, type Organization } from "@hypr/plugin-db";
Expand All @@ -20,25 +20,35 @@ interface ContactViewProps {
}

export function ContactView({ userId, initialPersonId, initialOrgId }: ContactViewProps) {
// Simple state initialization - handles both normal and deep-link cases
const [selectedOrganization, setSelectedOrganization] = useState<string | null>(initialOrgId || null);
const [selectedPerson, setSelectedPerson] = useState<string | null>(initialPersonId || null);

const [editingPerson, setEditingPerson] = useState<string | null>(null);
const [editingOrg, setEditingOrg] = useState<string | null>(null);
const [showNewOrg, setShowNewOrg] = useState(false);
const queryClient = useQueryClient();

// Load organizations once and keep cached (global data)
const { data: organizations = [] } = useQuery({
queryKey: ["organizations", userId],
queryKey: ["organizations"],
queryFn: () => dbCommands.listOrganizations(null),
});

const { data: people = [] } = useQuery({
queryKey: ["organization-members", selectedOrganization],
queryFn: () =>
selectedOrganization ? dbCommands.listOrganizationMembers(selectedOrganization) : Promise.resolve([]),
enabled: !!selectedOrganization,
// Load user's own profile
const { data: userProfile } = useQuery({
queryKey: ["user-profile", userId],
queryFn: async () => {
try {
return await dbCommands.getHuman(userId);
} catch (error) {
console.error("Error fetching user profile:", error);
return null;
}
},
});

// Load all people once and keep cached (user-specific data)
const { data: allPeople = [] } = useQuery({
queryKey: ["all-people", userId],
queryFn: async () => {
Expand All @@ -50,14 +60,32 @@ export function ContactView({ userId, initialPersonId, initialOrgId }: ContactVi
return [];
}
},
enabled: !selectedOrganization,
});

// Merge user profile with all people, ensuring user's own profile is included
const allPeopleWithUser = React.useMemo(() => {
if (!userProfile) {
return allPeople;
}

// Check if user is already in the list
const userInList = allPeople.some(person => person.id === userId);

if (userInList) {
return allPeople;
} else {
// Add user profile to the beginning of the list
return [userProfile, ...allPeople];
}
}, [allPeople, userProfile, userId]);

// Person sessions - only runs when person is selected
const { data: personSessions = [] } = useQuery({
queryKey: ["person-sessions", selectedPerson, userId],
queryKey: ["person-sessions", selectedPerson || "none"],
queryFn: async () => {
// Safety check - this should never run when selectedPerson is null
if (!selectedPerson) {
return [];
throw new Error("Query should not run when selectedPerson is null");
}

const sessions = await dbCommands.listSessions({
Expand All @@ -81,35 +109,20 @@ export function ContactView({ userId, initialPersonId, initialOrgId }: ContactVi

return sessionsWithPerson;
},
enabled: !!selectedPerson,
enabled: selectedPerson !== null && selectedPerson !== undefined && selectedPerson !== "",
gcTime: 5 * 60 * 1000,
staleTime: 30 * 1000,
});

const displayPeople = selectedOrganization ? people : allPeople;
// Client-side filtering: filter allPeopleWithUser by organization when one is selected
const displayPeople = selectedOrganization
? allPeopleWithUser.filter(person => person.organization_id === selectedOrganization)
: allPeopleWithUser;

const selectedPersonData = displayPeople.find(p => p.id === selectedPerson);

// Handle initial person selection
useEffect(() => {
if (initialPersonId && allPeople.length > 0) {
const person = allPeople.find(p => p.id === initialPersonId);
if (person) {
setSelectedPerson(initialPersonId);
if (person.organization_id) {
setSelectedOrganization(person.organization_id);
}
}
}
}, [initialPersonId, allPeople]);

// Handle initial organization selection
useEffect(() => {
if (initialOrgId && organizations.length > 0) {
const org = organizations.find(o => o.id === initialOrgId);
if (org) {
setSelectedOrganization(initialOrgId);
}
}
}, [initialOrgId, organizations]);
// Simple initialization - no complex useEffects needed
// Initial state is set directly in useState above

const handleSessionClick = (sessionId: string) => {
const path = { to: "/app/note/$id", params: { id: sessionId } } as const satisfies LinkProps;
Expand All @@ -134,7 +147,7 @@ export function ContactView({ userId, initialPersonId, initialOrgId }: ContactVi
mutationFn: (personId: string) => dbCommands.deleteHuman(personId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["all-people"] });
queryClient.invalidateQueries({ queryKey: ["organization-members"] });
queryClient.invalidateQueries({ queryKey: ["user-profile"] });

if (selectedPerson === selectedPersonData?.id) {
setSelectedPerson(null);
Expand Down Expand Up @@ -241,7 +254,7 @@ export function ContactView({ userId, initialPersonId, initialOrgId }: ContactVi
linkedin_username: null,
}).then(() => {
queryClient.invalidateQueries({ queryKey: ["all-people"] });
queryClient.invalidateQueries({ queryKey: ["organization-members"] });
queryClient.invalidateQueries({ queryKey: ["user-profile"] });
setSelectedPerson(newPersonId);
setEditingPerson(newPersonId);
});
Expand All @@ -268,7 +281,12 @@ export function ContactView({ userId, initialPersonId, initialOrgId }: ContactVi
</span>
</div>
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{person.full_name || person.email || "Unnamed"}</div>
<div className="font-medium truncate flex items-center gap-1">
{person.full_name || person.email || "Unnamed"}
{person.id === userId && (
<span className="text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded-full">You</span>
)}
</div>
{person.email && person.full_name && (
<div className="text-xs text-neutral-500 truncate">{person.email}</div>
)}
Expand Down Expand Up @@ -303,8 +321,11 @@ export function ContactView({ userId, initialPersonId, initialOrgId }: ContactVi
<div className="flex-1">
<div className="flex items-start justify-between">
<div>
<h2 className="text-lg font-semibold">
<h2 className="text-lg font-semibold flex items-center gap-2">
{selectedPersonData.full_name || "Unnamed Contact"}
{selectedPersonData.id === userId && (
<span className="text-sm bg-blue-100 text-blue-700 px-2 py-1 rounded-full">You</span>
)}
</h2>
{selectedPersonData.job_title && (
<p className="text-sm text-neutral-600">{selectedPersonData.job_title}</p>
Expand Down Expand Up @@ -433,7 +454,7 @@ function EditPersonForm({
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["all-people"] });
queryClient.invalidateQueries({ queryKey: ["organization-members"] });
queryClient.invalidateQueries({ queryKey: ["user-profile"] });
onSave();
},
onError: () => {
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/components/right-panel/hooks/useChatLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,14 @@ export function useChatLogic({
return;
}

if (messages.length >= 2 && !getLicense.data?.valid) {
if (messages.length >= 4 && !getLicense.data?.valid) {
if (userId) {
await analyticsCommands.event({
event: "pro_license_required_chat",
distinct_id: userId,
});
}
await message("2 messages are allowed per session for free users.", {
await message("2 messages are allowed per conversation for free users.", {
title: "Pro License Required",
kind: "info",
});
Expand Down
46 changes: 12 additions & 34 deletions apps/desktop/src/components/workspace-calendar/event-card.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,49 @@
import { Trans } from "@lingui/react/macro";
import { useQuery } from "@tanstack/react-query";
import type { LinkProps } from "@tanstack/react-router";
import { format } from "date-fns";
import { Calendar, FileText, Pen } from "lucide-react";
import { useMemo, useState } from "react";

import { useHypr } from "@/contexts";
import { openURL } from "@/utils/shell";
import type { Event } from "@hypr/plugin-db";
import { commands as dbCommands } from "@hypr/plugin-db";
import type { Event, Human, Session } from "@hypr/plugin-db";
import { commands as windowsCommands } from "@hypr/plugin-windows";
import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover";

export function EventCard({
event,
showTime = false,
session = null,
participants = [],
}: {
event: Event;
showTime?: boolean;
session?: Session | null;
participants?: Human[];
}) {
const { userId } = useHypr();
const session = useQuery({
queryKey: ["event-session", event.id],
queryFn: async () => dbCommands.getSession({ calendarEventId: event.id }),
});

const participants = useQuery({
queryKey: ["participants", session.data?.id],
queryFn: async () => {
if (!session.data?.id) {
return [];
}
const participants = await dbCommands.sessionListParticipants(session.data.id);
return participants.sort((a, b) => {
if (a.is_user && !b.is_user) {
return 1;
}
if (!a.is_user && b.is_user) {
return -1;
}
return 0;
});
},
enabled: !!session.data?.id,
});

const participantsPreview = useMemo(() => {
const count = participants.data?.length ?? 0;
const count = participants?.length ?? 0;
if (count === 0) {
return null;
}

return participants.data?.map(participant => {
return participants?.map(participant => {
if (participant.id === userId && !participant.full_name) {
return "You";
}
return participant.full_name ?? "??";
});
}, [participants.data, userId]);
}, [participants, userId]);

const [open, setOpen] = useState(false);

const handleClick = () => {
setOpen(false);

if (session.data) {
const id = session.data.id;
if (session) {
const id = session.id;
const url = { to: "/app/note/$id", params: { id } } as const satisfies LinkProps;
windowsCommands.windowShow({ type: "main" }).then(() => {
windowsCommands.windowEmitNavigate({ type: "main" }, {
Expand Down Expand Up @@ -129,15 +107,15 @@ export function EventCard({
</div>
)}

{session.data
{session
? (
<div
className="flex items-center gap-2 px-2 py-1 bg-neutral-50 border border-neutral-200 rounded-md cursor-pointer hover:bg-neutral-100 transition-colors"
onClick={handleClick}
>
<FileText className="size-3 text-neutral-600 flex-shrink-0" />
<div className="text-xs font-medium text-neutral-800 truncate">
{session.data.title || "Untitled Note"}
{session.title || "Untitled Note"}
</div>
</div>
)
Expand Down
Loading
Loading