feat(workspace): add hidden contacts filter and remove calendar scene swap
This commit is contained in:
@@ -41,6 +41,7 @@ type CalendarView = "day" | "week" | "month" | "year" | "agenda";
|
||||
type SortMode = "name" | "lastContact";
|
||||
type PeopleLeftMode = "contacts" | "calendar";
|
||||
type PeopleSortMode = "name" | "lastContact" | "company" | "country";
|
||||
type PeopleVisibilityMode = "all" | "hidden";
|
||||
type DocumentSortMode = "updatedAt" | "title" | "owner";
|
||||
|
||||
type FeedCard = {
|
||||
@@ -2397,7 +2398,6 @@ const calendarZoomOverlay = ref<{ active: boolean } & CalendarRect>({
|
||||
});
|
||||
const calendarZoomGhost = ref<CalendarZoomGhost | null>(null);
|
||||
const calendarZoomBusy = ref(false);
|
||||
const calendarSceneMasked = ref(false);
|
||||
const calendarCameraState = ref({
|
||||
active: false,
|
||||
left: 0,
|
||||
@@ -2412,6 +2412,8 @@ let calendarWheelLockUntil = 0;
|
||||
let calendarZoomPrimeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let calendarZoomPrimeLastAt = 0;
|
||||
const CALENDAR_ZOOM_DURATION_MS = 2400;
|
||||
const CALENDAR_ZOOM_FOCUS_MS = 1400;
|
||||
const CALENDAR_ZOOM_REVEAL_MS = Math.max(500, CALENDAR_ZOOM_DURATION_MS - CALENDAR_ZOOM_FOCUS_MS);
|
||||
const CALENDAR_ZOOM_PRIME_STEPS = 2;
|
||||
const CALENDAR_ZOOM_PRIME_MAX_SCALE = 1.05;
|
||||
const CALENDAR_ZOOM_PRIME_RESET_MS = 900;
|
||||
@@ -2762,11 +2764,15 @@ function waitCalendarZoomTransition() {
|
||||
});
|
||||
}
|
||||
|
||||
async function animateCalendarZoomIn(sourceElement: HTMLElement | null, ghost: CalendarZoomGhost, apply: () => void) {
|
||||
async function animateCalendarZoomIn(
|
||||
sourceElement: HTMLElement | null,
|
||||
ghost: CalendarZoomGhost,
|
||||
apply: () => void,
|
||||
resolveRevealTarget?: () => HTMLElement | null,
|
||||
) {
|
||||
clearCalendarZoomPrime();
|
||||
calendarZoomBusy.value = true;
|
||||
clearCalendarZoomOverlay();
|
||||
calendarSceneMasked.value = false;
|
||||
try {
|
||||
calendarZoomGhost.value = ghost;
|
||||
const fromRect = getElementRectInScene(sourceElement) ?? fallbackZoomOriginRectInScene();
|
||||
@@ -2790,19 +2796,36 @@ async function animateCalendarZoomIn(sourceElement: HTMLElement | null, ghost: C
|
||||
left: cameraTarget.left,
|
||||
top: cameraTarget.top,
|
||||
scale: cameraTarget.scale,
|
||||
durationMs: CALENDAR_ZOOM_DURATION_MS,
|
||||
durationMs: CALENDAR_ZOOM_FOCUS_MS,
|
||||
};
|
||||
await waitCalendarCameraTransition();
|
||||
// Freeze the filled block frame, then swap level while scene is masked.
|
||||
// This keeps the "zoom into block -> reveal next grid inside" sequence.
|
||||
calendarSceneMasked.value = true;
|
||||
await nextAnimationFrame();
|
||||
apply();
|
||||
await nextTick();
|
||||
const revealTargetElement = resolveRevealTarget ? resolveRevealTarget() : sourceElement;
|
||||
const revealTargetRect = getElementRectInScene(revealTargetElement) ?? fallbackZoomOriginRectInScene();
|
||||
const revealTarget = revealTargetRect ? cameraTransformForRect(revealTargetRect) : null;
|
||||
if (revealTarget) {
|
||||
calendarCameraState.value = {
|
||||
active: true,
|
||||
left: revealTarget.left,
|
||||
top: revealTarget.top,
|
||||
scale: revealTarget.scale,
|
||||
durationMs: 0,
|
||||
};
|
||||
await nextTick();
|
||||
await nextAnimationFrame();
|
||||
}
|
||||
calendarCameraState.value = {
|
||||
active: true,
|
||||
left: 0,
|
||||
top: 0,
|
||||
scale: 1,
|
||||
durationMs: CALENDAR_ZOOM_REVEAL_MS,
|
||||
};
|
||||
await waitCalendarCameraTransition();
|
||||
} finally {
|
||||
await resetCalendarCamera();
|
||||
calendarZoomGhost.value = null;
|
||||
calendarSceneMasked.value = false;
|
||||
calendarZoomBusy.value = false;
|
||||
}
|
||||
}
|
||||
@@ -2813,15 +2836,11 @@ async function animateCalendarZoomOut(apply: () => void, resolveTarget: () => HT
|
||||
clearCalendarZoomOverlay();
|
||||
try {
|
||||
calendarZoomGhost.value = zoomGhostForCurrentView();
|
||||
calendarSceneMasked.value = true;
|
||||
apply();
|
||||
await nextTick();
|
||||
const targetRect = getElementRectInScene(resolveTarget()) ?? fallbackZoomOriginRectInScene();
|
||||
const cameraStart = targetRect ? cameraTransformForRect(targetRect) : null;
|
||||
if (!cameraStart) {
|
||||
calendarSceneMasked.value = false;
|
||||
return;
|
||||
}
|
||||
if (!cameraStart) return;
|
||||
calendarCameraState.value = {
|
||||
active: true,
|
||||
left: cameraStart.left,
|
||||
@@ -2832,8 +2851,6 @@ async function animateCalendarZoomOut(apply: () => void, resolveTarget: () => HT
|
||||
await nextTick();
|
||||
await nextAnimationFrame();
|
||||
calendarSceneRef.value?.getBoundingClientRect();
|
||||
calendarSceneMasked.value = false;
|
||||
await nextAnimationFrame();
|
||||
calendarCameraState.value = {
|
||||
active: true,
|
||||
left: 0,
|
||||
@@ -2845,7 +2862,6 @@ async function animateCalendarZoomOut(apply: () => void, resolveTarget: () => HT
|
||||
} finally {
|
||||
await resetCalendarCamera();
|
||||
calendarZoomGhost.value = null;
|
||||
calendarSceneMasked.value = false;
|
||||
calendarZoomBusy.value = false;
|
||||
}
|
||||
}
|
||||
@@ -2895,7 +2911,7 @@ async function zoomInCalendar(event?: Event) {
|
||||
if (maybePrimeWheelZoom(wheelEvent, calendarPrimeMonthToken(monthIndex))) return;
|
||||
await animateCalendarZoomIn(sourceElement, zoomGhostForMonth(monthIndex), () => {
|
||||
openYearMonth(monthIndex);
|
||||
});
|
||||
}, () => queryCalendarElement(`[data-calendar-month-index="${monthIndex}"]`));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2914,6 +2930,9 @@ async function zoomInCalendar(event?: Event) {
|
||||
() => {
|
||||
openWeekView(anchorDayKey);
|
||||
},
|
||||
() =>
|
||||
queryCalendarElement(`[data-calendar-week-start-key="${rowStartKey}"]`) ??
|
||||
queryCalendarElement(`[data-calendar-day-key="${anchorDayKey}"]`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -2924,7 +2943,7 @@ async function zoomInCalendar(event?: Event) {
|
||||
if (maybePrimeWheelZoom(wheelEvent, calendarPrimeDayToken(dayAnchor))) return;
|
||||
await animateCalendarZoomIn(sourceElement, zoomGhostForDay(dayAnchor), () => {
|
||||
openDayView(dayAnchor);
|
||||
});
|
||||
}, () => queryCalendarElement(`[data-calendar-day-key="${dayAnchor}"]`));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2935,6 +2954,7 @@ async function zoomToMonth(monthIndex: number) {
|
||||
() => {
|
||||
openYearMonth(monthIndex);
|
||||
},
|
||||
() => queryCalendarElement(`[data-calendar-month-index="${monthIndex}"]`),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3366,6 +3386,7 @@ function openDocumentsTab(push = false) {
|
||||
const peopleListMode = ref<"contacts" | "deals">("contacts");
|
||||
const peopleSearch = ref("");
|
||||
const peopleSortMode = ref<PeopleSortMode>("lastContact");
|
||||
const peopleVisibilityMode = ref<PeopleVisibilityMode>("all");
|
||||
const brokenAvatarByContactId = ref<Record<string, boolean>>({});
|
||||
const peopleSortOptions: Array<{ value: PeopleSortMode; label: string }> = [
|
||||
{ value: "lastContact", label: "Last contact" },
|
||||
@@ -3373,6 +3394,10 @@ const peopleSortOptions: Array<{ value: PeopleSortMode; label: string }> = [
|
||||
{ value: "company", label: "Company" },
|
||||
{ value: "country", label: "Country" },
|
||||
];
|
||||
const peopleVisibilityOptions: Array<{ value: PeopleVisibilityMode; label: string }> = [
|
||||
{ value: "all", label: "All" },
|
||||
{ value: "hidden", label: "Hidden" },
|
||||
];
|
||||
const selectedDealId = ref(deals.value[0]?.id ?? "");
|
||||
const selectedDealStepsExpanded = ref(false);
|
||||
|
||||
@@ -3412,26 +3437,54 @@ const commThreads = computed(() => {
|
||||
map.get(item.contact)?.push(item);
|
||||
}
|
||||
|
||||
return contacts.value
|
||||
.map((contact) => {
|
||||
const items = map.get(contact.name) ?? [];
|
||||
const contactById = new Map(contacts.value.map((contact) => [contact.id, contact]));
|
||||
const inboxesByContactId = new Map<string, ContactInbox[]>();
|
||||
for (const inbox of contactInboxes.value) {
|
||||
if (!inboxesByContactId.has(inbox.contactId)) {
|
||||
inboxesByContactId.set(inbox.contactId, []);
|
||||
}
|
||||
inboxesByContactId.get(inbox.contactId)?.push(inbox);
|
||||
}
|
||||
|
||||
const contactIds = new Set<string>([
|
||||
...contacts.value.map((contact) => contact.id),
|
||||
...contactInboxes.value.map((inbox) => inbox.contactId),
|
||||
]);
|
||||
|
||||
return [...contactIds]
|
||||
.map((contactId) => {
|
||||
const contact = contactById.get(contactId);
|
||||
const inboxes = inboxesByContactId.get(contactId) ?? [];
|
||||
const contactName = contact?.name ?? inboxes[0]?.contactName ?? "";
|
||||
const items = map.get(contactName) ?? [];
|
||||
const last = items[items.length - 1];
|
||||
const channels = [...new Set([...contact.channels, ...items.map((item) => item.channel)])] as CommItem["channel"][];
|
||||
const channels = [
|
||||
...new Set([
|
||||
...(contact?.channels ?? []),
|
||||
...inboxes.map((inbox) => inbox.channel),
|
||||
...items.map((item) => item.channel),
|
||||
]),
|
||||
] as CommItem["channel"][];
|
||||
const inboxFallbackLast = inboxes
|
||||
.map((inbox) => inbox.lastMessageAt || inbox.updatedAt)
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
.at(-1);
|
||||
|
||||
return {
|
||||
id: contact.id,
|
||||
contact: contact.name,
|
||||
avatar: contact.avatar,
|
||||
company: contact.company,
|
||||
country: contact.country,
|
||||
location: contact.location,
|
||||
id: contactId,
|
||||
contact: contactName,
|
||||
avatar: contact?.avatar ?? "",
|
||||
company: contact?.company ?? "",
|
||||
country: contact?.country ?? "",
|
||||
location: contact?.location ?? "",
|
||||
channels,
|
||||
lastAt: last?.at ?? contact.lastContactAt,
|
||||
lastAt: last?.at ?? contact?.lastContactAt ?? inboxFallbackLast ?? "",
|
||||
lastText: last?.text ?? "No messages yet",
|
||||
items,
|
||||
};
|
||||
})
|
||||
.filter((thread) => thread.items.length > 0)
|
||||
.filter((thread) => thread.contact)
|
||||
.sort((a, b) => b.lastAt.localeCompare(a.lastAt));
|
||||
});
|
||||
|
||||
@@ -3442,8 +3495,12 @@ const peopleContactList = computed(() => {
|
||||
const haystack = [item.contact, item.company, item.country, item.location].join(" ").toLowerCase();
|
||||
return haystack.includes(query);
|
||||
});
|
||||
const byVisibility = list.filter((item) => {
|
||||
if (peopleVisibilityMode.value === "all") return true;
|
||||
return threadInboxes(item).some((inbox) => inbox.isHidden);
|
||||
});
|
||||
|
||||
return list.sort((a, b) => {
|
||||
return byVisibility.sort((a, b) => {
|
||||
if (peopleSortMode.value === "name") return a.contact.localeCompare(b.contact);
|
||||
if (peopleSortMode.value === "company") return a.company.localeCompare(b.company);
|
||||
if (peopleSortMode.value === "country") return a.country.localeCompare(b.country);
|
||||
@@ -4853,7 +4910,6 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
:on-calendar-hierarchy-wheel="onCalendarHierarchyWheel"
|
||||
:set-calendar-scene-ref="setCalendarSceneRef"
|
||||
:normalized-calendar-view="normalizedCalendarView"
|
||||
:calendar-scene-masked="calendarSceneMasked"
|
||||
:calendar-scene-transform-style="calendarSceneTransformStyle"
|
||||
:on-calendar-scene-mouse-leave="onCalendarSceneMouseLeave"
|
||||
:calendar-view="calendarView"
|
||||
@@ -4894,6 +4950,8 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
:people-search="peopleSearch"
|
||||
:people-sort-options="peopleSortOptions"
|
||||
:people-sort-mode="peopleSortMode"
|
||||
:people-visibility-options="peopleVisibilityOptions"
|
||||
:people-visibility-mode="peopleVisibilityMode"
|
||||
:people-contact-list="peopleContactList"
|
||||
:selected-comm-thread-id="selectedCommThreadId"
|
||||
:is-review-highlighted-contact="isReviewHighlightedContact"
|
||||
@@ -4915,6 +4973,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
:on-people-list-mode-change="(mode) => { peopleListMode = mode; }"
|
||||
:on-people-search-input="(value) => { peopleSearch = value; }"
|
||||
:on-people-sort-mode-change="(mode) => { peopleSortMode = mode; }"
|
||||
:on-people-visibility-mode-change="(mode) => { peopleVisibilityMode = mode; }"
|
||||
/>
|
||||
|
||||
<div class="hidden h-12 items-center justify-between gap-2 border-b border-base-300 px-3 md:flex md:col-span-2">
|
||||
|
||||
@@ -53,7 +53,6 @@ defineProps<{
|
||||
onCalendarHierarchyWheel: (event: WheelEvent) => void;
|
||||
setCalendarSceneRef: (element: HTMLDivElement | null) => void;
|
||||
normalizedCalendarView: string;
|
||||
calendarSceneMasked: boolean;
|
||||
calendarSceneTransformStyle: Record<string, string>;
|
||||
onCalendarSceneMouseLeave: () => void;
|
||||
calendarView: string;
|
||||
@@ -170,7 +169,6 @@ defineProps<{
|
||||
:class="[
|
||||
'calendar-scene',
|
||||
normalizedCalendarView === 'day' ? 'cursor-zoom-out' : 'cursor-zoom-in',
|
||||
calendarSceneMasked ? 'calendar-scene-hidden' : '',
|
||||
]"
|
||||
:style="calendarSceneTransformStyle"
|
||||
@mouseleave="onCalendarSceneMouseLeave"
|
||||
@@ -351,10 +349,6 @@ defineProps<{
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.calendar-scene-hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.calendar-scene.cursor-zoom-in,
|
||||
.calendar-scene.cursor-zoom-in * {
|
||||
cursor: zoom-in;
|
||||
|
||||
@@ -6,6 +6,8 @@ defineProps<{
|
||||
peopleSearch: string;
|
||||
peopleSortOptions: Array<{ value: string; label: string }>;
|
||||
peopleSortMode: string;
|
||||
peopleVisibilityOptions: Array<{ value: string; label: string }>;
|
||||
peopleVisibilityMode: string;
|
||||
peopleContactList: any[];
|
||||
selectedCommThreadId: string;
|
||||
isReviewHighlightedContact: (contactId: string) => boolean;
|
||||
@@ -26,6 +28,7 @@ defineProps<{
|
||||
onPeopleListModeChange: (mode: PeopleListMode) => void;
|
||||
onPeopleSearchInput: (value: string) => void;
|
||||
onPeopleSortModeChange: (mode: string) => void;
|
||||
onPeopleVisibilityModeChange: (mode: string) => void;
|
||||
}>();
|
||||
|
||||
function onSearchInput(event: Event) {
|
||||
@@ -91,6 +94,17 @@ function onSearchInput(event: Event) {
|
||||
<span>{{ option.label }}</span>
|
||||
<span v-if="peopleSortMode === option.value">✓</span>
|
||||
</button>
|
||||
<div class="my-1 h-px bg-base-300/70" />
|
||||
<p class="px-2 pb-1 text-[11px] font-semibold uppercase tracking-wide text-base-content/55">Filter contacts</p>
|
||||
<button
|
||||
v-for="option in peopleVisibilityOptions"
|
||||
:key="`people-visibility-${option.value}`"
|
||||
class="btn btn-ghost btn-sm w-full justify-between"
|
||||
@click="onPeopleVisibilityModeChange(option.value)"
|
||||
>
|
||||
<span>{{ option.label }}</span>
|
||||
<span v-if="peopleVisibilityMode === option.value">✓</span>
|
||||
</button>
|
||||
</template>
|
||||
<p v-else class="px-2 py-1 text-xs text-base-content/60">Deals are sorted by title.</p>
|
||||
</div>
|
||||
@@ -192,7 +206,7 @@ function onSearchInput(event: Event) {
|
||||
</button>
|
||||
|
||||
<p v-if="peopleListMode === 'contacts' && peopleContactList.length === 0" class="px-1 py-2 text-xs text-base-content/55">
|
||||
No contacts found.
|
||||
{{ peopleVisibilityMode === 'hidden' ? 'No hidden contacts found.' : 'No contacts found.' }}
|
||||
</p>
|
||||
<p v-if="peopleListMode === 'deals' && peopleDealList.length === 0" class="px-1 py-2 text-xs text-base-content/55">
|
||||
No deals found.
|
||||
|
||||
Reference in New Issue
Block a user