Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
67461bc
feat: add private date and time display preferences
rholak Oct 19, 2026
993b54b
fix: clarify date and time preference helper text
ampagent Oct 19, 2026
30b778a
fix: apply date preferences to ban dates
ampagent Oct 21, 2026
21f87e1
merge: sync upstream production
ampagent Oct 22, 2026
d7171d5
fix: apply date and time preferences to new displays
ampagent Oct 22, 2026
455169d
fix: apply date and time preferences to moderation deadlines
ampagent Oct 22, 2026
7825d8c
merge: sync production and apply date preferences to new displays
rholak Oct 23, 2026
91ffb74
merge: sync production while keeping date and translation preferences
rholak Oct 23, 2026
b888489
merge: sync production and format new date displays
rholak Oct 24, 2026
0415534
fix: align event columns for 12-hour times
rholak Oct 24, 2026
58c6284
test: make date-time display assertions locale-stable
rholak Oct 25, 2026
83ebba2
fix: compare date years in the display timezone
rholak Oct 25, 2026
99c77d2
Merge production and format pinned message timestamps
rholak Oct 26, 2026
88f6b6a
test: restore an unset timezone after date display checks
rholak Oct 26, 2026
86f8341
Merge upstream production profile changes
rholak Oct 26, 2026
49a28f8
Merge upstream profile clock changes
rholak Oct 26, 2026
8b62d53
Merge upstream scheduling controls with date preferences
rholak Oct 26, 2026
ab4a893
fix: show timeout notices on operations pages
rholak Oct 26, 2026
b650d8c
fix: keep date preferences focused on display formatting
rholak Oct 27, 2026
5954b6a
Merge production and preserve date display preferences
rholak Oct 27, 2026
41f6f44
Merge production test coverage updates
rholak Oct 27, 2026
f0183aa
chore: restore upstream AI guidelines
rholak Oct 29, 2026
a3d31cb
fix: format organization join dates with saved preferences
rholak Oct 30, 2026
7bcc9cd
merge: resolve production member search integration
rholak Oct 30, 2026
37f9d97
merge: apply date preferences to retention displays
rholak Oct 31, 2026
89a3ed2
merge: preserve date preferences with profile header updates
rholak Oct 31, 2026
4971aa8
Merge production and preserve date display preferences
rholak Nov 1, 2026
0f30ae1
Merge production and format traffic hours with saved preferences
rholak Nov 1, 2026
1e6750b
Merge production and retain notification date preferences
rholak Nov 1, 2026
1d36273
Merge production and retain date preferences beside event themes
rholak Nov 4, 2026
5053569
Merge production and retain date formats with live updates
rholak Nov 4, 2026
eba9eae
Merge branch 'production' into feature/date-time-preferences
rholak Nov 5, 2026
25199aa
Merge production and retain date formats in mentions
rholak Nov 6, 2026
e003c7c
Merge production and retain chat date preferences
rholak Nov 6, 2026
0a22290
Merge production and format app usage dates with saved preferences
rholak Nov 6, 2026
b110fe8
fix: preserve date preferences in link topics
rholak Nov 7, 2026
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
4 changes: 4 additions & 0 deletions KNOWLEDGE_BASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -510,8 +510,12 @@ Settings → Appearance offers:

These appearance choices are saved in the browser, not on the account, so each browser keeps its own. Settings → Profile lets you choose one of six lane colors (Orange, Green, Violet, Teal, Rose, Ochre), a custom color, or the automatic color used around your identity and activity graph.

Date and time formats are also under Settings → Appearance. Choose month-first (`Oct 19`) or day-first (`19 Oct`) dates and a 12- or 24-hour clock independently. “Browser default” uses the browser's date order and clock format. Month and weekday names remain in English. These format preferences are private and saved on the account. They change display only, not stored timestamps, scheduling, or day grouping.

Profile settings also let you share a timezone. Timestamps can show the author's local time as well as the viewer's when a timezone is shared. Leaving it unset uses the browser's timezone for your display without publishing your timezone. A separate, off-by-default setting shows your current local time on your profile, next to your location and website; it only applies while a timezone is set, and clearing the timezone turns it off. These /dev/null preferences are separate from your GitHub-managed profile information.

The profile clock uses the profile owner's timezone and the viewer's clock format. Date and time formats save separately from the profile timezone.

Sources: [appearance settings](resources/js/pages/appearance/update.tsx), [lane picker](resources/js/components/lane-picker.tsx), [timezone picker](resources/js/components/timezone-picker.tsx), [local time](resources/js/components/local-time.tsx), [timestamp tooltip](resources/js/components/timestamp-tooltip.tsx).

### Installing /dev/null as an app
Expand Down
2 changes: 2 additions & 0 deletions app/Actions/CreateExport.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ public function handle(User $user): array
'show_local_time' => $user->show_local_time,
'translation_language' => $user->translation_language,
'automatic_translation' => $user->automatic_translation ?? true,
'date_format' => $user->date_format?->value,
'time_format' => $user->time_format?->value,
'lane_color' => $user->lane_color,
'pasteis_mode' => $user->pasteis_mode,
'pasteis_received' => $user->pasteis_received,
Expand Down
20 changes: 20 additions & 0 deletions app/Actions/UpdateUserDateTime.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace App\Actions;

use App\Enums\DateFormat;
use App\Enums\TimeFormat;
use App\Models\User;

final readonly class UpdateUserDateTime
{
public function handle(User $user, ?DateFormat $dateFormat, ?TimeFormat $timeFormat): void
{
$user->update([
'date_format' => $dateFormat,
'time_format' => $timeFormat,
]);
}
}
7 changes: 6 additions & 1 deletion app/Dto/ReaderClock.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Dto;

use App\Enums\DateFormat;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Http\Request;
use IntlTimeZone;
Expand All @@ -21,6 +22,7 @@
public function __construct(
public ?string $timeZone,
public ?string $hourCycle,
public ?string $dateOrder,
) {
//
}
Expand All @@ -29,6 +31,7 @@ public static function of(Request $request): self
{
$timeZone = $request->cookie('time_zone');
$hourCycle = $request->cookie('hour_cycle');
$dateOrder = $request->cookie('date_order');
$isSystemTimeZone = false;

if (is_string($timeZone)) {
Expand All @@ -39,17 +42,19 @@ public static function of(Request $request): self
return new self(
is_string($timeZone) && $isSystemTimeZone ? $timeZone : null,
is_string($hourCycle) && in_array($hourCycle, self::HOUR_CYCLES, true) ? $hourCycle : null,
is_string($dateOrder) ? DateFormat::tryFrom($dateOrder)?->value : null,
);
}

/**
* @return array{timeZone: string|null, hourCycle: string|null}
* @return array{timeZone: string|null, hourCycle: string|null, dateOrder: string|null}
*/
public function toArray(): array
{
return [
'timeZone' => $this->timeZone,
'hourCycle' => $this->hourCycle,
'dateOrder' => $this->dateOrder,
];
}
}
11 changes: 11 additions & 0 deletions app/Enums/DateFormat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace App\Enums;

enum DateFormat: string
{
case MonthFirst = 'month_first';
case DayFirst = 'day_first';
}
11 changes: 11 additions & 0 deletions app/Enums/TimeFormat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace App\Enums;

enum TimeFormat: string
{
case TwelveHour = '12h';
case TwentyFourHour = '24h';
}
33 changes: 33 additions & 0 deletions app/Http/Controllers/UserDateTimeController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Actions\UpdateUserDateTime;
use App\Enums\DateFormat;
use App\Enums\TimeFormat;
use App\Http\Requests\UpdateUserDateTimeRequest;
use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;

final readonly class UserDateTimeController
{
public function update(UpdateUserDateTimeRequest $request, #[CurrentUser] User $user, UpdateUserDateTime $action): RedirectResponse
{
$action->handle(
$user,
$request->enum('date_format', DateFormat::class),
$request->enum('time_format', TimeFormat::class),
);

Inertia::flash('toast', [
'type' => 'success',
'message' => 'Date and time preferences saved.',
]);

return back();
}
}
4 changes: 4 additions & 0 deletions app/Http/Middleware/HandleInertiaRequests.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ public function share(Request $request): array
? Inertia::live(fn (): bool => resolve(Features::class)->enabled(Feature::OrganizationCommits), 'features', new Channel('features'))
: false,
'isAdmin' => $isAdmin,
'dateTime' => fn (): array => [
'dateFormat' => $user?->date_format,
'timeFormat' => $user?->time_format,
],
'automaticTranslation' => $live(fn (): bool => $user->automatic_translation ?? true, UserUpdate::Account),
// The language translations are read in, saved or from the browser, so a change saved elsewhere reaches
// the translations already shown.
Expand Down
29 changes: 29 additions & 0 deletions app/Http/Requests/UpdateUserDateTimeRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace App\Http\Requests;

use App\Enums\DateFormat;
use App\Enums\TimeFormat;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

final class UpdateUserDateTimeRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}

/**
* @return array<string, array<int, mixed>>
*/
public function rules(): array
{
return [
'date_format' => ['present', 'nullable', Rule::enum(DateFormat::class)],
'time_format' => ['present', 'nullable', Rule::enum(TimeFormat::class)],
];
}
}
10 changes: 10 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
use App\Actions\CreateTomFriendship;
use App\Actions\CreateWorldReply;
use App\Dto\LiveStream;
use App\Enums\DateFormat;
use App\Enums\LivePlatform;
use App\Enums\Log;
use App\Enums\RemotesFrom;
use App\Enums\TimeFormat;
use Carbon\CarbonInterface;
use Database\Factories\UserFactory;
use Illuminate\Contracts\Database\Eloquent\Builder as BuilderContract;
Expand Down Expand Up @@ -58,6 +60,8 @@
* @property-read bool|null $automatic_translation
* @property-read string|null $translation_language
* @property-read string|null $timezone
* @property-read DateFormat|null $date_format
* @property-read TimeFormat|null $time_format
* @property-read bool $show_local_time
* @property-read LivePlatform|null $live_platform
* @property-read string|null $live_url
Expand Down Expand Up @@ -96,6 +100,8 @@
* @property-read int $followers_count
*/
#[Hidden([
'date_format',
'time_format',
'profile',
'github_id',
'email',
Expand Down Expand Up @@ -161,6 +167,8 @@ final class User extends Authenticatable
* @var array<string, mixed>
*/
protected $attributes = [
'date_format' => null,
'time_format' => null,
'log' => Log::Following->value,
'translation_language' => null,
'show_local_time' => false,
Expand Down Expand Up @@ -224,6 +232,8 @@ public function casts(): array
'show_local_time' => 'boolean',
'translation_language' => 'encrypted',
'automatic_translation' => 'boolean',
'date_format' => DateFormat::class,
'time_format' => TimeFormat::class,
'log' => Log::class,
'onboarded_at' => 'datetime',
'github_follows_imported_at' => 'datetime',
Expand Down
2 changes: 1 addition & 1 deletion bootstrap/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
$middleware->trimStrings(except: ['sdp', 'signals.*.sdp']);
$middleware->append(PreventFraming::class);

$middleware->encryptCookies(except: ['appearance', 'sidebar_state', 'who_to_follow', 'who_to_follow_round', 'lane_dimming', 'event_theme', 'replies_per_page', 'time_zone', 'hour_cycle']);
$middleware->encryptCookies(except: ['appearance', 'sidebar_state', 'who_to_follow', 'who_to_follow_round', 'lane_dimming', 'event_theme', 'replies_per_page', 'time_zone', 'hour_cycle', 'date_order']);

// Approving the app is the one page a guest should leave for GitHub, then return to. Every other page
// they open while signed out stays the log. Locally, sign-in is the modal on that log.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table): void {
$table->string('date_format')->nullable();
$table->string('time_format')->nullable();
});
}
};
6 changes: 3 additions & 3 deletions resources/js/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import type { ComponentType } from 'react';
import { StrictMode } from 'react';
import { createRoot, hydrateRoot } from 'react-dom/client';
import { TimeoutDialog } from '@/components/timeout-dialog';
import { Toaster } from '@/components/ui/sonner';
import { TooltipProvider } from '@/components/ui/tooltip';
import { initializeTheme, setCookie } from '@/hooks/use-appearance';
Expand Down Expand Up @@ -78,13 +77,13 @@ void createInertiaApp({
// So the server renders the next pages with the reader's clock.
setCookie('time_zone', clock.timeZone);
setCookie('hour_cycle', clock.hourCycle);
setCookie('date_order', clock.dateOrder);

const app = (
<StrictMode>
<TooltipProvider delayDuration={150}>
<App {...props} />
<Toaster />
<TimeoutDialog />
</TooltipProvider>
</StrictMode>
);
Expand All @@ -95,7 +94,8 @@ void createInertiaApp({
if (
el.hasAttribute('data-server-rendered') &&
serverClock?.timeZone === clock.timeZone &&
serverClock.hourCycle === clock.hourCycle
serverClock.hourCycle === clock.hourCycle &&
serverClock.dateOrder === clock.dateOrder
) {
setRenderedAt(props.initialPage.props.now);
hydrateRoot(el, app);
Expand Down
Loading