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
1 change: 1 addition & 0 deletions app/Http/Resources/UserResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public function toArray(Request $request): array
'commits_count' => $this->whenCounted('commits'),
'followers_count' => $this->whenCounted('followers'),
'following_count' => $this->whenCounted('following'),
'contributor' => $this->isRepositoryContributor(),
'lane' => $this->when($request->user() instanceof User, fn (): int|string => $this->lane()),
'is_followed' => $this->whenHas('is_followed'),
'follows_you' => $this->whenHas('follows_you'),
Expand Down
10 changes: 10 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use App\Enums\LivePlatform;
use App\Enums\Log;
use App\Enums\RemotesFrom;
use App\Queries\RepositoryContributorsQuery;
use Carbon\CarbonInterface;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Hidden;
Expand Down Expand Up @@ -534,6 +535,14 @@ public function automaticLane(): int
return $hash % 6;
}

/**
* Whether this GitHub username has commits on the /dev/null repository.
*/
public function isRepositoryContributor(): bool
{
return resolve(RepositoryContributorsQuery::class)->contains($this->username);
}

/**
* @return array<mixed>
*/
Expand All @@ -543,6 +552,7 @@ public function toArray(): array
...parent::toArray(),
'lane' => $this->lane(),
'has_github_account' => $this->hasGitHubAccount(),
'contributor' => $this->isRepositoryContributor(),
'live' => $this->liveStream()?->toArray(),
];
}
Expand Down
98 changes: 98 additions & 0 deletions app/Queries/RepositoryContributorsQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

declare(strict_types=1);

namespace App\Queries;

use App\Services\GitHub;
use Illuminate\Http\Client\HttpClientException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Fluent;
use RuntimeException;
use Throwable;

/**
* GitHub accounts that have commits on the repository's default branch. Cached; soft-fails to nobody when GitHub is down.
*/
final readonly class RepositoryContributorsQuery
{
private const string CacheKey = 'repository-contributors:usernames';

private const int CacheSeconds = 900;

private const int MaxPages = 5;

public function __construct(private GitHub $github)
{
//
}

public function contains(string $username): bool
{
return in_array(mb_strtolower($username), $this->usernames(), true);
}

/**
* @return list<string>
*/
public function usernames(): array
{
return once(function (): array {
try {
/** @var list<string>|null $cached */
$cached = Cache::get(self::CacheKey);

if (is_array($cached)) {
return $cached;
}
} catch (Throwable) {
//
}

try {
$usernames = $this->fetch();

try {
Cache::put(self::CacheKey, $usernames, self::CacheSeconds);
} catch (Throwable) {
//
}

return $usernames;
} catch (HttpClientException|RuntimeException) {
return [];
}
});
}

/**
* @return list<string>
*/
private function fetch(): array
{
$usernames = [];

$repository = config()->string('services.github.repository');

for ($page = 1; $page <= self::MaxPages; $page++) {
$contributors = $this->github->request()->get('repos/'.$repository.'/contributors', [
'per_page' => 100,
'page' => $page,
])->throw()->collect();

foreach ($contributors->where('type', 'User')->mapInto(Fluent::class) as $contributor) {
$login = mb_strtolower($contributor->string('login')->value());

if ($login !== '') {
$usernames[] = $login;
}
}

if ($contributors->count() < 100) {
break;
}
}

return array_values(array_unique($usernames));
}
}
8 changes: 6 additions & 2 deletions resources/js/components/app-aside.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import TermsController from '@/actions/App/Http/Controllers/TermsController';
import UserFollowersController from '@/actions/App/Http/Controllers/UserFollowersController';
import UserFollowingController from '@/actions/App/Http/Controllers/UserFollowingController';
import { BioText } from '@/components/bio-text';
import { ContributorBadge } from '@/components/contributor-badge';
import { GitHubIcon } from '@/components/github-icon';
import { LiveFollowing } from '@/components/live-following';
import { Button } from '@/components/ui/button';
Expand Down Expand Up @@ -97,8 +98,11 @@ export function AppAside({
>
<UserAvatar user={user} className="size-12" />
<span className="min-w-0">
<span className="block truncate font-semibold group-hover:underline">
{user.name}
<span className="flex min-w-0 items-center gap-1.5">
<span className="truncate font-semibold group-hover:underline">
{user.name}
</span>
{user.contributor && <ContributorBadge />}
</span>
<span className="flex items-center gap-1.5 font-mono text-sm text-muted-foreground">
<span
Expand Down
4 changes: 4 additions & 0 deletions resources/js/components/commit-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
useThread,
} from '@/components/commit-thread';
import { useCommitTranslation } from '@/components/commit-translation';
import { ContributorBadge } from '@/components/contributor-badge';
import {
GraphDimmed,
GraphScope,
Expand Down Expand Up @@ -344,6 +345,9 @@ export function CommitCard({
>
{commit.user.name}
</Link>
{commit.user.contributor && (
<ContributorBadge />
)}
<Link
href={show(commit.user)}
tabIndex={-1}
Expand Down
2 changes: 2 additions & 0 deletions resources/js/components/commit-thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
PreviewToggle,
useComposerPreview,
} from '@/components/composer-preview';
import { ContributorBadge } from '@/components/contributor-badge';
import {
useGraphDimmed,
useGraphNode,
Expand Down Expand Up @@ -1174,6 +1175,7 @@ function CommitReply({
>
{reply.user.name}
</Link>
{reply.user.contributor && <ContributorBadge />}
<Link
href={show(reply.user)}
tabIndex={-1}
Expand Down
30 changes: 30 additions & 0 deletions resources/js/components/contributor-badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import AppLogoIcon from '@/components/app-logo-icon';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';

export function ContributorBadge({ className }: { className?: string }) {
return (
<Tooltip>
<TooltipTrigger asChild>
<span
className={cn(
'inline-flex shrink-0 items-center self-center text-foreground',
className,
)}
data-test="contributor-badge"
>
<AppLogoIcon
aria-hidden
className="size-3.5 fill-current"
/>
<span className="sr-only">Contributor</span>
</span>
</TooltipTrigger>
<TooltipContent>Contributor</TooltipContent>
</Tooltip>
);
}
2 changes: 2 additions & 0 deletions resources/js/components/quoted-commit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CommitBodyExcerpt } from '@/components/commit-body';
import { CommitImages } from '@/components/commit-images';
import { CommitPoll } from '@/components/commit-poll';
import { CommitStatusBadge } from '@/components/commit-status-badge';
import { ContributorBadge } from '@/components/contributor-badge';
import { LinkPreview } from '@/components/link-preview';
import { UserAvatar } from '@/components/user-avatar';
import { useNow } from '@/hooks/use-now';
Expand Down Expand Up @@ -50,6 +51,7 @@ export function QuotedCommit({
<span className="truncate font-semibold">
{commit.user.name}
</span>
{commit.user.contributor && <ContributorBadge />}
<span className="truncate font-mono text-xs text-muted-foreground">
@{commit.user.username}
</span>
Expand Down
6 changes: 4 additions & 2 deletions resources/js/components/user-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import UserFollowController from '@/actions/App/Http/Controllers/UserFollowContr
import UserFollowersController from '@/actions/App/Http/Controllers/UserFollowersController';
import UserFollowingController from '@/actions/App/Http/Controllers/UserFollowingController';
import { BioText } from '@/components/bio-text';
import { ContributorBadge } from '@/components/contributor-badge';
import {
FollowLabel,
followVisitOptions,
Expand Down Expand Up @@ -453,9 +454,10 @@ function UserPreviewContent({

<Link
href={show(user)}
className="mt-3 block truncate font-semibold hover:underline"
className="mt-3 flex min-w-0 items-center gap-1.5 font-semibold hover:underline"
>
{user.name}
<span className="truncate">{user.name}</span>
{user.contributor && <ContributorBadge />}
</Link>
<p className="flex items-center gap-1.5 font-mono text-[13px] text-muted-foreground">
<span
Expand Down
8 changes: 6 additions & 2 deletions resources/js/components/who-to-follow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Deferred, Link, router, usePage } from '@inertiajs/react';
import { Ellipsis, EyeOff, RefreshCw, X } from 'lucide-react';
import { useRef, useState } from 'react';
import SuggestionDismissalController from '@/actions/App/Http/Controllers/SuggestionDismissalController';
import { ContributorBadge } from '@/components/contributor-badge';
import {
FollowButton,
FollowIconButton,
Expand Down Expand Up @@ -180,8 +181,11 @@ export function SuggestedUserRow({
<UserAvatar user={user} className="size-9" />
</span>
<span className="min-w-0">
<span className="block truncate text-sm font-semibold group-hover:underline">
{user.name}
<span className="flex min-w-0 items-center gap-1">
<span className="truncate text-sm font-semibold group-hover:underline">
{user.name}
</span>
{user.contributor && <ContributorBadge />}
</span>
<span className="flex min-w-0 items-center gap-1.5 font-mono text-xs text-muted-foreground">
<span
Expand Down
11 changes: 8 additions & 3 deletions resources/js/pages/user/show.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ActivityGraph } from '@/components/activity-graph';
import { ActivityStreaks } from '@/components/activity-streaks';
import { BioText } from '@/components/bio-text';
import { CommitFeed, EmptyFeed } from '@/components/commit-feed';
import { ContributorBadge } from '@/components/contributor-badge';
import { EditProfileMenu } from '@/components/edit-profile-menu';
import { FollowButton } from '@/components/follow-button';
import { GitHubIcon } from '@/components/github-icon';
Expand Down Expand Up @@ -116,7 +117,10 @@ export default function Show({
<Head title={`${user.name} (@${user.username})`} />

<div className="sticky top-0 z-10 flex h-14 items-center gap-3 border-b bg-card px-4 sm:px-6">
<p className="min-w-0 truncate font-semibold">{user.name}</p>
<p className="flex min-w-0 items-center gap-1.5 font-semibold">
<span className="truncate">{user.name}</span>
{user.contributor && <ContributorBadge />}
</p>
<p
className="shrink-0 font-mono text-[13px] text-muted-foreground"
title={exactCountTitle(user.commits_count)}
Expand Down Expand Up @@ -215,8 +219,9 @@ export default function Show({
</div>
</div>

<h1 className="mt-4 text-2xl font-semibold tracking-tight text-balance">
{user.name}
<h1 className="mt-4 flex items-center gap-2 text-2xl font-semibold tracking-tight text-balance">
<span className="min-w-0 truncate">{user.name}</span>
{user.contributor && <ContributorBadge />}
</h1>
<div className="flex items-center gap-2">
<p className="flex items-center gap-1.5 font-mono text-sm text-muted-foreground">
Expand Down
4 changes: 4 additions & 0 deletions resources/js/types/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export type User = {
company: string | null;
has_sponsors_listing: boolean;
has_github_account: boolean;
/**
* Whether this person has commits on the /dev/null GitHub repository.
*/
contributor: boolean;
lane: Lane;
timezone: string | null;
show_local_time: boolean;
Expand Down
2 changes: 2 additions & 0 deletions tests/Feature/Controllers/Api/CommitControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
'html_url' => route('user.show', $user),
'feed_url' => route('user-feed.show', $user),
'created_at' => now()->toIso8601ZuluString(),
'contributor' => false,
],
]]);
});
Expand Down Expand Up @@ -231,6 +232,7 @@
'html_url' => route('user.show', $author),
'feed_url' => route('user-feed.show', $author),
'created_at' => now()->toIso8601ZuluString(),
'contributor' => false,
'lane' => $author->lane(),
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
'commits_count' => 1,
'followers_count' => 3,
'following_count' => 2,
'contributor' => false,
'lane' => $user->lane(),
'pasteis_mode' => true,
'pasteis_remaining' => 7,
Expand Down
1 change: 1 addition & 0 deletions tests/Feature/Controllers/Api/LogControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
'html_url' => route('user.show', $author),
'feed_url' => route('user-feed.show', $author),
'created_at' => now()->toIso8601ZuluString(),
'contributor' => false,
'lane' => $author->lane(),
],
'body_segments' => [['type' => 'text', 'text' => 'It was DNS.']],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
'html_url' => route('user.show', $user),
'feed_url' => route('user-feed.show', $user),
'created_at' => now()->toIso8601ZuluString(),
'contributor' => false,
'lane' => $user->lane(),
];

Expand Down
2 changes: 2 additions & 0 deletions tests/Feature/Controllers/Api/UserControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
'commits_count' => 2,
'followers_count' => 3,
'following_count' => 1,
'contributor' => false,
]]);
});

Expand Down Expand Up @@ -115,6 +116,7 @@
'commits_count' => 3,
'followers_count' => 1,
'following_count' => 2,
'contributor' => false,
'lane' => $user->lane(),
],
'is_following' => true,
Expand Down
Loading