Skip to content
Draft
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: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ These are the next things we want in the app. If you'd like to take one on, read
- Styling for livestreams
- Search
- A notifications centre that matches the web
- Who to follow
- A better welcome screen, with sign up and log in that don't mention GitHub, and maybe the public feed
- Everything Remotes does on the web, including audio recordings and image uploads
- WebSockets
Expand Down
13 changes: 13 additions & 0 deletions app/Devnull/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@

public const string BlockedPath = 'me/blocked';

/**
* Who to follow: three people to show and three spares, the next page of the day's picks for each ?round=.
*/
public const string SuggestedUsersPath = 'me/suggested-users';

/**
* Where pastéis mode is switched on and off for the signed-in account.
*/
Expand Down Expand Up @@ -429,6 +434,14 @@ public function unfollow(string $username): void
$this->cache->followChanged(rawurlencode($username), false);
}

/**
* Not interested, as the web's who to follow says it: devnull never suggests them again.
*/
public function dismissSuggestion(string $username): void
{
$this->send('POST', 'users/'.rawurlencode($username).'/dismiss');
}

/**
* Mutes someone, as the web's Mute does. It's silent: they aren't told,
* and the follows between you stay. Their commits leave your logs.
Expand Down
60 changes: 60 additions & 0 deletions app/Devnull/Data/SuggestedUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace App\Devnull\Data;

use App\Devnull\Display;

/**
* Someone who to follow suggests, as the web lists them: with whether you
* follow them now, whether they follow you, and the counts that say why
* they're suggested.
*/
final readonly class SuggestedUser
{
public string $id;

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

/**
* Reads one of `GET /me/suggested-users`.
*
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
user: User::fromArray($data),
followersCount: is_int($data['followers_count'] ?? null) ? $data['followers_count'] : 0,
followedByFollowingCount: is_int($data['followed_by_following_count'] ?? null) ? $data['followed_by_following_count'] : 0,
followsYou: ($data['follows_you'] ?? false) === true,
isFollowed: ($data['is_followed'] ?? false) === true,
);
}

public function withFollowed(bool $isFollowed): self
{
return new self($this->user, $this->followersCount, $this->followedByFollowingCount, $this->followsYou, $isFollowed);
}

/**
* Why they're suggested, as the web words it: how many people you follow
* follow them, or else how many follow them at all.
*/
public function reason(): ?string
{
return match (true) {
$this->followedByFollowingCount > 0 => 'Followed by '.Display::compactCount($this->followedByFollowingCount).' you follow',
$this->followersCount === 1 => '1 follower',
$this->followersCount > 1 => Display::compactCount($this->followersCount).' followers',
default => null,
};
}
}
23 changes: 22 additions & 1 deletion app/Devnull/Data/TimelineRow.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace App\Devnull\Data;

/**
* One row on the graph rail: a commit, a day marker or the last-visit marker.
* One row on the graph rail: a commit, a day marker, the last-visit marker or who to follow.
*
* $previousLane is the lane of the segment running into the row from above,
* or null when nothing runs into it. In a feed a commit carries on its
Expand Down Expand Up @@ -56,6 +56,22 @@ public static function lastVisit(int|string|null $previousLane): self
return new self('last-visit', $previousLane);
}

/**
* Who to follow, between two commits, with no rail running through it.
*/
public static function whoToFollow(): self
{
return new self('who-to-follow', null);
}

/**
* This commit row starting a run of its own, as the first under a row that breaks the rail.
*/
public function startingRun(): self
{
return new self($this->kind, null, $this->commit, label: $this->label);
}

public function isCommit(): bool
{
return $this->kind === 'commit';
Expand All @@ -70,4 +86,9 @@ public function isLastVisitMarker(): bool
{
return $this->kind === 'last-visit';
}

public function isWhoToFollow(): bool
{
return $this->kind === 'who-to-follow';
}
}
1 change: 1 addition & 0 deletions app/Devnull/RowWindow.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ private static function key(TimelineRow $row): string
return match (true) {
$row->isDayMarker() => 'day-'.$row->label,
$row->isLastVisitMarker() => 'last-visit',
$row->isWhoToFollow() => 'who-to-follow',
default => 'commit-'.$row->commit?->id,
};
}
Expand Down
39 changes: 35 additions & 4 deletions app/Devnull/Timeline.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ final class Timeline
* A feed, like the web's CommitFeed: a day marker wherever the day changes and
* the last-visit marker above the first commit you had already seen.
*
* Given $whoToFollowAfter, who to follow sits under that many commits, or under the last when there are fewer.
*
* @param list<Commit> $commits
* @return list<TimelineRow>
*/
public static function feed(array $commits, ?string $firstSeenId = null, ?CarbonInterface $now = null): array
public static function feed(array $commits, ?string $firstSeenId = null, ?CarbonInterface $now = null, ?int $whoToFollowAfter = null): array
{
$now ??= CarbonImmutable::now();
$rows = [];
Expand All @@ -59,7 +61,7 @@ public static function feed(array $commits, ?string $firstSeenId = null, ?Carbon
$previousDay = $day;
}

return self::joinRuns($rows);
return self::joinRuns(self::withWhoToFollow($rows, $whoToFollowAfter));
}

/**
Expand All @@ -73,7 +75,7 @@ public static function feed(array $commits, ?string $firstSeenId = null, ?Carbon
* @param list<Commit> $ranked
* @return list<TimelineRow>
*/
public static function ranked(array $latest, array $ranked): array
public static function ranked(array $latest, array $ranked, ?int $whoToFollowAfter = null): array
{
$rows = [];
$previous = null;
Expand All @@ -97,7 +99,36 @@ public static function ranked(array $latest, array $ranked): array
$previous = $commit;
}

return self::joinRuns($rows);
return self::joinRuns(self::withWhoToFollow($rows, $whoToFollowAfter));
}

/**
* Puts who to follow under the commit $after commits down, or under the
* last when there are fewer, as the web's log does on smaller screens. The
* rail stops above it, so the commit below starts a run of its own, as it
* does under a day marker.
*
* @param list<TimelineRow> $rows
* @return list<TimelineRow>
*/
private static function withWhoToFollow(array $rows, ?int $after): array
{
$commits = array_keys(array_filter($rows, fn (TimelineRow $row): bool => $row->isCommit()));

if ($after === null || $commits === []) {
return $rows;
}

$at = $commits[min(max(1, $after), count($commits)) - 1] + 1;
$below = $rows[$at] ?? null;

if ($below?->isCommit() === true) {
$rows[$at] = $below->startingRun();
}

array_splice($rows, $at, 0, [TimelineRow::whoToFollow()]);

return $rows;
}

/**
Expand Down
58 changes: 58 additions & 0 deletions app/Devnull/WhoToFollow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace App\Devnull;

use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\Cache;

/**
* Who to follow on this phone, per user: whether the Log shows it, which the
* web keeps in a cookie and Settings → Appearance switches, and which round
* of the day's picks it's on, which the web's refresh button moves through.
*/
final class WhoToFollow
{
public function shows(?string $userId): bool
{
return $userId !== null && Cache::get($this->key($userId, 'hidden')) !== true;
}

public function show(?string $userId, bool $shows): void
{
if ($userId === null) {
return;
}

$shows ? Cache::forget($this->key($userId, 'hidden')) : Cache::forever($this->key($userId, 'hidden'), true);
}

/**
* The round showing today. devnull picks afresh each day, so a new day starts at the first.
*/
public function round(?string $userId): int
{
$kept = $userId === null ? null : Cache::get($this->key($userId, 'round'));

return is_array($kept) && ($kept['day'] ?? null) === $this->today() && is_int($kept['round'] ?? null) ? $kept['round'] : 0;
}

/**
* The round showing now, once its people have landed.
*/
public function rememberRound(?string $userId, int $round): void
{
if ($userId !== null) {
Cache::forever($this->key($userId, 'round'), ['day' => $this->today(), 'round' => $round]);
}
}

private function today(): string
{
return CarbonImmutable::now('UTC')->toDateString();
}

private function key(string $userId, string $name): string
{
return "devnull.who-to-follow.{$userId}.{$name}";
}
}
2 changes: 2 additions & 0 deletions app/Icons/Lucide.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ enum Lucide
case Camera;
case ChartBar;
case Eye;
case EyeOff;
case Plus;
case Maximize2;

Expand Down Expand Up @@ -169,6 +170,7 @@ public function custom(bool $filled = false): ?string
self::Camera => 'camera',
self::ChartBar => 'chart-bar',
self::Eye => 'eye',
self::EyeOff => 'eye-off',
self::Plus => 'plus',
self::Maximize2 => 'maximize-2',
self::Terminal => 'terminal',
Expand Down
Loading
Loading