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
20 changes: 20 additions & 0 deletions app/Devnull/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Devnull\Data\Commit;
use App\Devnull\Data\CommitThread;
use App\Devnull\Data\Connection;
use App\Devnull\Data\CurrentUser;
use App\Devnull\Data\Notification;
use App\Devnull\Data\Page;
Expand Down Expand Up @@ -220,6 +221,25 @@ public function userCommits(string $username, string $tab = 'commits', ?string $
return Page::fromArray($this->get('users/'.rawurlencode($username).'/commits', $query), Commit::fromArray(...));
}

/**
* The people following someone, or with 'following' the people they
* follow, newest follow first as the web lists them, 30 a page.
*
* @return Page<Connection>
*/
public function connections(string $username, string $type, ?string $cursor = null): Page
{
return Page::fromArray($this->get(self::connectionsPath($username, $type), ['cursor' => $cursor]), Connection::fromArray(...));
}

/**
* Where someone's followers are, or with 'following' the people they follow.
*/
public static function connectionsPath(string $username, string $type): string
{
return 'users/'.rawurlencode($username).'/'.($type === 'following' ? 'following' : 'followers');
}

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

namespace App\Devnull\Data;

/**
* Someone in a profile's followers or following, as the web lists them, with
* whether you follow them and whether they follow you.
*/
final readonly class Connection
{
/**
* Their user id, so a fresh page can be laid over the list (see FreshPage).
*/
public string $id;

public function __construct(
public User $user,
public bool $isFollowed = false,
public bool $followsYou = false,
) {
$this->id = $user->id;
}

/**
* Reads one of `GET /users/{username}/followers` or `/following`: the user,
* with `is_followed` and `follows_you` beside their profile.
*
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
user: User::fromArray($data),
isFollowed: ($data['is_followed'] ?? false) === true,
followsYou: ($data['follows_you'] ?? false) === true,
);
}

public function withFollowed(bool $isFollowed): self
{
return new self($this->user, $isFollowed, $this->followsYou);
}
}
76 changes: 69 additions & 7 deletions app/Devnull/ResponseCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ final class ResponseCache
'#^users/[^/]+$#',
'#^users/[^/]+/commits$#',
'#^saved$#',
'#^users/[^/]+/(followers|following)$#',
];

/**
* The kept answers that list people with whether you follow each one.
*/
private const array FollowedPeople = [
'#^users/[^/]+/(followers|following)$#',
'#^notifications$#',
'#^notifications/[^/]+/group$#',
];

/**
Expand Down Expand Up @@ -344,21 +354,40 @@ public function commitDeleted(string $id): void
}

/**
* You followed or unfollowed someone: their profile and your following count change.
* You followed or unfollowed someone: their profile and your following
* count change, and every list of people kept with whether you follow them.
* Without their profile kept, the screen's word is taken that it changed.
*/
public function followChanged(string $username, bool $following): void
{
$changed = false;
$kept = $this->get("users/{$username}");
$changed = $kept === null || ($kept['is_following'] ?? false) !== $following;

$this->patch("users/{$username}", [], function (array $json) use ($following, &$changed): array {
$changed = ($json['is_following'] ?? false) !== $following;

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

if ($changed && ($me = $this->username()) !== null) {
$this->patch("users/{$me}", [], fn (array $json): array => self::count($json, 'following_count', $following ? 1 : -1));
}

$this->patchFollowedPeople(rawurldecode($username), $following);
}

/**
* Marks someone followed, or not, wherever a kept list of people says whether you follow them.
*/
private function patchFollowedPeople(string $username, bool $following): void
{
$userId = $this->userId();

foreach ($userId === null ? [] : $this->index($userId) as $meta) {
if (! self::listsFollowedPeople($meta['path'])) {
continue;
}

$this->patch($meta['path'], $meta['query'], fn (array $json): array => self::changeFollowed($json, $username, $following));
}
}

/**
Expand Down Expand Up @@ -662,6 +691,39 @@ private static function changeUser(mixed $node, string $id, array $profile): mix
return $node;
}

private static function listsFollowedPeople(string $path): bool
{
foreach (self::FollowedPeople as $pattern) {
if (preg_match($pattern, $path) === 1) {
return true;
}
}

return false;
}

/**
* Gives every copy of the user with this username, anywhere in $node, that says whether you follow them, the new answer.
*/
private static function changeFollowed(mixed $node, string $username, bool $following): mixed
{
if (! is_array($node)) {
return $node;
}

if (($node['username'] ?? null) === $username && array_key_exists('is_followed', $node)) {
$node['is_followed'] = $following;
}

foreach ($node as $key => $child) {
if (is_array($child)) {
$node[$key] = self::changeFollowed($child, $username, $following);
}
}

return $node;
}

/**
* devnull marks notifications read as it sends them, and you've seen them
* by the time they're shown again, so they're kept as read, and without
Expand Down
Loading
Loading