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
3 changes: 3 additions & 0 deletions templates/clips/actions/list-recordings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ export default defineAction({
// Lifecycle view filters
if (args.view === "trash") {
whereClauses.push(isNotNull(schema.recordings.trashedAt));
if (orgId) {
whereClauses.push(eq(schema.recordings.organizationId, orgId));
}
} else {
whereClauses.push(isNull(schema.recordings.trashedAt));
if (args.view === "archive") {
Expand Down
1 change: 1 addition & 0 deletions templates/clips/actions/search-meetings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const MEETING_COLUMNS = {
source: schema.meetings.source,
platform: schema.meetings.platform,
trashedAt: schema.meetings.trashedAt,
ownerEmail: schema.meetings.ownerEmail,
} as const;

type MeetingRow = Pick<
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { describe, expect, it } from "vitest";

import { formatParticipantNames } from "./meeting-history-row";
import { formatOwnerHint, formatParticipantNames } from "./meeting-history-row";

const viewer = "dev@local.test";
const fakeT = (key: string, options?: Record<string, unknown>) => {
if (key === "meetingDetail.recordedBy") return `Recorded by ${options?.name}`;

Check warning on line 7 in templates/clips/app/components/meetings/meeting-history-row.test.ts

View workflow job for this annotation

GitHub Actions / Lint & format

typescript(restrict-template-expressions)

Invalid type used in template literal expression.
if (key === "meetingDetail.me") return "Me";
return key;
};

describe("formatParticipantNames", () => {
it("names the one other person on a 1:1", () => {
Expand Down Expand Up @@ -32,8 +37,9 @@
).toBe("Jason, Elaine & 2 others");
});

// A solo note renders a document icon instead, so the subtitle must go empty
// rather than telling the reader they were in a meeting with themselves.
// The attendee subtitle must go empty rather than telling the reader they
// were in a meeting with themselves; `formatOwnerHint` is what still
// surfaces the owner's avatar/name on a solo note.
it("returns nothing when the viewer is the only attendee", () => {
expect(
formatParticipantNames([{ email: viewer, name: "Dev" }], viewer),
Expand Down Expand Up @@ -68,3 +74,23 @@
);
});
});

describe("formatOwnerHint", () => {
it("names the owner of a shared meeting", () => {
expect(formatOwnerHint("sidharth@builder.io", viewer, fakeT)).toBe(
"Recorded by sidharth",
);
});

it("shows the owner even on the viewer's own meetings, as 'Me'", () => {
expect(formatOwnerHint(viewer, viewer, fakeT)).toBe("Recorded by Me");
expect(formatOwnerHint(" DEV@Local.TEST ", viewer, fakeT)).toBe(
"Recorded by Me",
);
});

it("returns nothing without an owner", () => {
expect(formatOwnerHint(null, viewer, fakeT)).toBe("");
expect(formatOwnerHint(undefined, viewer, fakeT)).toBe("");
});
});
33 changes: 32 additions & 1 deletion templates/clips/app/components/meetings/meeting-history-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface MeetingHistoryItem {
actualEnd?: string | null;
createdAt?: string | null;
participants?: AttendeeStackParticipant[];
ownerEmail?: string | null;
}

function formatTime(iso?: string | null): string {
Expand Down Expand Up @@ -56,6 +57,28 @@ export function formatParticipantNames(
return `${names.slice(0, 2).join(", ")} & ${names.length - 2} others`;
}

/**
* A meeting's owner — who actually recorded it in Clips — isn't necessarily
* on the attendee list (an ad-hoc note has none at all), and two attendees on
* the same call can each hold their own copy. Unlike the attendee subtitle,
* this is shown unconditionally, including the viewer's own meetings, so
* ownership is never ambiguous once a meeting is shared.
*/
export function formatOwnerHint(
ownerEmail: string | null | undefined,
viewerEmail: string | null | undefined,
t: ReturnType<typeof useT>,
): string {
const owner = ownerEmail?.trim();
if (!owner) return "";
const viewer = viewerEmail?.trim().toLowerCase();
const name =
viewer && owner.toLowerCase() === viewer
? t("meetingDetail.me")
: owner.replace(/@.*$/, "");
return t("meetingDetail.recordedBy", { name });
}

export function MeetingHistoryRow({
meeting,
snippet,
Expand All @@ -66,11 +89,17 @@ export function MeetingHistoryRow({
const t = useT();
const { session } = useSession();
const participants = meeting.participants ?? [];
const subtitle =
const ownerHint = formatOwnerHint(meeting.ownerEmail, session?.email, t);
const primaryText =
snippet?.trim() || formatParticipantNames(participants, session?.email);
const subtitle = [primaryText, ownerHint].filter(Boolean).join(" · ");
const time = formatTime(
meeting.actualStart ?? meeting.scheduledStart ?? meeting.createdAt,
);
const soloOwnerAvatar: AttendeeStackParticipant[] =
participants.length === 0 && ownerHint && meeting.ownerEmail
? [{ email: meeting.ownerEmail }]
: [];

return (
<NavLink
Expand All @@ -79,6 +108,8 @@ export function MeetingHistoryRow({
>
{participants.length > 0 ? (
<AttendeeStack participants={participants} size="md" max={2} />
) : soloOwnerAvatar.length > 0 ? (
<AttendeeStack participants={soloOwnerAvatar} size="md" max={1} />
) : (
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
<IconFileText className="h-3.5 w-3.5" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function RecordingOptionsMenu({
<Button
variant="ghost"
size="icon"
className="-mx-1.5 h-auto w-auto shrink-0 px-0.5 py-1.5"
className="h-auto w-auto shrink-0 px-0.5 py-1.5"
aria-label={t("deleteRecordingMenu.clipOptions")}
>
<IconDotsVertical className="h-4 w-4" />
Expand Down
1 change: 1 addition & 0 deletions templates/clips/app/i18n/ar-SA.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ const messages = {
},
meetingDetail: {
untitledMeeting: "لقاء بلا عنوان",
recordedBy: "سجّله {{name}}",
unassigned: "غير معين",
them: "هم",
me: "أنا",
Expand Down
1 change: 1 addition & 0 deletions templates/clips/app/i18n/de-DE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ const messages = {
},
meetingDetail: {
untitledMeeting: "Treffen ohne Titel",
recordedBy: "Aufgezeichnet von {{name}}",
unassigned: "Nicht zugewiesen",
them: "Ihnen",
me: "Mich",
Expand Down
1 change: 1 addition & 0 deletions templates/clips/app/i18n/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ const messages = {
},
meetingDetail: {
untitledMeeting: "Untitled meeting",
recordedBy: "Recorded by {{name}}",
unassigned: "Unassigned",
them: "Them",
me: "Me",
Expand Down
1 change: 1 addition & 0 deletions templates/clips/app/i18n/es-ES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ const messages = {
},
meetingDetail: {
untitledMeeting: "Reunión sin título",
recordedBy: "Grabado por {{name}}",
unassigned: "No asignado",
them: "A ellos",
me: "A mí",
Expand Down
1 change: 1 addition & 0 deletions templates/clips/app/i18n/fr-FR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ const messages = {
},
meetingDetail: {
untitledMeeting: "Réunion sans titre",
recordedBy: "Enregistré par {{name}}",
unassigned: "Non attribué",
them: "Eux",
me: "Moi",
Expand Down
1 change: 1 addition & 0 deletions templates/clips/app/i18n/hi-IN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ const messages = {
},
meetingDetail: {
untitledMeeting: "शीर्षकहीन बैठक",
recordedBy: "{{name}} द्वारा रिकॉर्ड किया गया",
unassigned: "सौंपे नहीं गए",
them: "उन्हें",
me: "मुझे",
Expand Down
1 change: 1 addition & 0 deletions templates/clips/app/i18n/ja-JP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ const messages = {
},
meetingDetail: {
untitledMeeting: "無題の会議",
recordedBy: "{{name}} が記録",
unassigned: "未割り当て",
them: "彼ら",
me: "自分",
Expand Down
1 change: 1 addition & 0 deletions templates/clips/app/i18n/ko-KR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ const messages = {
},
meetingDetail: {
untitledMeeting: "제목 없는 회의",
recordedBy: "{{name}} 님이 기록함",
unassigned: "할당되지 않음",
them: "그들을",
me: "나",
Expand Down
1 change: 1 addition & 0 deletions templates/clips/app/i18n/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ const messages = {
},
meetingDetail: {
untitledMeeting: "Reunião sem título",
recordedBy: "Gravado por {{name}}",
unassigned: "Não atribuído",
them: "Eles",
me: "Meu",
Expand Down
1 change: 1 addition & 0 deletions templates/clips/app/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ const messages = {
},
meetingDetail: {
untitledMeeting: "无标题会议",
recordedBy: "由 {{name}} 记录",
unassigned: "未分配",
them: "他们",
me: "我",
Expand Down
1 change: 1 addition & 0 deletions templates/clips/app/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ const messages = {
},
meetingDetail: {
untitledMeeting: "無標題會議",
recordedBy: "由 {{name}} 記錄",
unassigned: "未指派",
them: "他們",
me: "我",
Expand Down
1 change: 1 addition & 0 deletions templates/clips/app/routes/_app.meetings._index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@
joinUrl?: string | null;
platform?: string | null;
transcriptStatus?:
| "pending"

Check warning on line 86 in templates/clips/app/routes/_app.meetings._index.tsx

View workflow job for this annotation

GitHub Actions / Lint & format

typescript(no-redundant-type-constituents)

pending is overridden by string in this union type.
| "ready"

Check warning on line 87 in templates/clips/app/routes/_app.meetings._index.tsx

View workflow job for this annotation

GitHub Actions / Lint & format

typescript(no-redundant-type-constituents)

ready is overridden by string in this union type.
| "failed"

Check warning on line 88 in templates/clips/app/routes/_app.meetings._index.tsx

View workflow job for this annotation

GitHub Actions / Lint & format

typescript(no-redundant-type-constituents)

failed is overridden by string in this union type.
| "in_progress"

Check warning on line 89 in templates/clips/app/routes/_app.meetings._index.tsx

View workflow job for this annotation

GitHub Actions / Lint & format

typescript(no-redundant-type-constituents)

in_progress is overridden by string in this union type.
| string
| null;
summaryPreview?: string | null;
Expand All @@ -94,6 +94,7 @@
userNotesMd?: string | null;
source?: "calendar" | "adhoc" | "manual";
participants?: AttendeeStackParticipant[];
ownerEmail?: string | null;
Comment thread
builder-io-integration[bot] marked this conversation as resolved.
}

interface SearchMeetingResult extends Meeting {
Expand All @@ -115,7 +116,7 @@

interface CalendarAccount {
id: string;
provider: "google" | "icloud" | "microsoft" | string;

Check warning on line 119 in templates/clips/app/routes/_app.meetings._index.tsx

View workflow job for this annotation

GitHub Actions / Lint & format

typescript(no-redundant-type-constituents)

icloud is overridden by string in this union type.

Check warning on line 119 in templates/clips/app/routes/_app.meetings._index.tsx

View workflow job for this annotation

GitHub Actions / Lint & format

typescript(no-redundant-type-constituents)

google is overridden by string in this union type.
displayName?: string | null;
email?: string | null;
status?: "connected" | "needs-reauth" | "disconnected" | string;
Expand Down Expand Up @@ -831,7 +832,7 @@
const map = new Map<string, string | null | undefined>();
for (const m of searchResults) map.set(m.id, m.snippet);
return map;
}, [searchResults]);

Check warning on line 835 in templates/clips/app/routes/_app.meetings._index.tsx

View workflow job for this annotation

GitHub Actions / Lint & format

react-hooks(exhaustive-deps)

React hook useMemo depends on `searchResults`, which changes every render

const calendarAccounts = accounts.data?.accounts ?? [];
const hasCalendar = calendarAccounts.length > 0;
Expand Down
49 changes: 43 additions & 6 deletions templates/clips/desktop/src-tauri/src/clips/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1275,11 +1275,13 @@ pub async fn resize_popover(app: AppHandle, height: f64, width: Option<f64>) ->
return Ok(());
}
if let Some(w) = app.get_webview_window("popover") {
let max_logical_height = w
let monitor = w
.current_monitor()
.ok()
.flatten()
.or_else(|| w.primary_monitor().ok().flatten())
.or_else(|| w.primary_monitor().ok().flatten());
let max_logical_height = monitor
.as_ref()
.map(|monitor| {
let scale = monitor.scale_factor().max(1.0);
((monitor.size().height as f64) / scale
Expand All @@ -1288,16 +1290,39 @@ pub async fn resize_popover(app: AppHandle, height: f64, width: Option<f64>) ->
.clamp(260.0, 820.0)
})
.unwrap_or(820.0);
// Same idea as height: a monitor narrower than the requested width
// (e.g. settings' 720) must not let the window grow past the screen
// edge, since `position_popover`'s x-clamp can only slide a
// too-wide window, not shrink it back onto the display.
let max_logical_width = monitor
.map(|monitor| {
let scale = monitor.scale_factor().max(1.0);
((monitor.size().width as f64) / scale - 16.0 - POPOVER_SHADOW_GUTTER_LOGICAL * 2.0)
Comment thread
shomix marked this conversation as resolved.
.clamp(320.0, 960.0)
})
.unwrap_or(960.0);
let clamped = height.clamp(200.0, max_logical_height);
let width = width.unwrap_or(320.0).clamp(320.0, 960.0);
let width = width
.unwrap_or(320.0)
.clamp(320.0, max_logical_width.max(320.0));
let (window_width, window_height) = popover_window_size_logical(width, clamped);
let _ = w.set_size(tauri::Size::Logical(tauri::LogicalSize::new(
window_width,
window_height,
)));
// Re-anchor to the tray icon so the window doesn't drift below the
// bottom of the monitor after a growth.
position_popover(&app, &w);
// bottom of the monitor after a growth. Pass the size we just asked
// for rather than letting `position_popover` re-read `outer_size()`:
// macOS doesn't always commit `set_size()` synchronously, so a
// stale (pre-resize) read here would center/clamp against the old,
// narrower width and let the window balloon past the screen edge
// once the real resize lands a moment later.
let target_scale = w.scale_factor().unwrap_or(1.0).max(1.0);
let target_physical = PhysicalSize::new(
(window_width * target_scale).round() as u32,
(window_height * target_scale).round() as u32,
);
position_popover_with_size(&app, &w, target_physical);
}
Ok(())
}
Expand Down Expand Up @@ -2668,13 +2693,25 @@ pub fn toggle_popover(app: &AppHandle) {
}

pub fn position_popover(app: &AppHandle, window: &WebviewWindow) {
let win_size = window.outer_size().unwrap_or(PhysicalSize::new(360, 440));
position_popover_with_size(app, window, win_size);
}

/// Same as `position_popover`, but takes the window's size explicitly instead
/// of querying `window.outer_size()`. Callers that just called `set_size()`
/// must pass the size they requested — see the comment at that call site in
/// `resize_popover` for why re-querying there is unsafe.
pub fn position_popover_with_size(
app: &AppHandle,
window: &WebviewWindow,
win_size: PhysicalSize<u32>,
) {
// If we have a recent tray icon rect, anchor the popover's top edge just
// below the icon and center it horizontally on the icon — same feel as
// Loom / Raycast / 1Password.
let anchor = app.state::<TrayAnchor>();
let tray_rect = anchor.0.lock().ok().and_then(|g| *g);

let win_size: PhysicalSize<u32> = window.outer_size().unwrap_or(PhysicalSize::new(360, 440));
// IMPORTANT: `current_monitor()` returns None when the window is offscreen
// (we park it at 99999,99999 on boot to hide the initial flash). Fall back
// to the primary monitor so we can still position correctly on first show.
Expand Down
12 changes: 12 additions & 0 deletions templates/clips/desktop/src-tauri/src/tray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,17 @@ fn build_menu_with_meetings(
)?;
let devtools_item =
MenuItem::with_id(app, "devtools", "Toggle DevTools", true, Some("Cmd+Alt+I"))?;
let version_item = MenuItem::with_id(
app,
"version",
format!("Clips v{}", env!("CARGO_PKG_VERSION")),
false,
None::<&str>,
)?;
let quit_item = MenuItem::with_id(app, "quit", "Quit Clips", true, None::<&str>)?;
let separator = PredefinedMenuItem::separator(app)?;
let separator2 = PredefinedMenuItem::separator(app)?;
let separator3 = PredefinedMenuItem::separator(app)?;
let menu = Menu::with_items(
app,
&[
Expand All @@ -147,6 +156,9 @@ fn build_menu_with_meetings(
&paste_last_dictation_item,
&region_guides_item,
&devtools_item,
&separator2,
&version_item,
&separator3,
&quit_item,
],
)?;
Expand Down
Loading
Loading