From fed0289d5ff5d6c2b5b8eaa5b55b4d323f23c7c9 Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:34:31 +0530 Subject: [PATCH 1/4] feat: show partner local time in direct remotes --- KNOWLEDGE_BASE.md | 2 +- resources/js/components/chat/chat-dock.tsx | 4 ++- .../js/components/chat/remote-local-time.tsx | 22 +++++++++++++ resources/js/components/timezone-picker.tsx | 8 ++--- resources/js/pages/conversation/index.tsx | 6 ++-- tests/Unit/RemoteLocalTime.test.tsx | 31 +++++++++++++++++++ 6 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 resources/js/components/chat/remote-local-time.tsx create mode 100644 tests/Unit/RemoteLocalTime.test.tsx diff --git a/KNOWLEDGE_BASE.md b/KNOWLEDGE_BASE.md index 4f6391d9f7..0288cd4b95 100644 --- a/KNOWLEDGE_BASE.md +++ b/KNOWLEDGE_BASE.md @@ -510,7 +510,7 @@ 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. -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. +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, and in the header of direct Remotes with you. 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. 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). diff --git a/resources/js/components/chat/chat-dock.tsx b/resources/js/components/chat/chat-dock.tsx index 53aef369c1..3ed73b020b 100644 --- a/resources/js/components/chat/chat-dock.tsx +++ b/resources/js/components/chat/chat-dock.tsx @@ -14,6 +14,7 @@ import OrgController from '@/actions/App/Http/Controllers/OrgController'; import { ConversationActions } from '@/components/chat/conversation-actions'; import { ConversationThread } from '@/components/chat/conversation-thread'; import { InboxPanel } from '@/components/chat/inbox-panel'; +import { RemoteLocalTime } from '@/components/chat/remote-local-time'; import { MemberStack, OrgHeader, orgTitle } from '@/components/chat/org'; import { SearchDialog } from '@/components/search-dialog'; import { @@ -310,7 +311,7 @@ function DirectDockHeader({ @@ -319,6 +320,7 @@ function DirectDockHeader({ @{partner.handle} + ; +}) { + if (!partner.show_local_time || !partner.timezone) { + return null; + } + + return ( +
    + +
+ ); +} diff --git a/resources/js/components/timezone-picker.tsx b/resources/js/components/timezone-picker.tsx index 5a91e147c5..4b4b78f2eb 100644 --- a/resources/js/components/timezone-picker.tsx +++ b/resources/js/components/timezone-picker.tsx @@ -71,7 +71,7 @@ export default function TimezonePicker() {
- Show my local time on my profile + Show my local time on my profile and in Remotes - Anyone who opens your profile sees the - current time in your timezone. + People viewing your profile or chatting + with you can see your current local time. diff --git a/resources/js/pages/conversation/index.tsx b/resources/js/pages/conversation/index.tsx index a9ccf5ff74..02317daaf8 100644 --- a/resources/js/pages/conversation/index.tsx +++ b/resources/js/pages/conversation/index.tsx @@ -11,6 +11,7 @@ import OrgController from '@/actions/App/Http/Controllers/OrgController'; import { ConversationActions } from '@/components/chat/conversation-actions'; import { ConversationThread } from '@/components/chat/conversation-thread'; import { InboxPanel } from '@/components/chat/inbox-panel'; +import { RemoteLocalTime } from '@/components/chat/remote-local-time'; import { OrgHeader, OrgPeopleDialog } from '@/components/chat/org'; import { RemoteSettingsMenu } from '@/components/chat/remote-settings-menu'; import { SearchDialog } from '@/components/search-dialog'; @@ -287,7 +288,7 @@ function ThreadHeader({ } as React.CSSProperties } /> - +
@{partner.handle} - + +
{ + assert.equal( + RemoteLocalTime({ + partner: { timezone: 'Asia/Kolkata', show_local_time: false }, + }), + null, + ); + assert.equal( + RemoteLocalTime({ partner: { timezone: null, show_local_time: true } }), + null, + ); + + const result = RemoteLocalTime({ + partner: { timezone: 'Asia/Kolkata', show_local_time: true }, + }); + + assert.ok( + isValidElement<{ + title: string; + children: ReactElement<{ timeZone: string }>; + }>(result), + ); + assert.equal(result.props.title, 'Asia/Kolkata'); + assert.equal(result.props.children.props.timeZone, 'Asia/Kolkata'); +}); From 1eb2641a11e0e5ad996b2d66e616c4f777644da9 Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:53:22 +0530 Subject: [PATCH 2/4] fix: show partner local time in minimized remotes dock --- resources/js/components/chat/chat-dock.tsx | 27 +++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/resources/js/components/chat/chat-dock.tsx b/resources/js/components/chat/chat-dock.tsx index 3ed73b020b..a9ed2c55ca 100644 --- a/resources/js/components/chat/chat-dock.tsx +++ b/resources/js/components/chat/chat-dock.tsx @@ -218,17 +218,22 @@ function DockedChat({ user }: { user: User }) { avatarClassName="size-6" /> )} - - {target.kind === 'direct' - ? target.partner.name - : orgTitle( - target.org, - thread?.members ?? - summary?.members ?? - null, - user, - )} - +
+ + {target.kind === 'direct' + ? target.partner.name + : orgTitle( + target.org, + thread?.members ?? + summary?.members ?? + null, + user, + )} + + {target.kind === 'direct' && ( + + )} +
{unread > 0 && ( Date: Fri, 6 Nov 2026 21:27:00 +0530 Subject: [PATCH 3/4] feat: show local time in group remotes --- KNOWLEDGE_BASE.md | 2 +- .../Controllers/ConversationController.php | 20 +++++++-- app/Http/Controllers/InboxController.php | 10 ++++- app/Http/Controllers/OrgController.php | 17 +++++++- app/Http/Controllers/OrgMessageController.php | 12 +++++- app/Http/Resources/ConversationResource.php | 6 ++- app/Http/Resources/OrgThreadResource.php | 6 ++- resources/js/components/chat/chat-dock.tsx | 7 ++-- resources/js/components/chat/org.tsx | 2 + .../js/components/chat/remote-local-time.tsx | 2 +- resources/js/components/timezone-picker.tsx | 10 +++-- resources/js/pages/conversation/index.tsx | 21 ++++++---- .../Api/OrgMessageControllerTest.php | 17 ++++++++ .../ConversationControllerTest.php | 41 +++++++++++++++++++ .../Feature/Controllers/OrgControllerTest.php | 16 ++++++++ .../Controllers/OrgMessageControllerTest.php | 16 ++++++++ 16 files changed, 180 insertions(+), 25 deletions(-) diff --git a/KNOWLEDGE_BASE.md b/KNOWLEDGE_BASE.md index 0288cd4b95..a1ba473aad 100644 --- a/KNOWLEDGE_BASE.md +++ b/KNOWLEDGE_BASE.md @@ -510,7 +510,7 @@ 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. -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, and in the header of direct Remotes with you. 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. +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, in direct Remote headers, and beside members who have opted in on group Remotes. 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. 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). diff --git a/app/Http/Controllers/ConversationController.php b/app/Http/Controllers/ConversationController.php index e8f4869581..6fd1366a0e 100644 --- a/app/Http/Controllers/ConversationController.php +++ b/app/Http/Controllers/ConversationController.php @@ -7,6 +7,7 @@ use App\Actions\UpdateConversationReadThrough; use App\Http\Requests\ListConversationsRequest; use App\Http\Requests\ShowConversationRequest; +use App\Http\Resources\ConversationResource; use App\Models\Conversation; use App\Models\User; use App\Queries\ConversationsQuery; @@ -20,7 +21,12 @@ public function index(ListConversationsRequest $request, #[CurrentUser] User $user, ConversationsQuery $conversations): Response { return Inertia::render('conversation/index', [ - 'inbox' => fn (): array => $conversations->get($user), + 'inbox' => function () use ($request, $user, $conversations): array { + $inbox = $conversations->get($user); + $inbox['conversations'] = ConversationResource::collection($inbox['conversations'])->resolve($request); + + return $inbox; + }, 'partner' => null, 'thread' => null, ]); @@ -44,8 +50,16 @@ public function show(ShowConversationRequest $request, #[CurrentUser] User $user return $thread; }, - 'inbox' => fn (): array => $conversations->get($user), - 'partner' => $partner, + 'inbox' => function () use ($request, $user, $conversations): array { + $inbox = $conversations->get($user); + $inbox['conversations'] = ConversationResource::collection($inbox['conversations'])->resolve($request); + + return $inbox; + }, + 'partner' => [ + ...$partner->toArray(), + 'timezone' => $partner->show_local_time ? $partner->timezone : null, + ], ]); } } diff --git a/app/Http/Controllers/InboxController.php b/app/Http/Controllers/InboxController.php index ce8b3dc5df..70a6f28295 100644 --- a/app/Http/Controllers/InboxController.php +++ b/app/Http/Controllers/InboxController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers; use App\Http\Requests\ListInboxRequest; +use App\Http\Resources\ConversationResource; use App\Models\User; use App\Queries\ConversationsQuery; use Illuminate\Container\Attributes\CurrentUser; @@ -14,10 +15,15 @@ { public function index(ListInboxRequest $request, #[CurrentUser] User $user, ConversationsQuery $conversations): JsonResponse { + $inbox = $conversations->get($user, $request->tab(), $request->term(), $request->beforeId()); + $inbox['conversations'] = ConversationResource::collection($inbox['conversations'])->resolve($request); + return response()->json([ ...($request->notificationBaseline() ? ['notification_baseline' => $conversations->notificationBaseline($user)] : []), - ...$conversations->get($user, $request->tab(), $request->term(), $request->beforeId()), - ...($request->notificationConversations() ? ['notification_conversations' => $conversations->get($user, pinnedFirst: false)['conversations']] : []), + ...$inbox, + ...($request->notificationConversations() ? [ + 'notification_conversations' => ConversationResource::collection($conversations->get($user, pinnedFirst: false)['conversations'])->resolve($request), + ] : []), 'unread' => $conversations->unreadCount($user), 'requests' => $conversations->requestCount($user), ])->header('Cache-Control', 'private, no-store'); diff --git a/app/Http/Controllers/OrgController.php b/app/Http/Controllers/OrgController.php index 0e779a994f..57f54a69d9 100644 --- a/app/Http/Controllers/OrgController.php +++ b/app/Http/Controllers/OrgController.php @@ -10,6 +10,7 @@ use App\Http\Requests\CreateOrgRequest; use App\Http\Requests\ShowOrgRequest; use App\Http\Requests\UpdateOrgRequest; +use App\Http\Resources\ConversationResource; use App\Models\Conversation; use App\Models\User; use App\Queries\ConversationsQuery; @@ -32,11 +33,25 @@ public function show(ShowOrgRequest $request, #[CurrentUser] User $user, Convers 'thread' => function () use ($user, $org, $threads, $action): array { $thread = $threads->org($user, $org); + $thread['members'] = $thread['members']->map(function (array $member): array { + $member['user'] = [ + ...$member['user']->toArray(), + 'timezone' => $member['user']->show_local_time ? $member['user']->timezone : null, + ]; + + return $member; + }); + $action->handle($user, $org, $thread['messages']->last()->id ?? 0); return $thread; }, - 'inbox' => fn (): array => $conversations->get($user), + 'inbox' => function () use ($request, $user, $conversations): array { + $inbox = $conversations->get($user); + $inbox['conversations'] = ConversationResource::collection($inbox['conversations'])->resolve($request); + + return $inbox; + }, 'partner' => null, 'org' => ['id' => $org->id, 'name' => $org->name], ]); diff --git a/app/Http/Controllers/OrgMessageController.php b/app/Http/Controllers/OrgMessageController.php index 83bdb18233..4264273137 100644 --- a/app/Http/Controllers/OrgMessageController.php +++ b/app/Http/Controllers/OrgMessageController.php @@ -17,9 +17,19 @@ { public function index(ListOrgMessagesRequest $request, #[CurrentUser] User $user, Conversation $org, ConversationThreadQuery $threads): JsonResponse { + $thread = $threads->org($user, $org, $request->afterId(), $request->beforeId(), $request->reactionsAfter()); + $thread['members'] = $thread['members']->map(function (array $member): array { + $member['user'] = [ + ...$member['user']->toArray(), + 'timezone' => $member['user']->show_local_time ? $member['user']->timezone : null, + ]; + + return $member; + }); + return response()->json([ 'org' => ['id' => $org->id, 'name' => $org->name], - ...$threads->org($user, $org, $request->afterId(), $request->beforeId(), $request->reactionsAfter()), + ...$thread, ])->header('Cache-Control', 'private, no-store'); } diff --git a/app/Http/Resources/ConversationResource.php b/app/Http/Resources/ConversationResource.php index a9d6011234..ad54e16b63 100644 --- a/app/Http/Resources/ConversationResource.php +++ b/app/Http/Resources/ConversationResource.php @@ -30,7 +30,11 @@ public function toArray(Request $request): array return [ ...$conversation, - 'partner' => $conversation['partner'] instanceof User ? UserResource::make($conversation['partner']) : null, + 'partner' => $conversation['partner'] instanceof User ? [ + ...UserResource::make($conversation['partner'])->toArray($request), + 'timezone' => $conversation['partner']->show_local_time ? $conversation['partner']->timezone : null, + 'show_local_time' => $conversation['partner']->show_local_time, + ] : null, 'members' => $conversation['members']?->map(fn (array $member): array => [ 'user' => UserResource::make($member['user']), 'role' => $member['role'], diff --git a/app/Http/Resources/OrgThreadResource.php b/app/Http/Resources/OrgThreadResource.php index 8df0efe9b5..90cc7cf8e3 100644 --- a/app/Http/Resources/OrgThreadResource.php +++ b/app/Http/Resources/OrgThreadResource.php @@ -48,7 +48,11 @@ public function toArray(Request $request): array $members = $thread['members']->map(fn (array $member): array => [ 'role' => $member['role'], 'read_through_id' => $member['read_through_id'], - 'user' => UserResource::make($member['user']), + 'user' => [ + ...UserResource::make($member['user'])->toArray($request), + 'timezone' => $member['user']->show_local_time ? $member['user']->timezone : null, + 'show_local_time' => $member['user']->show_local_time, + ], ]); return [ diff --git a/resources/js/components/chat/chat-dock.tsx b/resources/js/components/chat/chat-dock.tsx index a9ed2c55ca..afffd76263 100644 --- a/resources/js/components/chat/chat-dock.tsx +++ b/resources/js/components/chat/chat-dock.tsx @@ -14,8 +14,8 @@ import OrgController from '@/actions/App/Http/Controllers/OrgController'; import { ConversationActions } from '@/components/chat/conversation-actions'; import { ConversationThread } from '@/components/chat/conversation-thread'; import { InboxPanel } from '@/components/chat/inbox-panel'; -import { RemoteLocalTime } from '@/components/chat/remote-local-time'; import { MemberStack, OrgHeader, orgTitle } from '@/components/chat/org'; +import { RemoteLocalTime } from '@/components/chat/remote-local-time'; import { SearchDialog } from '@/components/search-dialog'; import { Tooltip, @@ -64,8 +64,9 @@ function DockedChat({ user }: { user: User }) { const target: ChatTarget | null = dock.conversation === null ? null - : (threads[dock.conversation]?.target ?? - (summary === undefined ? null : targetOf(summary))); + : summary === undefined + ? (threads[dock.conversation]?.target ?? null) + : targetOf(summary); const thread = target === null ? undefined : threads[targetKey(target)]; const open = (next: ChatTarget) => { diff --git a/resources/js/components/chat/org.tsx b/resources/js/components/chat/org.tsx index 3e9a7868fd..748f507e35 100644 --- a/resources/js/components/chat/org.tsx +++ b/resources/js/components/chat/org.tsx @@ -4,6 +4,7 @@ import type { FormEvent, ReactNode } from 'react'; import { useEffect, useRef, useState } from 'react'; import { toast } from 'sonner'; import { ConversationActions } from '@/components/chat/conversation-actions'; +import { RemoteLocalTime } from '@/components/chat/remote-local-time'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { @@ -532,6 +533,7 @@ function MemberRow({ @{member.user.handle} + diff --git a/resources/js/components/timezone-picker.tsx b/resources/js/components/timezone-picker.tsx index 4b4b78f2eb..62122ef885 100644 --- a/resources/js/components/timezone-picker.tsx +++ b/resources/js/components/timezone-picker.tsx @@ -71,7 +71,7 @@ export default function TimezonePicker() {
- Show my local time on my profile and in Remotes + Show my local time on my profile and in direct + and group Remotes - People viewing your profile or chatting - with you can see your current local time. + People viewing your profile or in a direct + or group remote with you can see your + current local time. diff --git a/resources/js/pages/conversation/index.tsx b/resources/js/pages/conversation/index.tsx index 02317daaf8..6b8cd5644d 100644 --- a/resources/js/pages/conversation/index.tsx +++ b/resources/js/pages/conversation/index.tsx @@ -11,8 +11,8 @@ import OrgController from '@/actions/App/Http/Controllers/OrgController'; import { ConversationActions } from '@/components/chat/conversation-actions'; import { ConversationThread } from '@/components/chat/conversation-thread'; import { InboxPanel } from '@/components/chat/inbox-panel'; -import { RemoteLocalTime } from '@/components/chat/remote-local-time'; import { OrgHeader, OrgPeopleDialog } from '@/components/chat/org'; +import { RemoteLocalTime } from '@/components/chat/remote-local-time'; import { RemoteSettingsMenu } from '@/components/chat/remote-settings-menu'; import { SearchDialog } from '@/components/search-dialog'; import { Button } from '@/components/ui/button'; @@ -44,13 +44,22 @@ export default function Index({ thread: Thread | OrgThread | null; }) { const { user } = useUser(); + const conversations = useInbox(); const dock = useChatDock(); const [isPicking, setIsPicking] = useState(false); const [isStartingOrg, setIsStartingOrg] = useState(false); const [seededFrom, setSeededFrom] = useState(null); + const currentPartner = + partner === null + ? null + : (conversations?.find( + (conversation) => + conversation.kind === 'direct' && + conversation.partner?.id === partner.id, + )?.partner ?? partner); const target: ChatTarget | null = - partner !== null - ? { kind: 'direct', partner } + currentPartner !== null + ? { kind: 'direct', partner: currentPartner } : org !== null ? { kind: 'org', org } : null; @@ -64,8 +73,6 @@ export default function Index({ } } - useInbox(); - // Keeps the inbox's tab and search while moving between conversations. const visit = (href: Parameters[0]) => router.visit(href, { preserveScroll: true, preserveState: true }); @@ -91,8 +98,8 @@ export default function Index({ <> assertJsonPath('data.access', 'open'); }); +it('shares a member timezone through the group API only when they opt in', function (): void { + [$sharing, $private] = followersOf($this->user, 2); + $sharing->update(['timezone' => 'America/Los_Angeles', 'show_local_time' => true]); + $private->update(['timezone' => 'Asia/Tokyo', 'show_local_time' => false]); + $org = createOrg($this->user, [$sharing, $private]); + + $members = collect(signedApiRequest($this->keyPair, 'GET', route('api-org-message.index', $org)) + ->assertOk() + ->json('data.members')) + ->keyBy('user.handle'); + + expect($members[$sharing->handle]['user']['timezone'])->toBe('America/Los_Angeles') + ->and($members[$sharing->handle]['user']['show_local_time'])->toBeTrue() + ->and($members[$private->handle]['user']['timezone'])->toBeNull() + ->and($members[$private->handle]['user']['show_local_time'])->toBeFalse(); +}); + it('names who an event is about', function (): void { [$nuno, $taylor] = followersOf($this->user, 2); $org = createOrg($this->user, [$nuno]); diff --git a/tests/Feature/Controllers/ConversationControllerTest.php b/tests/Feature/Controllers/ConversationControllerTest.php index 6a851b6b55..5f4fdfbb98 100644 --- a/tests/Feature/Controllers/ConversationControllerTest.php +++ b/tests/Feature/Controllers/ConversationControllerTest.php @@ -74,6 +74,47 @@ ->where('thread.messages', [])); }); +it('shares a direct partner timezone only when they opt in', function (): void { + $mark = User::factory()->withRemotes()->create(); + $sharing = User::factory()->withRemotes()->create([ + 'timezone' => 'America/Los_Angeles', + 'show_local_time' => true, + ]); + $private = User::factory()->withRemotes()->create([ + 'timezone' => 'Asia/Tokyo', + 'show_local_time' => false, + ]); + sendChatMessage($sharing, $mark, 'local time is visible'); + sendChatMessage($private, $mark, 'local time is private'); + + $this->actingAs($mark)->get(route('conversation.show', $sharing)) + ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page + ->where('partner.timezone', 'America/Los_Angeles') + ->where('partner.show_local_time', true)); + + $this->get(route('conversation.show', $private)) + ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page + ->where('partner.timezone', null) + ->where('partner.show_local_time', false) + ->where('inbox.conversations.0.partner.handle', $private->handle) + ->where('inbox.conversations.0.partner.timezone', null)); + + $private->update(['show_local_time' => true]); + $inbox = collect($this->getJson(route('inbox.index'))->assertOk()->json('conversations')) + ->keyBy('partner.handle'); + + expect($inbox[$sharing->handle]['partner']['timezone'])->toBe('America/Los_Angeles') + ->and($inbox[$private->handle]['partner']['timezone'])->toBe('Asia/Tokyo') + ->and($inbox[$private->handle]['partner']['show_local_time'])->toBeTrue(); + + $private->update(['show_local_time' => false]); + $inbox = collect($this->getJson(route('inbox.index'))->assertOk()->json('conversations')) + ->keyBy('partner.handle'); + + expect($inbox[$private->handle]['partner']['timezone'])->toBeNull() + ->and($inbox[$private->handle]['partner']['show_local_time'])->toBeFalse(); +}); + it('hides conversations you cannot have', function (): void { $mark = User::factory()->withRemotes()->create(); $nuno = User::factory()->withRemotes()->create(); diff --git a/tests/Feature/Controllers/OrgControllerTest.php b/tests/Feature/Controllers/OrgControllerTest.php index 39648da8b0..5d0c538c67 100644 --- a/tests/Feature/Controllers/OrgControllerTest.php +++ b/tests/Feature/Controllers/OrgControllerTest.php @@ -118,6 +118,22 @@ $this->getJson(route('org-message.index', $org))->assertNotFound(); }); +it('shows opted-in member local times in a group remote without sharing timezones from other members', function (): void { + $mark = User::factory()->create(); + [$sharing, $private] = followersOf($mark, 2); + $sharing->update(['timezone' => 'America/Los_Angeles', 'show_local_time' => true]); + $private->update(['timezone' => 'Asia/Tokyo', 'show_local_time' => false]); + $org = createOrg($mark, [$sharing, $private]); + + $this->actingAs($mark)->get(route('org.show', $org)) + ->assertOk() + ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page + ->where('thread.members.1.user.timezone', 'America/Los_Angeles') + ->where('thread.members.1.user.show_local_time', true) + ->where('thread.members.2.user.timezone', null) + ->where('thread.members.2.user.show_local_time', false)); +}); + it('polls the badge counts on an open org without marking it read', function (): void { $mark = User::factory()->create(); [$nuno] = followersOf($mark, 1); diff --git a/tests/Feature/Controllers/OrgMessageControllerTest.php b/tests/Feature/Controllers/OrgMessageControllerTest.php index 51a1b83883..11c747c71e 100644 --- a/tests/Feature/Controllers/OrgMessageControllerTest.php +++ b/tests/Feature/Controllers/OrgMessageControllerTest.php @@ -37,6 +37,22 @@ $this->actingAs($mark)->getJson(route('org-message.index', $org))->assertJsonPath('members.1.read_through_id', 0); }); +it('shares a member timezone with the group only when they opt in', function (): void { + $mark = User::factory()->create(); + [$sharing, $private] = followersOf($mark, 2); + $sharing->update(['timezone' => 'America/Los_Angeles', 'show_local_time' => true]); + $private->update(['timezone' => 'Asia/Tokyo', 'show_local_time' => false]); + $org = createOrg($mark, [$sharing, $private]); + + $members = collect($this->actingAs($mark)->getJson(route('org-message.index', $org))->assertOk()->json('members')) + ->keyBy('user.handle'); + + expect($members[$sharing->handle]['user']['timezone'])->toBe('America/Los_Angeles') + ->and($members[$sharing->handle]['user']['show_local_time'])->toBeTrue() + ->and($members[$private->handle]['user']['timezone'])->toBeNull() + ->and($members[$private->handle]['user']['show_local_time'])->toBeFalse(); +}); + it('shows who is typing', function (): void { $mark = User::factory()->create(); [$nuno] = followersOf($mark, 1); From e30cc3060f506af166469e2ef8acdfec9ea46281 Mon Sep 17 00:00:00 2001 From: Punyapal Shah Date: Fri, 6 Nov 2026 21:34:25 +0530 Subject: [PATCH 4/4] fix: include opted-in timezones in user resources --- app/Http/Resources/ConversationResource.php | 2 -- app/Http/Resources/OrgThreadResource.php | 6 +---- app/Http/Resources/UserResource.php | 2 ++ .../Api/MentionSuggestionControllerTest.php | 26 +++++++++++++++++++ 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/app/Http/Resources/ConversationResource.php b/app/Http/Resources/ConversationResource.php index ad54e16b63..a3b89b6efd 100644 --- a/app/Http/Resources/ConversationResource.php +++ b/app/Http/Resources/ConversationResource.php @@ -32,8 +32,6 @@ public function toArray(Request $request): array ...$conversation, 'partner' => $conversation['partner'] instanceof User ? [ ...UserResource::make($conversation['partner'])->toArray($request), - 'timezone' => $conversation['partner']->show_local_time ? $conversation['partner']->timezone : null, - 'show_local_time' => $conversation['partner']->show_local_time, ] : null, 'members' => $conversation['members']?->map(fn (array $member): array => [ 'user' => UserResource::make($member['user']), diff --git a/app/Http/Resources/OrgThreadResource.php b/app/Http/Resources/OrgThreadResource.php index 90cc7cf8e3..8df0efe9b5 100644 --- a/app/Http/Resources/OrgThreadResource.php +++ b/app/Http/Resources/OrgThreadResource.php @@ -48,11 +48,7 @@ public function toArray(Request $request): array $members = $thread['members']->map(fn (array $member): array => [ 'role' => $member['role'], 'read_through_id' => $member['read_through_id'], - 'user' => [ - ...UserResource::make($member['user'])->toArray($request), - 'timezone' => $member['user']->show_local_time ? $member['user']->timezone : null, - 'show_local_time' => $member['user']->show_local_time, - ], + 'user' => UserResource::make($member['user']), ]); return [ diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index 1f1d0b6383..91aef1caa8 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -36,6 +36,8 @@ public function toArray(Request $request): array 'bio' => $this->bio, 'company' => $this->company, 'location' => $this->location, + 'timezone' => $this->show_local_time ? $this->timezone : null, + 'show_local_time' => $this->show_local_time, 'website' => $this->website, 'github_url' => "https://github.com/{$this->handle}", 'html_url' => route('user.show', $this->resource), diff --git a/tests/Feature/Controllers/Api/MentionSuggestionControllerTest.php b/tests/Feature/Controllers/Api/MentionSuggestionControllerTest.php index 2f923b3a12..17d5de834f 100644 --- a/tests/Feature/Controllers/Api/MentionSuggestionControllerTest.php +++ b/tests/Feature/Controllers/Api/MentionSuggestionControllerTest.php @@ -24,6 +24,32 @@ ->assertJsonMissingPath('data.0.email'); }); +it('shares a suggested user timezone only when they opt in', function (): void { + $sharing = User::factory()->create([ + 'handle' => 'sharing', + 'timezone' => 'America/Los_Angeles', + 'show_local_time' => true, + ]); + $private = User::factory()->create([ + 'handle' => 'private', + 'timezone' => 'Asia/Tokyo', + 'show_local_time' => false, + ]); + $this->user->following()->attach([$sharing->id, $private->id]); + + $users = collect(signedApiRequest($this->keyPair, 'GET', route('api-mention-suggestion.index')) + ->assertOk() + ->json('data'))->keyBy('handle'); + + expect($users['sharing'])->toMatchArray([ + 'timezone' => 'America/Los_Angeles', + 'show_local_time' => true, + ])->and($users['private'])->toMatchArray([ + 'timezone' => null, + 'show_local_time' => false, + ]); +}); + it('suggests matches, people the user follows first, leaving out muted people and people across a block', function (): void { User::factory()->create(['handle' => 'taylor', 'name' => 'Taylor']); $followed = User::factory()->create(['handle' => 'taylorotwell', 'name' => 'Taylor Otwell']);