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
15 changes: 12 additions & 3 deletions apps/web/core/components/home/user-greetings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,49 @@
* See the LICENSE file for details.
*/

import { observer } from "mobx-react";
// plane types
import { useTranslation } from "@plane/i18n";
import type { IUser } from "@plane/types";
// plane ui
// hooks
import { useCurrentTime } from "@/hooks/use-current-time";
import { useDisplayTimezone } from "@/hooks/use-display-timezone";

export interface IUserGreetingsView {
user: IUser;
}

export function UserGreetingsView(props: IUserGreetingsView) {
export const UserGreetingsView = observer(function UserGreetingsView(props: IUserGreetingsView) {
const { user } = props;
// current time hook
const { currentTime } = useCurrentTime();
// store hooks
const { t } = useTranslation();
// resolved display timezone: user preference (UTC treated as unset) -> workspace timezone -> browser local
const timeZone = useDisplayTimezone(user?.user_timezone);

// all fields below must use the same timezone, otherwise the greeting and
// the displayed time can belong to different timezones
const hour = new Intl.DateTimeFormat("en-US", {
timeZone,
hour12: false,
hour: "numeric",
}).format(currentTime);

const date = new Intl.DateTimeFormat("en-US", {
timeZone,
month: "short",
day: "numeric",
}).format(currentTime);

const weekDay = new Intl.DateTimeFormat("en-US", {
timeZone,
weekday: "long",
}).format(currentTime);

const timeString = new Intl.DateTimeFormat("en-US", {
timeZone: user?.user_timezone,
timeZone,
hour12: false, // Use 24-hour format
hour: "2-digit",
minute: "2-digit",
Expand All @@ -58,4 +67,4 @@ export function UserGreetingsView(props: IUserGreetingsView) {
</h5>
</div>
);
}
});
14 changes: 10 additions & 4 deletions apps/web/core/components/profile/time.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,27 @@
* See the LICENSE file for details.
*/

import { observer } from "mobx-react";
// hooks
import { useCurrentTime } from "@/hooks/use-current-time";
import { useDisplayTimezone } from "@/hooks/use-display-timezone";

type Props = {
timeZone: string | undefined;
};

export function ProfileSidebarTime(props: Props) {
export const ProfileSidebarTime = observer(function ProfileSidebarTime(props: Props) {
const { timeZone } = props;
// current time hook
const { currentTime } = useCurrentTime();
// resolved display timezone: user preference (UTC treated as unset) -> workspace timezone -> browser local
const resolvedTimeZone = useDisplayTimezone(timeZone);
// when both are unset the browser's local timezone is used; resolve its name for display
const displayTimeZone = resolvedTimeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;

// Create a date object for the current time in the specified timezone
const formatter = new Intl.DateTimeFormat("en-US", {
timeZone: timeZone,
timeZone: displayTimeZone,
hour12: false, // Use 24-hour format
hour: "2-digit",
minute: "2-digit",
Expand All @@ -27,7 +33,7 @@ export function ProfileSidebarTime(props: Props) {

return (
<span>
{timeString} <span className="text-secondary">{timeZone}</span>
{timeString} <span className="text-secondary">{displayTimeZone}</span>
</span>
);
}
});
15 changes: 12 additions & 3 deletions apps/web/core/components/user/user-greetings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,49 @@
* See the LICENSE file for details.
*/

import { observer } from "mobx-react";
// plane types
import { useTranslation } from "@plane/i18n";
// hooks
import type { IUser } from "@plane/types";
import { useCurrentTime } from "@/hooks/use-current-time";
import { useDisplayTimezone } from "@/hooks/use-display-timezone";
// types

export interface IUserGreetingsView {
user: IUser;
}

export function UserGreetingsView(props: IUserGreetingsView) {
export const UserGreetingsView = observer(function UserGreetingsView(props: IUserGreetingsView) {
const { user } = props;
// current time hook
const { currentTime } = useCurrentTime();
// store hooks
const { t } = useTranslation();
// resolved display timezone: user preference (UTC treated as unset) -> workspace timezone -> browser local
const timeZone = useDisplayTimezone(user?.user_timezone);

// all fields below must use the same timezone, otherwise the greeting and
// the displayed time can belong to different timezones
const hour = new Intl.DateTimeFormat("en-US", {
timeZone,
hour12: false,
hour: "numeric",
}).format(currentTime);

const date = new Intl.DateTimeFormat("en-US", {
timeZone,
month: "short",
day: "numeric",
}).format(currentTime);

const weekDay = new Intl.DateTimeFormat("en-US", {
timeZone,
weekday: "long",
}).format(currentTime);

const timeString = new Intl.DateTimeFormat("en-US", {
timeZone: user?.user_timezone,
timeZone,
hour12: false, // Use 24-hour format
hour: "2-digit",
minute: "2-digit",
Expand All @@ -58,4 +67,4 @@ export function UserGreetingsView(props: IUserGreetingsView) {
</h5>
</div>
);
}
});
39 changes: 39 additions & 0 deletions apps/web/core/hooks/use-display-timezone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/

// hooks
import { useWorkspace } from "@/hooks/store/use-workspace";

/**
* Resolve the timezone to use when displaying times in the UI.
*
* Resolution order:
* 1. The user's preferred timezone (`user_timezone`). `"UTC"` is treated as
* "not set": it is the database default for users who never picked a
* timezone, and we cannot distinguish that from a user who deliberately
* chose UTC. The workspace timezone is the better fallback for those users.
* 2. The current workspace's timezone, which the organization has explicitly
* declared in workspace settings.
* 3. `undefined`, which makes the Intl APIs fall back to the browser's local
* timezone.
*/
export const resolveDisplayTimezone = (
userTimezone: string | undefined,
workspaceTimezone: string | undefined
): string | undefined => {
if (userTimezone && userTimezone !== "UTC") return userTimezone;
if (workspaceTimezone) return workspaceTimezone;
return undefined;
};

/**
* Hook variant of `resolveDisplayTimezone` that reads the current workspace
* from the store.
*/
export const useDisplayTimezone = (userTimezone: string | undefined): string | undefined => {
const { currentWorkspace } = useWorkspace();
return resolveDisplayTimezone(userTimezone, currentWorkspace?.timezone);
};