Skip to content

Commit 579c1bf

Browse files
committed
Timezones: Seperated out store & display timezones to two options
1 parent 242b7df commit 579c1bf

File tree

6 files changed

+90
-9
lines changed

6 files changed

+90
-9
lines changed

.env.example.complete

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,14 @@ APP_LANG=en
3636
# APP_LANG will be used if such a header is not provided.
3737
APP_AUTO_LANG_PUBLIC=true
3838

39-
# Application timezone
40-
# Used where dates are displayed such as on exported content.
39+
# Application timezones
40+
# The first option is used to determine what timezone is used for date storage.
41+
# Leaving that as "UTC" is advised.
42+
# The second option is used to set the timezone which will be used for date
43+
# formatting and display. This defaults to the "APP_TIMEZONE" value.
4144
# Valid timezone values can be found here: https://www.php.net/manual/en/timezones.php
4245
APP_TIMEZONE=UTC
46+
APP_DISPLAY_TIMEZONE=UTC
4347

4448
# Application theme
4549
# Used to specific a themes/<APP_THEME> folder where BookStack UI

app/App/Providers/ViewTweaksServiceProvider.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,23 @@
33
namespace BookStack\App\Providers;
44

55
use BookStack\Entities\BreadcrumbsViewComposer;
6+
use BookStack\Util\DateFormatter;
67
use Illuminate\Pagination\Paginator;
78
use Illuminate\Support\Facades\Blade;
89
use Illuminate\Support\Facades\View;
910
use Illuminate\Support\ServiceProvider;
1011

1112
class ViewTweaksServiceProvider extends ServiceProvider
1213
{
14+
public function register()
15+
{
16+
$this->app->singleton(DateFormatter::class, function ($app) {
17+
return new DateFormatter(
18+
$app['config']->get('app.display_timezone'),
19+
);
20+
});
21+
}
22+
1323
/**
1424
* Bootstrap services.
1525
*/
@@ -21,6 +31,9 @@ public function boot(): void
2131
// View Composers
2232
View::composer('entities.breadcrumbs', BreadcrumbsViewComposer::class);
2333

34+
// View Globals
35+
View::share('dates', $this->app->make(DateFormatter::class));
36+
2437
// Custom blade view directives
2538
Blade::directive('icon', function ($expression) {
2639
return "<?php echo (new \BookStack\Util\SvgIcon($expression))->toHtml(); ?>";

app/Config/app.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@
7070
// A list of the sources/hostnames that can be reached by application SSR calls.
7171
// This is used wherever users can provide URLs/hosts in-platform, like for webhooks.
7272
// Host-specific functionality (usually controlled via other options) like auth
73-
// or user avatars for example, won't use this list.
74-
// Space seperated if multiple. Can use '*' as a wildcard.
73+
// or user avatars, for example, won't use this list.
74+
// Space separated if multiple. Can use '*' as a wildcard.
7575
// Values will be compared prefix-matched, case-insensitive, against called SSR urls.
7676
// Defaults to allow all hosts.
7777
'ssr_hosts' => env('ALLOWED_SSR_HOSTS', '*'),
@@ -80,8 +80,10 @@
8080
// Integer value between 0 (IP hidden) to 4 (Full IP usage)
8181
'ip_address_precision' => env('IP_ADDRESS_PRECISION', 4),
8282

83-
// Application timezone for back-end date functions.
83+
// Application timezone for stored date/time values.
8484
'timezone' => env('APP_TIMEZONE', 'UTC'),
85+
// Application timezone for displayed date/time values in the UI.
86+
'display_timezone' => env('APP_DISPLAY_TIMEZONE', env('APP_TIMEZONE', 'UTC')),
8587

8688
// Default locale to use
8789
// A default variant is also stored since Laravel can overwrite

app/Util/DateFormatter.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace BookStack\Util;
4+
5+
use Carbon\Carbon;
6+
7+
class DateFormatter
8+
{
9+
public function __construct(
10+
protected string $displayTimezone,
11+
) {
12+
}
13+
14+
public function isoWithTimezone(Carbon $date): string
15+
{
16+
$withDisplayTimezone = $date->clone()->setTimezone($this->displayTimezone);
17+
18+
return $withDisplayTimezone->format('Y-m-d H:i:s T');
19+
}
20+
21+
public function relative(Carbon $date): string
22+
{
23+
return $date->diffForHumans();
24+
}
25+
}

resources/views/entities/meta.blade.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,15 @@
3131
@icon('star')
3232
<div>
3333
{!! trans('entities.meta_created_name', [
34-
'timeLength' => '<span title="'.$entity->created_at->toDayDateTimeString().'">'.$entity->created_at->diffForHumans() . '</span>',
34+
'timeLength' => '<span title="'. $dates->isoWithTimezone($entity->created_at) .'">'. $dates->relative($entity->created_at) . '</span>',
3535
'user' => "<a href='{$entity->createdBy->getProfileUrl()}'>".e($entity->createdBy->name). "</a>"
3636
]) !!}
3737
</div>
3838
</div>
3939
@else
4040
<div class="entity-meta-item">
4141
@icon('star')
42-
<span title="{{$entity->created_at->toDayDateTimeString()}}">{{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }}</span>
42+
<span title="{{ $dates->isoWithTimezone($entity->created_at) }}">{{ trans('entities.meta_created', ['timeLength' => $dates->relative($entity->created_at)]) }}</span>
4343
</div>
4444
@endif
4545

@@ -48,15 +48,15 @@
4848
@icon('edit')
4949
<div>
5050
{!! trans('entities.meta_updated_name', [
51-
'timeLength' => '<span title="' . $entity->updated_at->toDayDateTimeString() .'">' . $entity->updated_at->diffForHumans() .'</span>',
51+
'timeLength' => '<span title="' . $dates->isoWithTimezone($entity->updated_at) .'">' . $dates->relative($entity->updated_at) .'</span>',
5252
'user' => "<a href='{$entity->updatedBy->getProfileUrl()}'>".e($entity->updatedBy->name). "</a>"
5353
]) !!}
5454
</div>
5555
</div>
5656
@elseif (!$entity->isA('revision'))
5757
<div class="entity-meta-item">
5858
@icon('edit')
59-
<span title="{{ $entity->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }}</span>
59+
<span title="{{ $dates->isoWithTimezone($entity->updated_at) }}">{{ trans('entities.meta_updated', ['timeLength' => $dates->relative($entity->updated_at)]) }}</span>
6060
</div>
6161
@endif
6262

tests/Util/DateFormatterTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace Tests\Util;
4+
5+
use BookStack\Util\DateFormatter;
6+
use Carbon\Carbon;
7+
use Tests\TestCase;
8+
9+
class DateFormatterTest extends TestCase
10+
{
11+
public function test_iso_with_timezone_alters_from_stored_to_display_timezone()
12+
{
13+
$formatter = new DateFormatter('Europe/London');
14+
$dateTime = new Carbon('2020-06-01 12:00:00', 'UTC');
15+
16+
$result = $formatter->isoWithTimezone($dateTime);
17+
$this->assertEquals('2020-06-01 13:00:00 BST', $result);
18+
}
19+
20+
public function test_iso_with_timezone_works_from_non_utc_dates()
21+
{
22+
$formatter = new DateFormatter('Asia/Shanghai');
23+
$dateTime = new Carbon('2025-06-10 15:25:00', 'America/New_York');
24+
25+
$result = $formatter->isoWithTimezone($dateTime);
26+
$this->assertEquals('2025-06-11 03:25:00 CST', $result);
27+
}
28+
29+
public function test_relative()
30+
{
31+
$formatter = new DateFormatter('Europe/London');
32+
$dateTime = (new Carbon('now', 'UTC'))->subMinutes(50);
33+
34+
$result = $formatter->relative($dateTime);
35+
$this->assertEquals('50 minutes ago', $result);
36+
}
37+
}

0 commit comments

Comments
 (0)