Skip to content
Open
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 apps/desktop/src/settings/general/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ function useSettingsForm() {
ignored_platforms: row.ignored_platforms
? JSON.stringify(row.ignored_platforms)
: undefined,
included_platforms: row.included_platforms
? JSON.stringify(row.included_platforms)
: undefined,
ignored_recurring_series: row.ignored_recurring_series
? JSON.stringify(row.ignored_recurring_series)
: undefined,
Expand Down
89 changes: 66 additions & 23 deletions apps/desktop/src/settings/general/notification-app-options.test.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,77 @@
import { describe, expect, test } from "vitest";

import { getIgnoredAppOptions } from "./notification-app-options";

describe("getIgnoredAppOptions", () => {
test("returns installed app matches for partial searches", () => {
const options = getIgnoredAppOptions({
allInstalledApps: [
{ id: "us.zoom.xos", name: "Zoom Workplace" },
{ id: "com.tinyspeck.slackmacgap", name: "Slack" },
],
import {
getEffectiveIgnoredPlatformIds,
getMicDetectionAppOptions,
orderIgnoredPlatformIdsForWrap,
} from "./notification-app-options";

describe("getMicDetectionAppOptions", () => {
test("hides default ignored apps from dropdown matches", () => {
const options = getMicDetectionAppOptions({
allInstalledApps: [{ id: "com.microsoft.VSCode", name: "VS Code" }],
ignoredPlatforms: [],
includedPlatforms: [],
inputValue: "code",
defaultIgnoredBundleIds: ["com.microsoft.VSCode"],
});

expect(options).toEqual([]);
});

test("shows explicitly included default apps as excludable again", () => {
const options = getMicDetectionAppOptions({
allInstalledApps: [{ id: "com.microsoft.VSCode", name: "VS Code" }],
ignoredPlatforms: [],
inputValue: "zoom",
defaultIgnoredBundleIds: [],
includedPlatforms: ["com.microsoft.VSCode"],
inputValue: "code",
defaultIgnoredBundleIds: ["com.microsoft.VSCode"],
});

expect(options).toEqual([{ id: "us.zoom.xos", name: "Zoom Workplace" }]);
expect(options).toEqual([{ id: "com.microsoft.VSCode", name: "VS Code" }]);
});
});

describe("getEffectiveIgnoredPlatformIds", () => {
test("includes installed default ignored apps unless explicitly included", () => {
expect(
getEffectiveIgnoredPlatformIds({
installedApps: [
{ id: "com.microsoft.VSCode", name: "VS Code" },
{ id: "us.zoom.xos", name: "Zoom Workplace" },
],
ignoredPlatforms: [],
includedPlatforms: [],
defaultIgnoredBundleIds: ["com.microsoft.VSCode"],
}),
).toEqual(["com.microsoft.VSCode"]);

expect(
getEffectiveIgnoredPlatformIds({
installedApps: [{ id: "com.microsoft.VSCode", name: "VS Code" }],
ignoredPlatforms: [],
includedPlatforms: ["com.microsoft.VSCode"],
defaultIgnoredBundleIds: ["com.microsoft.VSCode"],
}),
).toEqual([]);
});
});

test("filters out already ignored and default ignored apps", () => {
const options = getIgnoredAppOptions({
allInstalledApps: [
{ id: "us.zoom.xos", name: "Zoom Workplace" },
{ id: "com.tinyspeck.slackmacgap", name: "Slack" },
{ id: "com.openai.chat", name: "ChatGPT" },
],
ignoredPlatforms: ["com.tinyspeck.slackmacgap"],
inputValue: "",
defaultIgnoredBundleIds: ["com.openai.chat"],
describe("orderIgnoredPlatformIdsForWrap", () => {
test("reorders chips to better fill each row", () => {
const ordered = orderIgnoredPlatformIdsForWrap({
bundleIds: ["a", "b", "c", "d"],
availableWidth: 214,
bundleIdToName: (bundleId) =>
({
a: "ABCDEFGHIJ",
b: "KLMNOPQRST",
c: "Tiny",
d: "Mini",
})[bundleId] ?? bundleId,
isDefaultIgnored: () => false,
});

expect(options).toEqual([{ id: "us.zoom.xos", name: "Zoom Workplace" }]);
expect(ordered).toEqual(["a", "d", "b", "c"]);
});
});
149 changes: 144 additions & 5 deletions apps/desktop/src/settings/general/notification-app-options.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,161 @@
import type { InstalledApp } from "@hypr/plugin-detect";

export function getIgnoredAppOptions({
function isEffectivelyIgnored({
bundleId,
ignoredPlatforms,
includedPlatforms,
defaultIgnoredBundleIds,
}: {
bundleId: string;
ignoredPlatforms: string[];
includedPlatforms: string[];
defaultIgnoredBundleIds: string[] | undefined;
}) {
const isDefaultIgnored = defaultIgnoredBundleIds?.includes(bundleId) ?? false;
const isIncluded = includedPlatforms.includes(bundleId);
const isUserIgnored = ignoredPlatforms.includes(bundleId);

return isUserIgnored || (isDefaultIgnored && !isIncluded);
}

export function getMicDetectionAppOptions({
allInstalledApps,
ignoredPlatforms,
includedPlatforms,
inputValue,
defaultIgnoredBundleIds,
}: {
allInstalledApps: InstalledApp[] | undefined;
ignoredPlatforms: string[];
includedPlatforms: string[];
inputValue: string;
defaultIgnoredBundleIds: string[] | undefined;
}) {
return (allInstalledApps ?? []).filter((app) => {
const matchesSearch = app.name
.toLowerCase()
.includes(inputValue.toLowerCase());
const notAlreadyAdded = !ignoredPlatforms.includes(app.id);
const notDefaultIgnored = !(defaultIgnoredBundleIds ?? []).includes(app.id);
return matchesSearch && notAlreadyAdded && notDefaultIgnored;
.includes(inputValue.trim().toLowerCase());
const isIgnored = isEffectivelyIgnored({
bundleId: app.id,
ignoredPlatforms,
includedPlatforms,
defaultIgnoredBundleIds,
});
return matchesSearch && !isIgnored;
});
}

export function getEffectiveIgnoredPlatformIds({
installedApps,
ignoredPlatforms,
includedPlatforms,
defaultIgnoredBundleIds,
}: {
installedApps: InstalledApp[] | undefined;
ignoredPlatforms: string[];
includedPlatforms: string[];
defaultIgnoredBundleIds: string[] | undefined;
}) {
const installedAppIds = new Set((installedApps ?? []).map((app) => app.id));
const bundleIds = new Set(ignoredPlatforms);

for (const bundleId of defaultIgnoredBundleIds ?? []) {
if (!installedAppIds.has(bundleId)) {
continue;
}

if (
isEffectivelyIgnored({
bundleId,
ignoredPlatforms,
includedPlatforms,
defaultIgnoredBundleIds,
})
) {
bundleIds.add(bundleId);
}
}

return [...bundleIds];
}

function estimateIgnoredChipWidth({
label,
isDefaultIgnored,
}: {
label: string;
isDefaultIgnored: boolean;
}) {
const labelWidth = label.length * 9;
const defaultWidth = isDefaultIgnored ? 78 : 0;

return labelWidth + defaultWidth + 44;
}

export function orderIgnoredPlatformIdsForWrap({
bundleIds,
availableWidth,
bundleIdToName,
isDefaultIgnored,
}: {
bundleIds: string[];
availableWidth: number | null;
bundleIdToName: (bundleId: string) => string;
isDefaultIgnored: (bundleId: string) => boolean;
}) {
const items = bundleIds.map((bundleId) => ({
bundleId,
label: bundleIdToName(bundleId),
estimatedWidth: estimateIgnoredChipWidth({
label: bundleIdToName(bundleId),
isDefaultIgnored: isDefaultIgnored(bundleId),
}),
}));

if (!availableWidth || availableWidth <= 0) {
return items
.sort(
(left, right) =>
right.estimatedWidth - left.estimatedWidth ||
left.label.localeCompare(right.label),
)
.map((item) => item.bundleId);
}

const rows: { items: typeof items; remainingWidth: number }[] = [];

for (const item of items.sort(
(left, right) =>
right.estimatedWidth - left.estimatedWidth ||
left.label.localeCompare(right.label),
)) {
let bestRow: { items: typeof items; remainingWidth: number } | undefined;

for (const row of rows) {
if (row.remainingWidth < item.estimatedWidth) {
continue;
}

if (
!bestRow ||
row.remainingWidth - item.estimatedWidth <
bestRow.remainingWidth - item.estimatedWidth
) {
bestRow = row;
}
}

if (bestRow) {
bestRow.items.push(item);
bestRow.remainingWidth -= item.estimatedWidth;
continue;
}

rows.push({
items: [item],
remainingWidth: Math.max(0, availableWidth - item.estimatedWidth),
});
}

return rows.flatMap((row) => row.items.map((item) => item.bundleId));
}
Loading
Loading