Skip to content
Merged
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
30 changes: 30 additions & 0 deletions app/Devnull/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,36 @@ public static function connectionsPath(string $username, string $type): string
return 'users/'.rawurlencode($username).'/'.($type === 'following' ? 'following' : 'followers');
}

/**
* A tab of an organization's page: the commits it posted, or with 'mentions' the commits that mention it.
*
* @return Page<Commit>
*/
public function organizationCommits(string $handle, string $tab = 'commits', ?string $cursor = null): Page
{
return Page::fromArray($this->get(self::organizationCommitsPath($handle, $tab), ['cursor' => $cursor]), Commit::fromArray(...));
}

/**
* Where an organization's commits are, or with 'mentions' the commits that mention it.
*/
public static function organizationCommitsPath(string $handle, string $tab): string
{
return 'organizations/'.rawurlencode($handle).'/'.($tab === 'mentions' ? 'mentions' : 'commits');
}

public function followOrganization(string $handle): void
{
$this->send('POST', 'organizations/'.rawurlencode($handle).'/follow');
$this->cache->organizationFollowChanged(rawurlencode($handle), true);
}

public function unfollowOrganization(string $handle): void
{
$this->send('DELETE', 'organizations/'.rawurlencode($handle).'/follow');
$this->cache->organizationFollowChanged(rawurlencode($handle), false);
}

public function follow(string $username): void
{
$this->send('POST', 'users/'.rawurlencode($username).'/follow');
Expand Down
47 changes: 47 additions & 0 deletions app/Devnull/Data/Organization.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace App\Devnull\Data;

/**
* An organization's page, as `GET /organizations/{handle}` sends it: the
* organization with the fields a user has, how many follow it, its admins,
* first one first, and whether you follow it.
*/
final readonly class Organization
{
/**
* @param list<User> $admins
*/
public function __construct(
public User $organization,
public int $followersCount = 0,
public array $admins = [],
public bool $isFollowing = false,
) {}

/**
* @param array<string, mixed> $json
*/
public static function fromArray(array $json): self
{
$data = is_array($json['data'] ?? null) ? $json['data'] : [];
$admins = is_array($data['admins'] ?? null) ? $data['admins'] : [];

return new self(
organization: User::fromArray(['type' => 'organization', ...$data]),
followersCount: max(0, (int) ($data['followers_count'] ?? 0)),
admins: array_values(array_map(User::fromArray(...), array_filter($admins, fn (mixed $admin): bool => is_array($admin) && is_string($admin['handle'] ?? null) && $admin['handle'] !== ''))),
isFollowing: ($json['is_following'] ?? $data['is_following'] ?? false) === true,
);
}

/**
* Followed or unfollowed here before devnull answers, with the count moved to match, as the web does.
*/
public function withFollowing(bool $isFollowing): self
{
$change = $isFollowing === $this->isFollowing ? 0 : ($isFollowing ? 1 : -1);

return new self($this->organization, max(0, $this->followersCount + $change), $this->admins, $isFollowing);
}
}
11 changes: 8 additions & 3 deletions app/Devnull/Data/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public function __construct(
public ?string $feedUrl = null,
public ?CarbonImmutable $createdAt = null,
public ?Live $live = null,
public bool $isOrganization = false,
) {}

/**
Expand All @@ -52,13 +53,14 @@ public static function fromArray(array $data): self
feedUrl: self::text($data['feed_url'] ?? null),
createdAt: Commit::date($data['created_at'] ?? null),
live: Live::fromArray($data['live'] ?? null),
isOrganization: ($data['type'] ?? null) === 'organization',
);
}

/**
* The user in the API's shape, so it can be kept and read back.
*
* @return array{id: string, handle: string, name: string, avatar_url: ?string, bio: ?string, lane: int|string|null, company: ?string, location: ?string, website: ?string, github_url: ?string, html_url: ?string, feed_url: ?string, created_at: ?string, live: array{platform: ?string, url: string, thumbnail_url: ?string, ends_at: string}|null}
* @return array{id: string, handle: string, name: string, avatar_url: ?string, bio: ?string, lane: int|string|null, company: ?string, location: ?string, website: ?string, github_url: ?string, html_url: ?string, feed_url: ?string, created_at: ?string, live: array{platform: ?string, url: string, thumbnail_url: ?string, ends_at: string}|null, type: string}
*/
public function toArray(): array
{
Expand All @@ -77,6 +79,7 @@ public function toArray(): array
'feed_url' => $this->feedUrl,
'created_at' => $this->createdAt?->toIso8601ZuluString(),
'live' => $this->live?->toArray(),
'type' => $this->isOrganization ? 'organization' : 'user',
];
}

Expand All @@ -91,11 +94,13 @@ public function isLive(): bool
/**
* The lane the user picked on the web, a palette lane or a custom
* `#rrggbb` colour, or the one devnull gave them. Hashing the id is only
* for a server too old to send it.
* for a server too old to send it. An organization whose admins haven't
* picked one is in Commit Blue, as the web draws it, on its page and on
* its commits alike.
*/
public function lane(): int|string
{
return $this->lane ?? Lane::for($this->id);
return $this->lane ?? ($this->isOrganization ? Lane::Primary : Lane::for($this->id));
}

public function handle(): string
Expand Down
8 changes: 7 additions & 1 deletion app/Devnull/Lane.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ final class Lane
*/
public const string Rail = 'rail';

/**
* Commit Blue, which an organization is drawn in until its admins pick a lane, as the web's
* organizationLaneColor() draws it.
*/
public const string Primary = 'primary';

/**
* Reads a lane from the API: a palette lane, a custom colour as lowercase
* `#rrggbb`, or null when it's missing or neither.
Expand Down Expand Up @@ -76,7 +82,7 @@ public static function isCustom(int|string|null $lane): bool
public static function token(int|string|null $lane): string
{
return match (true) {
$lane === null => 'primary',
$lane === null, $lane === self::Primary => 'primary',
$lane === self::Rail => 'rail',
is_string($lane) => self::isCustom($lane) ? self::customToken(strtolower($lane)) : 'primary',
default => 'lane-'.$lane,
Expand Down
12 changes: 12 additions & 0 deletions app/Devnull/ResponseCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,18 @@ public function followChanged(string $username, bool $following): void
$this->patchFollowedPeople(rawurldecode($username), $following);
}

/**
* You followed or unfollowed an organization: its kept page says so, with its followers counted to match.
*/
public function organizationFollowChanged(string $handle, bool $following): void
{
$kept = $this->get("organizations/{$handle}");

if ($kept !== null && ($kept['is_following'] ?? false) !== $following) {
$this->patch("organizations/{$handle}", [], fn (array $json): array => [...self::count($json, 'followers_count', $following ? 1 : -1), 'is_following' => $following]);
}
}

/**
* Marks someone followed, or not, wherever a kept list of people says whether you follow them.
*/
Expand Down
2 changes: 2 additions & 0 deletions app/Icons/Lucide.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ enum Lucide
case Phone;
case PhoneOff;
case MicOff;
case Share2;

/**
* The icon the web's inbox draws for a kind of notification.
Expand Down Expand Up @@ -233,6 +234,7 @@ public function custom(bool $filled = false): ?string
self::Phone => 'phone',
self::PhoneOff => 'phone-off',
self::MicOff => 'mic-off',
self::Share2 => 'share-2',
default => null,
};

Expand Down
10 changes: 9 additions & 1 deletion app/NativeComponents/Concerns/ShowsCommits.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,20 @@ public function openReplies(string $id): void
}

/**
* Opens a profile with what a row already knows of its author.
* Opens a profile with what a row already knows of its author. An
* organization that posted the commit opens on its organization page,
* since devnull has no user by its handle.
*/
public function openProfile(string $username): void
{
$author = $this->shownAuthor($username);

if ($author?->isOrganization === true) {
$this->openOrganization($username);

return;
}

$this->openInTab('/users/'.$username, $author === null ? [] : ['user' => $author]);
}

Expand Down
Loading
Loading