diff --git a/app/Http/Controllers/NotificationController.php b/app/Http/Controllers/NotificationController.php index deef67fe79..276cb886c0 100644 --- a/app/Http/Controllers/NotificationController.php +++ b/app/Http/Controllers/NotificationController.php @@ -21,7 +21,7 @@ public function index(ListNotificationsRequest $request, #[CurrentUser] User $us $type = $request->type($user); return Inertia::render('notification/index', [ - 'notifications' => Inertia::scroll(fn (): CursorPaginator => $query->withStarredCommitImages($query->withQuotedCommits($user, $query->paginate($user, $type, $request->through()))))->matchOn('data.group_id'), + 'notifications' => Inertia::scroll(fn (): CursorPaginator => $query->withStarredCommitImages($query->withQuotedCommits($user, $query->withCommitStars($user, $query->paginate($user, $type, $request->through())))))->matchOn('data.group_id'), 'filters' => ['type' => $type], 'readAt' => $readAt?->toJSON(), ]); diff --git a/app/Http/Controllers/NotificationGroupController.php b/app/Http/Controllers/NotificationGroupController.php index 6f55d5a3b2..c322c54bf9 100644 --- a/app/Http/Controllers/NotificationGroupController.php +++ b/app/Http/Controllers/NotificationGroupController.php @@ -9,6 +9,7 @@ use App\Models\User; use App\Queries\NotificationGroupsQuery; use Illuminate\Container\Attributes\CurrentUser; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Http\JsonResponse; final readonly class NotificationGroupController @@ -16,6 +17,6 @@ public function show(ShowNotificationGroupRequest $request, #[CurrentUser] User $user, Notification $notification, NotificationGroupsQuery $query): JsonResponse { return response()->json($query->members($user, $notification, $request->type()) - ->with('commit')->latest()->latest('id')->cursorPaginate(20)->withQueryString()); + ->with(['commit' => fn (BelongsTo $query): BelongsTo => $query->withStarsFor($user)])->latest()->latest('id')->cursorPaginate(20)->withQueryString()); } } diff --git a/app/Queries/NotificationGroupsQuery.php b/app/Queries/NotificationGroupsQuery.php index 927458fb8c..6f2f64b6cc 100644 --- a/app/Queries/NotificationGroupsQuery.php +++ b/app/Queries/NotificationGroupsQuery.php @@ -22,7 +22,11 @@ private const int STACKED_ACTORS = 3; - /** @return CursorPaginator */ + /** + * The groups come without their commits, which each caller loads with what it shows of them. + * + * @return CursorPaginator + */ public function paginate(User $user, string $type = 'all', ?CarbonImmutable $through = null): CursorPaginator { $order = 'PARTITION BY group_id ORDER BY CASE WHEN actor_position = 1 THEN 0 ELSE 1 END, created_at DESC, id DESC @@ -33,11 +37,8 @@ public function paginate(User $user, string $type = 'all', ?CarbonImmutable $thr $query = Notification::query()->fromSub($events, 'notifications') ->where('group_rank', 1) - ->with([ - 'actor' => fn (BelongsTo $query): BelongsTo => $query - ->withExists(['followers as is_followed' => fn (Builder $query): Builder => $query->whereKey($user->id)]), - 'commit', - ]) + ->with(['actor' => fn (BelongsTo $query): BelongsTo => $query + ->withExists(['followers as is_followed' => fn (Builder $query): Builder => $query->whereKey($user->id)])]) ->latest()->latest('id'); $perPage = $through instanceof CarbonImmutable ? max(self::PER_PAGE, $this->groupsSince($user, $through, $type)->where('group_rank', 1)->count()) @@ -66,6 +67,20 @@ public function withQuotedCommits(User $user, CursorPaginator $groups): CursorPa return $groups; } + /** + * The inbox loads the commits of its notifications with whether the recipient starred each and how many stars it has, + * so they can star it from there. + * + * @param CursorPaginator $groups + * @return CursorPaginator + */ + public function withCommitStars(User $user, CursorPaginator $groups): CursorPaginator + { + new Collection($groups->items())->load(['commit' => fn (BelongsTo $query): BelongsTo => $query->withStarsFor($user)]); + + return $groups; + } + /** * A star is always on the recipient's own commit, so its images may show beside it: an image-only commit has nothing * else to show. diff --git a/resources/js/pages/notification/index.tsx b/resources/js/pages/notification/index.tsx index 88cc4fefd6..e76b6ef25f 100644 --- a/resources/js/pages/notification/index.tsx +++ b/resources/js/pages/notification/index.tsx @@ -28,6 +28,8 @@ import { show as groupShow } from '@/actions/App/Http/Controllers/NotificationGr import { CommitBodyExcerpt } from '@/components/commit-body'; import { EmptyFeed } from '@/components/commit-feed'; import { replaceFailedImage } from '@/components/commit-images'; +import { CommitQuoteButton } from '@/components/commit-quote-button'; +import { CommitStarButton } from '@/components/commit-star-button'; import { FollowIconButton } from '@/components/follow-button'; import { GraphCanvas } from '@/components/graph-canvas'; import { GraphRail, RailSegment } from '@/components/graph-rail'; @@ -673,6 +675,26 @@ function QuotedOwnCommit({ return ; } +/** + * Stars and quotes the actor's commit right from the notification, as a commit card would. + */ +function CommitActions({ + commit, + author, +}: { + commit: Omit; + author: User; +}) { + const card = { ...commit, user: author }; + + return ( +
+ + +
+ ); +} + export function ActorAvatar({ user, anchor, @@ -760,6 +782,9 @@ export function NotificationContent({ notification.type === 'mentioned' || notification.type === 'first_commit'; const level = notification.type === 'leveled_up' ? notification : null; + const actions = isActorsCommit && ( + + ); const Icon = { followed: UserPlus, leveled_up: ChevronsUp, @@ -895,6 +920,7 @@ export function NotificationContent({ {excerpt} + {actions} ); @@ -1197,6 +1223,7 @@ export function NotificationContent({ {excerpt} {quote} {reason} + {actions} {notification.type === 'followed' && actorsCount === 1 && ( diff --git a/tests/Feature/Controllers/NotificationControllerTest.php b/tests/Feature/Controllers/NotificationControllerTest.php index fc47dc5594..19671a4836 100644 --- a/tests/Feature/Controllers/NotificationControllerTest.php +++ b/tests/Feature/Controllers/NotificationControllerTest.php @@ -129,6 +129,22 @@ expect($user->notifications()->unread()->exists())->toBeFalse(); }); +it('says whether the user starred the commit of a notification, so they can star it from there', function (): void { + $user = User::factory()->create(); + $replied = Notification::factory()->for($user, 'recipient')->replied()->create(); + $replied->commit->stars()->attach([$user->id, User::factory()->create()->id]); + + $this->actingAs($user)->get(route('notification.index')) + ->assertInertia(fn ($page) => $page + ->where('notifications.data.0.commit.starred', true) + ->where('notifications.data.0.commit.stars_count', 2)); + + $this->getJson(route('notification-group.show', $replied)) + ->assertOk() + ->assertJsonPath('data.0.commit.starred', true) + ->assertJsonPath('data.0.commit.stars_count', 2); +}); + it('shows the images of a starred commit', function (): void { $user = User::factory()->create(); $commit = Commit::factory()->for($user)->create(['body' => '']); diff --git a/tests/Unit/Queries/NotificationGroupsQueryTest.php b/tests/Unit/Queries/NotificationGroupsQueryTest.php index ec1ce6fc28..6c850726cf 100644 --- a/tests/Unit/Queries/NotificationGroupsQueryTest.php +++ b/tests/Unit/Queries/NotificationGroupsQueryTest.php @@ -190,7 +190,7 @@ function followAt(User $follower, User $user, int $minutesAgo): void resolve(NotificationGroupsQuery::class)->paginate($user); - expect(DB::getQueryLog())->toHaveCount(6); + expect(DB::getQueryLog())->toHaveCount(5); })->with([1, 5]); it('skips loading stacked actors when nothing is grouped', function (): void {