Skip to content

Commit

Permalink
Update online viewers
Browse files Browse the repository at this point in the history
  • Loading branch information
danon committed Oct 8, 2024
1 parent 2cda098 commit 66c921a
Show file tree
Hide file tree
Showing 11 changed files with 378 additions and 101 deletions.
33 changes: 33 additions & 0 deletions app/Domain/Online/FakeSessionRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
namespace Coyote\Domain\Online;

use Coyote\User;
use Illuminate\Database\Eloquent;

/**
* For development purposes only.
*/
readonly class FakeSessionRepository extends SessionRepository
{
public function sessionsIn(string $prefixPath): SessionsSnapshot
{
if ($prefixPath === '/') {
return new SessionsSnapshot(
users:$this->id(User::query()->inRandomOrder(rand(1, 100))),
guestsCount:\rand(450, 900),
);
}
return new SessionsSnapshot(
users:[
...$this->id(User::query()->whereNotNull('group_name')->limit(1)),
...$this->id(User::query()->inRandomOrder()->limit(\rand(5, 7))),
],
guestsCount:\rand(5, 90),
);
}

private function id(Eloquent\Builder $builder): array
{
return $builder->pluck('id')->toArray();
}
}
26 changes: 26 additions & 0 deletions app/Domain/Online/Viewers.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,30 @@ public function __construct(
)
{
}

public function totalCount(): int
{
return \count($this->users) + $this->guestsCount;
}

/**
* @return ViewerUser[]
*/
public function usersWithGroup(): array
{
return $this->usersFiltered(fn(ViewerUser $user) => $user->groupName);
}

/**
* @return ViewerUser[]
*/
public function usersWithoutGroup(): array
{
return $this->usersFiltered(fn(ViewerUser $user) => !$user->groupName);
}

private function usersFiltered(callable $predicate): array
{
return \array_values(\array_filter($this->users, $predicate));
}
}
38 changes: 38 additions & 0 deletions app/Domain/Spacer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
namespace Coyote\Domain;

readonly class Spacer
{
public function __construct(private int $spaces)
{
if ($spaces < 1) {
throw new \InvalidArgumentException();
}
}

public function fitInSpace(array $items): array
{
return $this->partitionArrayAt($items, $this->outputLength($items));
}

private function partitionArrayAt(array $items, int $length): array
{
return [
\array_slice($items, 0, $length),
\count($items) - $length,
];
}

private function outputLength(array $items): int
{
return $this->spaces - $this->padding($items);
}

private function padding(array $items): int
{
if (\count($items) === $this->spaces) {
return 0;
}
return 1;
}
}
4 changes: 2 additions & 2 deletions app/Http/Controllers/Forum/BaseController.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,14 @@ private function globalViewers(): View
{
/** @var Renderer $renderer */
$renderer = app(Renderer::class);
return $renderer->render('Online w serwisie', requestUri:null);
return $renderer->render('/', local:false);
}

private function localViewers(): View
{
/** @var Renderer $renderer */
$renderer = app(Renderer::class);
return $renderer->render('Aktualnie na tej stronie', requestUri:$this->request->getRequestUri());
return $renderer->render($this->request->getRequestUri(), local:true);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Controllers/HomeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ private function globalViewers(): View
{
/** @var Renderer $renderer */
$renderer = app(Renderer::class);
return $renderer->render('Użytkownicy online', requestUri:null);
return $renderer->render('/', local:false);
}

private function flags(): array
Expand Down
115 changes: 34 additions & 81 deletions app/Services/Session/Renderer.php
Original file line number Diff line number Diff line change
@@ -1,110 +1,63 @@
<?php
namespace Coyote\Services\Session;

use Coyote\Session;
use Illuminate\Database;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\Expression;
use Coyote\Domain\Online\SessionRepository;
use Coyote\Domain\Online\Viewers;
use Coyote\Domain\Online\ViewersStore;
use Coyote\Domain\Spacer;
use Illuminate\Http\Request;
use Illuminate\Support;
use Illuminate\View\View;

class Renderer
{
const USER = 'Użytkownik';
private Spacer $spacer;

public function __construct(
private Database\Connection $db,
private Registered $registered,
private Request $request)
private SessionRepository $session,
private ViewersStore $store,
private Request $request,
)
{
$this->spacer = new Spacer(8);
}

public function render(string $title, ?string $requestUri): View
public function render(string $requestUri, bool $local): View
{
$collection = $this->data($requestUri);
$viewers = $this->sessionViewers($requestUri);

$total = $collection->sum('count');
$guests = $collection->where('user_id', null)->sum('count');
$registered = $total - $guests;
$robots = $collection->filter(fn($item) => $item->robot)->sum('count');

$guests -= $robots;
$total -= $robots;

$collection = $this->map($collection);

if ($this->request->user()) {
if (!$collection->contains('user_id', $this->request->user()->id)) {
$collection->push(new Session(['user_id' => $this->request->user()->id, 'path' => $requestUri]));
$total++;
$registered++;
}
} else if ($collection->count() === 0) {
// we keep session in redis but also - list of online users - in postgres.
// we refresh table every 1 minute, so info about user's current page might be sometimes outdated.
$total++;
$guests++;
}

$collection = $this->unique($collection);
$collection = $this->registered->setup($collection);

$groups = [self::USER => []];
foreach ($collection->groupBy('group') as $name => $users) {
if ($name === '') {
$name = self::USER;
} else if (!isset($groups[$name])) {
$groups[$name] = [];
}
foreach ($users as $user) {
if ($user['user_id'] !== null) {
$groups[$name][] = $this->makeProfileLink($user['user_id'], $user['name']);
}
}
}

unset($groups[self::USER]);
ksort($groups);
[$users, $superfluous] = $this->spacer->fitInSpace($viewers->usersWithoutGroup());

return view('components.viewers', [
'isLocalViewers' => $requestUri !== null,
'title' => $title,
'groups' => $requestUri === null ? $groups : [],
'total' => $total,
'guests' => $guests,
'registered' => $registered,
'local' => $local,
'guestsCount' => $viewers->guestsCount,
'usersCount' => \count($viewers->users),
'title' => $local
? 'Aktualnie na tej stronie'
: "{$viewers->totalCount()} użytkowników online",
'usersWithGroup' => $viewers->usersWithGroup(),
'usersWithoutGroup' => $users,
'superfluousCount' => $superfluous,
]);
}

private function data(?string $requestUri): Support\Collection
{
return $this
->db
->table('sessions')
->when($requestUri !== null, fn(Builder $builder) => $builder
->where('path', 'LIKE', \mb_strToLower(\strTok($requestUri, '?')) . '%'))
->groupBy(['user_id', 'robot'])
->get(['user_id', 'robot', new Expression('COUNT(*)')]);
}

private function map(Support\Collection $collection): Support\Collection
private function sessionViewers(string $requestUri): Viewers
{
return $collection->map(fn($item) => new Session((array)$item));
$sessions = $this->session->sessionsIn($requestUri);
if ($this->isUserLogged()) {
$sessions = $sessions->coalesceUser($this->loggedUserId());
} else {
$sessions = $sessions->coalesceGuest();
}
return $this->store->viewers($sessions);
}

private function makeProfileLink(int $userId, string $userName): string
private function isUserLogged(): bool
{
return link_to_route('profile', $userName, [$userId], ['data-user-id' => $userId]);
return !!$this->request->user();
}

private function unique(Support\Collection $sessions): Support\Collection
private function loggedUserId()
{
$guests = $sessions->filter(fn(Session $item) => $item->userId === null);
$sessions
->filter(fn(Session $item) => $item->userId !== null)
->unique('user_id')
->each(fn(Session $item) => $guests->push($item));
return $guests;
return $this->request->user()->id;
}
}
90 changes: 90 additions & 0 deletions resources/feature/viewersOnline/viewers-online.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
@mixin dark {
body.theme-dark & {
@content;
}
}

@mixin light {
body.theme-light & {
@content;
}
}

.viewers-online {
.viewer-pill {
padding: 4px;
overflow: hidden;
border-radius: 21px;
align-items: center;

@include dark {
background: #2e2e2e;
border: 1px solid #383838;
}

@include light {
background: white;
border: 1px solid #dedede;
}

.viewer-pill-avatar {
width: 32px;
height: 32px;
border-radius: 16px;
overflow: hidden;
}

.viewer-pill-title {
padding-left: 6px;
padding-right: 14px;
font-size: 0.8em;
line-height: 1.3em;
}
}

.viewers-users-group {
// More horizontal space for pills,
// it's acceptable for them to overflow a little.
margin-left: -6px;
margin-right: -12px;
}

.viewers-users {
max-width: 90%;

.circle {
border-radius: 32px;
overflow: hidden;
margin-right: -12px;
width: 32px;
height: 32px;

&.circle-number {
text-align: center;
line-height: 32px;
font-size: 0.8em;
}

@include light {
border: 1px solid #dedede;
background: white;
}
@include dark {
border: 1px solid #444;
background: #2e2e2e;
}

&.circle-number-muted {
@include light {
color: #888;
background: #f5f5f5;
}
@include dark {
color: #777;
background: #252525;;
border: 1px solid #3f3f3f;
}
}
}
}
}
1 change: 1 addition & 0 deletions resources/sass/core.scss
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
@import "pages/wiki";

@import "../feature/stickyAside/sticky-aside";
@import "../feature/viewersOnline/viewers-online";
@import "../js/components/defaultAvatar";

@import "../../node_modules/@riddled/4play/src/style";
Expand Down
Loading

0 comments on commit 66c921a

Please sign in to comment.