Skip to content
Closed
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
8 changes: 7 additions & 1 deletion apps/desktop/src/components/main/sidebar/search/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { cn } from "@hypr/utils";
import { type SearchResult } from "../../../../contexts/search/ui";
import * as main from "../../../../store/tinybase/store/main";
import { type TabInput, useTabs } from "../../../../store/zustand/tabs";
import { useTimezone } from "../../../../utils/timezone";
import { getInitials } from "../../body/contacts/shared";

export function SearchResultItem({ result }: { result: SearchResult }) {
Expand Down Expand Up @@ -141,6 +142,8 @@ function SessionSearchResultItem({
result: SearchResult;
onClick: () => void;
}) {
const timezone = useTimezone();

const displayTitle = useMemo(() => {
const sanitized = DOMPurify.sanitize(result.titleHighlighted, {
ALLOWED_TAGS: ["mark"],
Expand Down Expand Up @@ -198,7 +201,10 @@ function SessionSearchResultItem({
} else if (diffDays === 1) {
timeAgo = "Yesterday";
} else if (diffDays < 7) {
timeAgo = createdAt.toLocaleDateString("en-US", { weekday: "long" });
timeAgo = createdAt.toLocaleDateString("en-US", {
weekday: "long",
timeZone: timezone,
});
} else if (diffDays < 30) {
const weeks = Math.floor(diffDays / 7);
timeAgo = weeks === 1 ? "a week ago" : `${weeks} weeks ago`;
Expand Down
24 changes: 19 additions & 5 deletions apps/desktop/src/components/main/sidebar/timeline/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
type TimelineItem,
TimelinePrecision,
} from "../../../../utils/timeline";
import { useTimezone } from "../../../../utils/timezone";
import { InteractiveButton } from "../../../interactive-button";

export const TimelineItemComponent = memo(
Expand Down Expand Up @@ -109,6 +110,7 @@ const EventItem = memo(
const openCurrent = useTabs((state) => state.openCurrent);
const openNew = useTabs((state) => state.openNew);
const invalidateResource = useTabs((state) => state.invalidateResource);
const timezone = useTimezone();

const eventId = item.id;

Expand Down Expand Up @@ -155,8 +157,8 @@ const EventItem = memo(
const calendarId = item.data.calendar_id ?? null;
const recurrenceSeriesId = item.data.recurrence_series_id;
const displayTime = useMemo(
() => formatDisplayTime(item.data.started_at, precision),
[item.data.started_at, precision],
() => formatDisplayTime(item.data.started_at, precision, timezone),
[item.data.started_at, precision, timezone],
);

const openEvent = useCallback(
Expand Down Expand Up @@ -306,6 +308,7 @@ const SessionItem = memo(
const openCurrent = useTabs((state) => state.openCurrent);
const openNew = useTabs((state) => state.openNew);
const invalidateResource = useTabs((state) => state.invalidateResource);
const timezone = useTimezone();

const sessionId = item.id;
const title =
Expand Down Expand Up @@ -336,8 +339,12 @@ const SessionItem = memo(

const displayTime = useMemo(
() =>
formatDisplayTime(eventStartedAt ?? item.data.created_at, precision),
[eventStartedAt, item.data.created_at, precision],
formatDisplayTime(
eventStartedAt ?? item.data.created_at,
precision,
timezone,
),
[eventStartedAt, item.data.created_at, precision, timezone],
);

const handleClick = useCallback(() => {
Expand Down Expand Up @@ -399,6 +406,7 @@ const SessionItem = memo(
function formatDisplayTime(
timestamp: string | null | undefined,
precision: TimelinePrecision,
timezone: string,
): string {
const date = safeParseDate(timestamp);
if (!date) {
Expand All @@ -408,6 +416,7 @@ function formatDisplayTime(
const time = date.toLocaleTimeString([], {
hour: "numeric",
minute: "numeric",
timeZone: timezone,
});

if (precision === "time") {
Expand All @@ -416,11 +425,16 @@ function formatDisplayTime(

const sameYear = date.getFullYear() === new Date().getFullYear();
const dateStr = sameYear
? date.toLocaleDateString([], { month: "short", day: "numeric" })
? date.toLocaleDateString([], {
month: "short",
day: "numeric",
timeZone: timezone,
})
: date.toLocaleDateString([], {
month: "short",
day: "numeric",
year: "numeric",
timeZone: timezone,
});

return `${dateStr}, ${time}`;
Expand Down
13 changes: 12 additions & 1 deletion apps/desktop/src/components/settings/general/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { NotificationSettingsView } from "./notification";
import { Permissions } from "./permissions";
import { SpokenLanguagesView } from "./spoken-languages";
import { StorageSettingsView } from "./storage";
import { TimezoneView } from "./timezone";

function useSettingsForm() {
const value = useConfigValues([
Expand All @@ -27,6 +28,7 @@ function useSettingsForm() {
"ai_language",
"spoken_languages",
"current_stt_provider",
"timezone",
] as const);

const setPartialValues = settings.UI.useSetPartialValuesCallback(
Expand Down Expand Up @@ -55,6 +57,7 @@ function useSettingsForm() {
telemetry_consent: value.telemetry_consent,
ai_language: value.ai_language,
spoken_languages: value.spoken_languages,
timezone: value.timezone,
},
listeners: {
onChange: ({ formApi }) => {
Expand Down Expand Up @@ -195,7 +198,7 @@ export function SettingsApp() {

<div>
<h2 className="text-lg font-semibold font-serif mb-4">
Language & Vocabulary
Language & Region
</h2>
<div className="flex flex-col gap-6">
<form.Field name="ai_language">
Expand All @@ -222,6 +225,14 @@ export function SettingsApp() {
</>
)}
</form.Field>
<form.Field name="timezone">
{(field) => (
<TimezoneView
value={field.state.value}
onChange={(val) => field.handleChange(val)}
/>
)}
</form.Field>
</div>
</div>

Expand Down
92 changes: 92 additions & 0 deletions apps/desktop/src/components/settings/general/timezone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useMemo } from "react";

import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@hypr/ui/components/ui/select";

const COMMON_TIMEZONES = [
{ value: "Pacific/Honolulu", label: "Hawaii (HST)" },
{ value: "America/Anchorage", label: "Alaska (AKST)" },
{ value: "America/Los_Angeles", label: "Pacific Time (PST)" },
{ value: "America/Denver", label: "Mountain Time (MST)" },
{ value: "America/Chicago", label: "Central Time (CST)" },
{ value: "America/New_York", label: "Eastern Time (EST)" },
{ value: "America/Sao_Paulo", label: "Brasilia (BRT)" },
{ value: "Atlantic/Reykjavik", label: "Iceland (GMT)" },
{ value: "Europe/London", label: "London (GMT/BST)" },
{ value: "Europe/Paris", label: "Paris (CET)" },
{ value: "Europe/Berlin", label: "Berlin (CET)" },
{ value: "Europe/Moscow", label: "Moscow (MSK)" },
{ value: "Asia/Dubai", label: "Dubai (GST)" },
{ value: "Asia/Kolkata", label: "India (IST)" },
{ value: "Asia/Bangkok", label: "Bangkok (ICT)" },
{ value: "Asia/Singapore", label: "Singapore (SGT)" },
{ value: "Asia/Shanghai", label: "China (CST)" },
{ value: "Asia/Tokyo", label: "Tokyo (JST)" },
{ value: "Asia/Seoul", label: "Seoul (KST)" },
{ value: "Australia/Sydney", label: "Sydney (AEST)" },
{ value: "Pacific/Auckland", label: "Auckland (NZST)" },
];

const SYSTEM_TIMEZONE_VALUE = "system";

function getSystemTimezone(): string {
try {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
} catch {
return "UTC";
}
}

export function TimezoneView({
value,
onChange,
}: {
value: string | undefined;
onChange: (value: string | undefined) => void;
}) {
const systemTimezone = useMemo(() => getSystemTimezone(), []);

const displayValue = value ?? SYSTEM_TIMEZONE_VALUE;

const handleChange = (newValue: string) => {
if (newValue === SYSTEM_TIMEZONE_VALUE) {
onChange(undefined);
} else {
onChange(newValue);
}
};

const systemLabel = useMemo(() => {
const tz = COMMON_TIMEZONES.find((t) => t.value === systemTimezone);
return tz ? `System (${tz.label})` : `System (${systemTimezone})`;
}, [systemTimezone]);

return (
<div className="flex flex-row items-center justify-between">
<div>
<h3 className="text-sm font-medium mb-1">Timezone</h3>
<p className="text-xs text-neutral-600">
Timezone for displaying dates and times
</p>
</div>
<Select value={displayValue} onValueChange={handleChange}>
<SelectTrigger className="w-52 shadow-none focus:ring-0 focus:ring-offset-0">
<SelectValue />
</SelectTrigger>
<SelectContent className="max-h-[300px] overflow-auto">
<SelectItem value={SYSTEM_TIMEZONE_VALUE}>{systemLabel}</SelectItem>
{COMMON_TIMEZONES.map((tz) => (
<SelectItem key={tz.value} value={tz.value}>
{tz.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}
8 changes: 7 additions & 1 deletion apps/desktop/src/config/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export type ConfigKey =
| "save_recordings"
| "telemetry_consent"
| "current_llm_provider"
| "current_llm_model";
| "current_llm_model"
| "timezone";

type ConfigValueType<K extends ConfigKey> =
(typeof CONFIG_REGISTRY)[K]["default"];
Expand Down Expand Up @@ -139,4 +140,9 @@ export const CONFIG_REGISTRY = {
key: "current_llm_model",
default: undefined,
},

timezone: {
key: "timezone",
default: undefined as string | undefined,
},
} satisfies Record<ConfigKey, ConfigDefinition>;
1 change: 1 addition & 0 deletions apps/desktop/src/store/tinybase/store/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export const SETTINGS_MAPPING = {
type: "number",
path: ["billing", "pro_subscription_modal_shown_at"],
},
timezone: { type: "string", path: ["general", "timezone"] },
},
tables: {
ai_providers: {
Expand Down
49 changes: 49 additions & 0 deletions apps/desktop/src/utils/timezone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useConfigValue } from "../config/use-config";

export function getSystemTimezone(): string {
try {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
} catch {
return "UTC";
}
}

export function useTimezone(): string {
const configuredTimezone = useConfigValue("timezone");
return configuredTimezone ?? getSystemTimezone();
}

export function formatTimeWithTimezone(
date: Date,
timezone: string,
options?: Intl.DateTimeFormatOptions,
): string {
const defaultOptions: Intl.DateTimeFormatOptions = {
hour: "numeric",
minute: "numeric",
timeZone: timezone,
};
return date.toLocaleTimeString([], { ...defaultOptions, ...options });
}

export function formatDateWithTimezone(
date: Date,
timezone: string,
options?: Intl.DateTimeFormatOptions,
): string {
const defaultOptions: Intl.DateTimeFormatOptions = {
timeZone: timezone,
};
return date.toLocaleDateString([], { ...defaultOptions, ...options });
}

export function formatDateTimeWithTimezone(
date: Date,
timezone: string,
options?: Intl.DateTimeFormatOptions,
): string {
const defaultOptions: Intl.DateTimeFormatOptions = {
timeZone: timezone,
};
return date.toLocaleString([], { ...defaultOptions, ...options });
}
1 change: 1 addition & 0 deletions packages/store/src/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ export const generalSchema = z.object({
current_llm_model: z.string().optional(),
current_stt_provider: z.string().optional(),
current_stt_model: z.string().optional(),
timezone: z.string().optional(),
});

export const aiProviderSchema = z
Expand Down
Loading