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
2 changes: 1 addition & 1 deletion app/Http/Controllers/NotificationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
]);
Expand Down
3 changes: 2 additions & 1 deletion app/Http/Controllers/NotificationGroupController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
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
{
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());
}
}
27 changes: 21 additions & 6 deletions app/Queries/NotificationGroupsQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@

private const int STACKED_ACTORS = 3;

/** @return CursorPaginator<int, Notification> */
/**
* The groups come without their commits, which each caller loads with what it shows of them.
*
* @return CursorPaginator<int, Notification>
*/
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
Expand All @@ -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())
Expand Down Expand Up @@ -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<int, Notification> $groups
* @return CursorPaginator<int, Notification>
*/
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.
Expand Down
27 changes: 27 additions & 0 deletions resources/js/pages/notification/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -673,6 +675,26 @@ function QuotedOwnCommit({
return <OwnCommitExcerpt segments={commit.body_segments} />;
}

/**
* Stars and quotes the actor's commit right from the notification, as a commit card would.
*/
function CommitActions({
commit,
author,
}: {
commit: Omit<Commit, 'user' | 'quoted_commit'>;
author: User;
}) {
const card = { ...commit, user: author };

return (
<div className="relative z-10 mt-1 -ml-2 flex w-fit items-center gap-0.5 text-[13px] not-has-[button]:hidden">
<CommitStarButton commit={card} className="px-2 py-0.5" />
<CommitQuoteButton commit={card} />
</div>
);
}

export function ActorAvatar({
user,
anchor,
Expand Down Expand Up @@ -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 && (
<CommitActions commit={notification.commit} author={actor} />
);
const Icon = {
followed: UserPlus,
leveled_up: ChevronsUp,
Expand Down Expand Up @@ -895,6 +920,7 @@ export function NotificationContent({
<Link href={href} tabIndex={-1} className="block">
{excerpt}
</Link>
{actions}
</div>
</div>
);
Expand Down Expand Up @@ -1197,6 +1223,7 @@ export function NotificationContent({
{excerpt}
{quote}
{reason}
{actions}
</div>
{notification.type === 'followed' && actorsCount === 1 && (
<span className="flex h-10 items-center">
Expand Down
16 changes: 16 additions & 0 deletions tests/Feature/Controllers/NotificationControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => '']);
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Queries/NotificationGroupsQueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down