From e71802ae01d340890f8f305e9769c691e3641160 Mon Sep 17 00:00:00 2001 From: Daniel Wilkowski Date: Wed, 2 Oct 2024 19:30:16 +0200 Subject: [PATCH] Update online viewers --- app/Domain/Online/FakeSessionRepository.php | 33 +++++ app/Domain/Online/Viewers.php | 26 ++++ app/Domain/Spacer.php | 38 ++++++ app/Http/Controllers/Forum/BaseController.php | 4 +- app/Http/Controllers/HomeController.php | 2 +- app/Services/Session/Renderer.php | 115 ++++++------------ .../feature/viewersOnline/viewers-online.scss | 83 +++++++++++++ resources/sass/core.scss | 1 + resources/views/components/viewers.twig | 52 +++++--- tests/Unit/OnlineUsers/SpacerTest.php | 67 ++++++++++ tests/Unit/OnlineUsers/ViewersTest.php | 51 ++++++++ 11 files changed, 371 insertions(+), 101 deletions(-) create mode 100644 app/Domain/Online/FakeSessionRepository.php create mode 100644 app/Domain/Spacer.php create mode 100644 resources/feature/viewersOnline/viewers-online.scss create mode 100644 tests/Unit/OnlineUsers/SpacerTest.php create mode 100644 tests/Unit/OnlineUsers/ViewersTest.php diff --git a/app/Domain/Online/FakeSessionRepository.php b/app/Domain/Online/FakeSessionRepository.php new file mode 100644 index 000000000..d94b4feeb --- /dev/null +++ b/app/Domain/Online/FakeSessionRepository.php @@ -0,0 +1,33 @@ +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(); + } +} diff --git a/app/Domain/Online/Viewers.php b/app/Domain/Online/Viewers.php index d52658f1d..117f9d562 100644 --- a/app/Domain/Online/Viewers.php +++ b/app/Domain/Online/Viewers.php @@ -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)); + } } diff --git a/app/Domain/Spacer.php b/app/Domain/Spacer.php new file mode 100644 index 000000000..9977e3798 --- /dev/null +++ b/app/Domain/Spacer.php @@ -0,0 +1,38 @@ +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; + } +} diff --git a/app/Http/Controllers/Forum/BaseController.php b/app/Http/Controllers/Forum/BaseController.php index 5fc413e67..312796479 100644 --- a/app/Http/Controllers/Forum/BaseController.php +++ b/app/Http/Controllers/Forum/BaseController.php @@ -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); } /** diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 61832f87c..ad7c358b6 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -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 diff --git a/app/Services/Session/Renderer.php b/app/Services/Session/Renderer.php index 60410cacc..5cd5a63a4 100644 --- a/app/Services/Session/Renderer.php +++ b/app/Services/Session/Renderer.php @@ -1,110 +1,63 @@ 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; } } diff --git a/resources/feature/viewersOnline/viewers-online.scss b/resources/feature/viewersOnline/viewers-online.scss new file mode 100644 index 000000000..075a21acb --- /dev/null +++ b/resources/feature/viewersOnline/viewers-online.scss @@ -0,0 +1,83 @@ +@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-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; + } + } + } + } +} diff --git a/resources/sass/core.scss b/resources/sass/core.scss index 8b5445d6c..1ea46079a 100644 --- a/resources/sass/core.scss +++ b/resources/sass/core.scss @@ -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"; diff --git a/resources/views/components/viewers.twig b/resources/views/components/viewers.twig index cb25b845d..a7d7c4f7d 100644 --- a/resources/views/components/viewers.twig +++ b/resources/views/components/viewers.twig @@ -1,6 +1,6 @@

- {% if isLocalViewers %} + {% if local %} {% else %} @@ -9,20 +9,38 @@

- + {% if guestsCount > 0 %} +

+ + +{{ guestsCount }} niezalogowanych online + +

+ {% endif %} + + diff --git a/tests/Unit/OnlineUsers/SpacerTest.php b/tests/Unit/OnlineUsers/SpacerTest.php new file mode 100644 index 000000000..8cb8c6539 --- /dev/null +++ b/tests/Unit/OnlineUsers/SpacerTest.php @@ -0,0 +1,67 @@ +fitInSpace([]); + $this->assertSame([], $items); + $this->assertSame(0, $remaining); + } + + #[Test] + public function spacesZero_isInvalid(): void + { + $this->expectException(\InvalidArgumentException::class); + new Spacer(0); + } + + #[Test] + public function spacesNegative_isInvalid(): void + { + $this->expectException(\InvalidArgumentException::class); + new Spacer(-1); + } + + #[Test] + public function spaceOne(): void + { + $spacer = new Spacer(1); + $this->assertSame([['Foo'], 0], $spacer->fitInSpace(['Foo'])); + } + + #[Test] + public function spaceMany(): void + { + $spacer = new Spacer(2); + $this->assertSame([['Foo', 'Bar'], 0], $spacer->fitInSpace(['Foo', 'Bar'])); + } + + #[Test] + public function overflowTwo(): void + { + $spacer = new Spacer(1); + $this->assertSame([[], 2], $spacer->fitInSpace(['Foo', 'Bar'])); + } + + #[Test] + public function overflowThree(): void + { + $spacer = new Spacer(2); + $this->assertSame([['Foo'], 2], $spacer->fitInSpace(['Foo', 'Bar', 'Cat'])); + } + + #[Test] + public function overflowFour(): void + { + $spacer = new Spacer(3); + $this->assertSame([['Foo', 'Bar'], 2], $spacer->fitInSpace(['Foo', 'Bar', 'Cat', 'Door'])); + } +} diff --git a/tests/Unit/OnlineUsers/ViewersTest.php b/tests/Unit/OnlineUsers/ViewersTest.php new file mode 100644 index 000000000..53033f573 --- /dev/null +++ b/tests/Unit/OnlineUsers/ViewersTest.php @@ -0,0 +1,51 @@ +usersOfSize(2), 3); + $this->assertSame(5, $viewers->totalCount()); + } + + #[Test] + public function usersWithGroup(): void + { + $viewers = new Viewers([ + $this->user(), + $this->user(name:'Mark', group:'Blue'), + ], 0); + $users = $viewers->usersWithGroup(); + $this->assertCount(1, $users); + $this->assertSame('Mark', $users[0]->name); + } + + #[Test] + public function usersWithoutGroup(): void + { + $viewers = new Viewers([ + $this->user(name:'Tom'), + $this->user(group:'Blue'), + ], 0); + $users = $viewers->usersWithoutGroup(); + $this->assertCount(1, $users); + $this->assertSame('Tom', $users[0]->name); + } + + private function usersOfSize(int $size): array + { + return \array_fill(0, $size, $this->user()); + } + + private function user(string $name = null, string $group = null): ViewerUser + { + return new ViewerUser($name ?? '', $group, null); + } +}