Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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,
},
} satisfies Record<ConfigKey, ConfigDefinition>;
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