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
9 changes: 9 additions & 0 deletions KNOWLEDGE_BASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This file documents the platform's features, stack, and stories. Feature entries
- [Achievement badges and XP](#achievement-badges-and-xp)
- [Following developers and suggestions](#following-developers-and-suggestions)
- [Following, Latest, and Trending logs](#following-latest-and-trending-logs)
- [Lists](#lists)
- [Searching developers and commits](#searching-developers-and-commits)
- [Writing commits](#writing-commits)
- [Translating commits](#translating-commits)
Expand Down Expand Up @@ -173,6 +174,14 @@ If a background check hits a temporary connection or gateway failure, the page k

Sources: [log types](app/Enums/Log.php), [log queries](app/Queries/LogQueries), [log page](resources/js/pages/log/show.tsx), [background polling](resources/js/lib/background-poll.ts).

### Lists

A **list** groups developers so you can read only their commits. Open **Lists** from the side navigation or the account menu, name a list (up to 40 characters), then add or remove developers with the list button on their profiles. A list's log shows its members' top-level commits newest first, with the same visibility, mute, and block filtering as Following. Members can also be removed from the list's own page.

Lists are private: only their owner sees them, and nobody is told when they are added. Pin up to **two** lists to show them as tabs next to Following, Latest, and Trending. Each account can have up to **10** lists of up to **250** developers each. You cannot add yourself or someone across a block. Deleting a list only removes the grouping; follows are unaffected.

Sources: [list controller](app/Http/Controllers/UserListController.php), [list model](app/Models/UserList.php), [list log query](app/Queries/UserListCommitsQuery.php), [lists page](resources/js/pages/user-list/index.tsx), [list page](resources/js/pages/user-list/show.tsx), [profile list menu](resources/js/components/user-list-menu.tsx).

### Searching developers and commits

Open search with **⌘/Ctrl+K** or **/**. Developer search finds people by name or GitHub username. Commit search, also available at `/search`, finds posts by meaning rather than requiring an exact phrase: describe the topic you want to read about. Commit queries must contain at least three characters and at most 100, and cover replies as well as top-level commits. An AI relevance check filters the nearest matches, so a search can return fewer results than asked. Results respect visibility, mutes, and blocks; semantic matching does not guarantee that every relevant commit will be returned. Guests can search too. Commit search is limited to 15 searches a minute per person (or per IP for guests).
Expand Down
1 change: 1 addition & 0 deletions PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ What exists, one line each (details in the knowledge base):

- **Commits:** text with code, images, link cards and embedded players, or a poll; replies, quotes, threads, scheduling, translation.
- **Logs:** Following, Latest, and Trending.
- **Lists:** private groups of developers, each with its own log; up to two pinned next to the logs.
- **Engagement:** follows, stars, saved commits, pins, mentions, and pastéis (an opt-in daily allowance of pastries).
- **Profiles:** mirrored from GitHub, with an activity graph, streaks, achievements, a Unix rank, and a live marker while you stream on YouTube.
- **Organization pages:** GitHub organizations get a page mirrored from GitHub once an approved commit mentions them, and `@organization` mentions link to it.
Expand Down
8 changes: 8 additions & 0 deletions app/Actions/CreateExport.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use App\Models\PollOption;
use App\Models\PushSubscription;
use App\Models\User;
use App\Models\UserList;
use App\Models\UserTimeout;
use App\Queries\CommitBuilder;
use App\Services\CommitTranslations;
Expand Down Expand Up @@ -44,6 +45,7 @@
* muting: Generator<int, ExportUser>,
* blocking: Generator<int, ExportUser>,
* dismissed_suggestions: Generator<int, ExportUser>,
* lists: array<int, array{name: string, pinned_at: string|null, created_at: string, members: array<int, string>}>,
* messages: Generator<int, ExportMessage>,
* remote_pins: array<int, array{to: string|null, pinned_at: string|null}>,
* remote_mutes: array<int, array{to: string|null, muted_until_at: string|null}>,
Expand Down Expand Up @@ -110,6 +112,12 @@ public function handle(User $user): array
'muting' => $this->handles($user->muting()),
'blocking' => $this->handles($user->blocking()),
'dismissed_suggestions' => $this->handles($user->dismissedSuggestions()),
'lists' => $user->lists()->oldest()->get()->map(fn (UserList $list): array => [
'name' => $list->name,
'pinned_at' => $list->pinned_at?->toIso8601String(),
'created_at' => $list->created_at->toIso8601String(),
'members' => $list->members()->join('profiles', 'profiles.github_id', '=', 'users.github_id')->orderBy('profiles.handle')->get(['users.*'])->map(fn (User $member): string => $member->handle)->values()->all(),
])->all(),
'messages' => $this->messages($user),
'remote_mutes' => Conversation::query()
->join('conversation_participants as mine', 'mine.conversation_id', '=', 'conversations.id')
Expand Down
16 changes: 16 additions & 0 deletions app/Actions/CreateUserList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace App\Actions;

use App\Models\User;
use App\Models\UserList;

final readonly class CreateUserList
{
public function handle(User $user, string $name): UserList
{
return $user->lists()->create(['name' => $name]);
}
}
19 changes: 19 additions & 0 deletions app/Actions/CreateUserListMember.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace App\Actions;

use App\Models\User;
use App\Models\UserList;

final readonly class CreateUserListMember
{
/**
* Lists are private, so the member is not notified.
*/
public function handle(UserList $list, User $member): void
{
$list->members()->syncWithoutDetaching([$member->id]);
}
}
18 changes: 18 additions & 0 deletions app/Actions/DeleteUserList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace App\Actions;

use App\Models\UserList;

final readonly class DeleteUserList
{
/**
* Its members are only taken off the list, so nobody hears about it.
*/
public function handle(UserList $list): void
{
$list->delete();
}
}
16 changes: 16 additions & 0 deletions app/Actions/DeleteUserListMember.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace App\Actions;

use App\Models\User;
use App\Models\UserList;

final readonly class DeleteUserListMember
{
public function handle(UserList $list, User $member): void
{
$list->members()->detach($member->id);
}
}
15 changes: 15 additions & 0 deletions app/Actions/DeleteUserListPin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace App\Actions;

use App\Models\UserList;

final readonly class DeleteUserListPin
{
public function handle(UserList $list): void
{
$list->update(['pinned_at' => null]);
}
}
20 changes: 20 additions & 0 deletions app/Actions/UpdateUserListPin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace App\Actions;

use App\Models\UserList;

final readonly class UpdateUserListPin
{
/**
* Pinning again keeps the list where it already sits among the tabs.
*/
public function handle(UserList $list): void
{
if (! $list->isPinned()) {
$list->update(['pinned_at' => now()]);
}
}
}
1 change: 1 addition & 0 deletions app/Http/Controllers/LogController.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function show(ShowLogRequest $request, #[CurrentUser] ?User $user, Log $l
'log' => $log,
'commits' => Inertia::scroll(fn (): CursorPaginator => $commits->paginate($log, $user, $request->through())),
'isRanked' => $log->isRanked(),
'pinnedLists' => fn (): Collection => $user?->pinnedLists() ?? new Collection(),
'newCommitsCount' => Inertia::optional(function () use ($request, $user, $log, $latest): int {
$seen = $request->since();

Expand Down
9 changes: 9 additions & 0 deletions app/Http/Controllers/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use App\Http\Requests\ShowUserRequest;
use App\Models\Commit;
use App\Models\User;
use App\Models\UserList;
use App\Queries\BioProfilesQuery;
use App\Queries\FollowersYouKnowQuery;
use App\Queries\GitHubReadmeQuery;
Expand All @@ -22,6 +23,7 @@
use App\Queries\UserStreakQuery;
use App\Services\PageMetadata;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\RedirectResponse;
use Illuminate\Pagination\CursorPaginator;
use Illuminate\Support\Facades\Auth;
Expand Down Expand Up @@ -64,6 +66,13 @@ public function show(ShowUserRequest $request, #[CurrentUser] ?User $currentUser
'followersYouKnow' => fn (): ?array => $currentUser instanceof User && $currentUser->isNot($user)
? $followersYouKnow->preview($user, $currentUser)
: null,
'lists' => Inertia::optional(fn (): array => $currentUser instanceof User ? $currentUser->lists()
->withExists(['members as has_member' => fn (Builder $query): Builder => $query->whereKey($user->id)])
->get()
->sortBy('name', SORT_NATURAL | SORT_FLAG_CASE)
->values()
->map(fn (UserList $list): array => ['id' => $list->id, 'name' => $list->name, 'has_member' => (bool) $list->getAttribute('has_member')])
->all() : []),
'canPush' => fn (): bool => $currentUser instanceof User
&& $currentUser->canMessage($user)
&& ! in_array($pushAccess->get($currentUser, $user), [PushAccess::Restricted, PushAccess::Closed], true),
Expand Down
67 changes: 67 additions & 0 deletions app/Http/Controllers/UserListController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Actions\CreateUserList;
use App\Actions\DeleteUserList;
use App\Http\Requests\CreateUserListRequest;
use App\Http\Requests\DeleteUserListRequest;
use App\Http\Requests\ShowUserListRequest;
use App\Models\User;
use App\Models\UserList;
use App\Queries\UserListCommitsQuery;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;

final readonly class UserListController
{
public function index(#[CurrentUser] User $user): Response
{
return Inertia::render('user-list/index', [
// Names are encrypted, so they are sorted here rather than by the database.
'lists' => fn (): Collection => $user->lists()->withCount(['members' => fn (Builder $query): Builder => $query->whereNotIn('users.id', $user->idsAcrossBlocks())])->get()->sortBy('name', SORT_NATURAL | SORT_FLAG_CASE)->values(),
'maxLists' => UserList::MAX_PER_USER,
'maxPinned' => UserList::MAX_PINNED,
]);
}

public function store(CreateUserListRequest $request, #[CurrentUser] User $user, CreateUserList $action): RedirectResponse
{
$list = $action->handle($user, $request->name());

Inertia::flash('toast', [
'type' => 'success',
'message' => "Created {$list->name}. Add developers to it from their profiles.",
]);

return back();
}

/**
* Someone across a block stays on the list, but its owner no longer sees or counts them there, as anywhere else.
*/
public function show(ShowUserListRequest $request, #[CurrentUser] User $user, UserList $userList, UserListCommitsQuery $commits): Response
{
return Inertia::render('user-list/show', [
'list' => $userList->loadCount(['members' => fn (Builder $query): Builder => $query->whereNotIn('users.id', $user->idsAcrossBlocks())]),
'members' => fn (): Collection => $userList->members()->whereNotIn('users.id', $user->idsAcrossBlocks())->join('profiles', 'profiles.github_id', '=', 'users.github_id')->orderBy('profiles.handle')->get(),
'pinnedLists' => $user->pinnedLists(...),
'commits' => Inertia::scroll(fn () => $commits->builder($user, $userList)->cursorPaginate(20)),
]);
}

public function destroy(DeleteUserListRequest $request, UserList $userList, DeleteUserList $action): RedirectResponse
{
$action->handle($userList);

Inertia::flash('toast', ['type' => 'success', 'message' => "Deleted {$userList->name}."]);

return to_route('user-list.index');
}
}
35 changes: 35 additions & 0 deletions app/Http/Controllers/UserListMemberController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Actions\CreateUserListMember;
use App\Actions\DeleteUserListMember;
use App\Http\Requests\CreateUserListMemberRequest;
use App\Http\Requests\DeleteUserListMemberRequest;
use App\Models\User;
use App\Models\UserList;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;

final readonly class UserListMemberController
{
public function store(CreateUserListMemberRequest $request, UserList $userList, User $user, CreateUserListMember $action): RedirectResponse
{
$action->handle($userList, $user);

Inertia::flash('toast', ['type' => 'success', 'message' => "Added @{$user->handle} to {$userList->name}."]);

return back();
}

public function destroy(DeleteUserListMemberRequest $request, UserList $userList, User $user, DeleteUserListMember $action): RedirectResponse
{
$action->handle($userList, $user);

Inertia::flash('toast', ['type' => 'success', 'message' => "Removed @{$user->handle} from {$userList->name}."]);

return back();
}
}
34 changes: 34 additions & 0 deletions app/Http/Controllers/UserListPinController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Actions\DeleteUserListPin;
use App\Actions\UpdateUserListPin;
use App\Http\Requests\DeleteUserListPinRequest;
use App\Http\Requests\UpdateUserListPinRequest;
use App\Models\UserList;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;

final readonly class UserListPinController
{
public function update(UpdateUserListPinRequest $request, UserList $userList, UpdateUserListPin $action): RedirectResponse
{
$action->handle($userList);

Inertia::flash('toast', ['type' => 'success', 'message' => "Pinned {$userList->name} next to your logs."]);

return back();
}

public function destroy(DeleteUserListPinRequest $request, UserList $userList, DeleteUserListPin $action): RedirectResponse
{
$action->handle($userList);

Inertia::flash('toast', ['type' => 'success', 'message' => "Unpinned {$userList->name}."]);

return back();
}
}
Loading