Link to viewer profile from actions list

This commit is contained in:
Alex Thomassen 2023-12-21 14:14:55 +00:00
parent 64fd2596e7
commit e45fb69b6a
Signed by: Alex
GPG Key ID: 10BD786B5F6FF5DE
7 changed files with 119 additions and 76 deletions

View File

@ -6,35 +6,50 @@
use App\Http\Requests\ViewerRequest;
use App\Models\Trace\Message;
use App\Models\Trace\ModeratorAction;
class ViewerController extends Controller
{
public function index(ViewerRequest $request, string $viewerId)
public function index(ViewerRequest $request, string $viewer)
{
// Check if viewerId is numeric
if (!is_numeric($viewerId)) {
$message = Message::orderBy('timestamp', 'desc')
->where('author_login', $viewerId)
->firstOrFail();
$viewerId = $message->author_id;
}
$viewer = strtolower($viewer);
$message = Message::orderBy('timestamp', 'desc')
->where('author_id', $viewer)
->orWhere('author_login', $viewer)
->firstOrFail();
// Check if viewerId exists
$viewerId = $message->author_id;
// Get the latest message from the viewer
$initialMessage = Message::orderBy('timestamp', 'desc')
->where('author_id', $viewerId)
->firstOrFail();
// Get distinct author_login based on author_id
$usernames = Message::where('author_id', $viewerId)
->distinct('author_login')
->get()
->orderBy('timestamp', 'desc')
->pluck('author_login')
->unique()
->toArray();
$messages = [];
foreach ($usernames as $username) {
$messages[$username] = Message::where('author_id', $viewerId)
->where('author_login', $username)
->orderBy('timestamp', 'desc')
->first();
}
// Sort $messages based on value's timestamp attribute
uasort($messages, function ($a, $b) {
return $b->timestamp <=> $a->timestamp;
});
return view('viewer.index', [
'id' => $viewerId,
'username' => $initialMessage->author_login,
'usernames' => $usernames,
'messages' => $messages,
]);
}
}

View File

@ -29,7 +29,7 @@ public function formatted($asHtml = false) : string
* Only bans, unbans will be implicitly handled by default format.
*/
if ($cmd === 'ban') {
return sprintf('/%s %s %s', $cmd, $this->targetName(), $this->reason ?? '');
return sprintf('/%s %s %s', $cmd, $this->targetName($asHtml), $this->reason ?? '');
}
if ($cmd === 'followers') {
@ -43,7 +43,7 @@ public function formatted($asHtml = false) : string
$message = htmlspecialchars($message);
}
return sprintf('/%s %s %s %s', $cmd, $this->targetName(), $message, $this->message_id);
return sprintf('/%s %s %s %s', $cmd, $this->targetName($asHtml), $message, $this->message_id);
}
/**
@ -56,7 +56,7 @@ public function formatted($asHtml = false) : string
$reason = htmlspecialchars($reason);
}
return sprintf('/%s %s %s %s', $cmd, $this->targetName(), $duration, $reason ?? '');
return sprintf('/%s %s %s %s', $cmd, $this->targetName($asHtml), $duration, $reason ?? '');
}
if ($discriminator === 'TermAction') {
@ -68,22 +68,39 @@ public function formatted($asHtml = false) : string
return sprintf('/%s %s %s', $cmd, $text, $this->term_id);
}
return trim(sprintf('/%s %s', $cmd, $this->targetName()));
return trim(sprintf('/%s %s', $cmd, $this->targetName($asHtml)));
}
/**
* Get the target of the action, if any
*
* @param bool $linkToViewerPage Returns an HTML link to the viewer page
* @return string
*/
public function targetName() : ?string
public function targetName($linkToViewerPage = false) : ?string
{
$format = '<a href="%s" class="text-amber-400" title="See viewer profile">%s</a>';
if (!empty($this->target_name)) {
return $this->target_name;
$name = $this->target_name;
$id = $this->target_id ?? $name;
if (!$linkToViewerPage) {
return $name;
}
return sprintf($format, route('viewer', ['viewer' => $id]), $name);
}
if (!empty($this->targeted_moderator_action_target_name)) {
return $this->targeted_moderator_action_target_name;
$name = $this->targeted_moderator_action_target_name;
if (!$linkToViewerPage) {
return $name;
}
$id = $this->target_id ?? $name;
return sprintf($format, route('viewer', ['viewer' => $id]), $name);
}
return '';

View File

@ -5,34 +5,42 @@
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-full mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 dark:text-gray-100">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ $channel->username() }}
</h2>
<div class="mx-auto sm:px-6 lg:px-8">
<div class="bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-100">
<h2 class="font-semibold text-xl text-gray-200 leading-tight">
{{ $channel->username() }}
</h2>
<table class="table-auto w-full">
<thead>
<table class="table-auto w-full">
<thead>
<tr>
<th class="px-4 py-2">Time &amp; date</th>
<th class="px-4 py-2">Command</th>
<th class="px-4 py-2">Moderator</th>
</tr>
</thead>
<tbody>
@foreach ($actions as $action)
<tr>
<th class="px-4 py-2">Time &amp; date</th>
<th class="px-4 py-2">Command</th>
<th class="px-4 py-2">Moderator</th>
</tr>
</thead>
<tbody>
@foreach ($actions as $action)
<tr>
<td class="border px-4 py-2" title="{{ $action->timestamp }}">{{ $action->timestamp }}</td>
<td class="border px-4 py-2" title="{{ $action->timestamp }}">{{ $action->timestamp }}
</td>
@if (empty($action->targetName()))
<td class="border px-4 py-2 break-all">
<code>{{ $action->formatted() }}</code>
</td>
<td class="border px-4 py-2">{{ $action->user->login }}</td>
</tr>
@endforeach
</tbody>
@if ($actions->hasPages())
@else
<td class="border px-4 py-2 break-all">
<code>
{!! $action->formatted(true) !!}
</code>
</td>
@endif
<td class="border px-4 py-2">{{ $action->user->login }}</td>
</tr>
@endforeach
</tbody>
@if ($actions->hasPages())
<tfoot>
<tr>
<td colspan="3" class="border px-4 py-2">
@ -40,9 +48,8 @@
</td>
</tr>
</tfoot>
@endif
</table>
</div>
@endif
</table>
</div>
</div>
</div>

View File

@ -5,16 +5,14 @@
</h2>
</x-slot>
<div class="max-w-full mx-auto sm:px-6 lg:px-8">
<div class="max-w-full mx-auto">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 dark:text-gray-100">
{{ __("You're logged in!") }}
</div>
<div class="mx-auto sm:px-6 lg:px-8">
<div class="bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-100">
{{ __("Hi :user!", ['user' => Auth::user()->display_name]) }}
</div>
</div>
<div class="p-6 text-gray-900 dark:text-gray-100">
<div class="bg-gray-800 p-6 -mt-6 text-gray-100">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight mb-2">
{{ __('Channels') }}
</h2>

View File

@ -13,6 +13,8 @@
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
@yield('head')
</head>
<body class="font-sans antialiased bg-gray-900">
<div class="flex flex-col h-screen justify-between">
@ -20,7 +22,7 @@
<!-- Page Heading -->
@if (isset($header))
<header class="bg-white dark:bg-gray-800 shadow">
<header class="bg-gray-800 shadow">
<div class="max-w-full mx-auto py-6 px-4 sm:px-6 lg:px-8">
{{ $header }}
</div>
@ -29,37 +31,41 @@
<div class="mt-4">
<div class="max-w-full mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 dark:text-gray-100">
<div class="bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-100">
<p>This is the initial implementation of this project. A few different things that are planned:</p>
<ul class="list-disc list-inside mt-2">
<li>Channels that are tracked where you have moderator access will be <a href="https://twitch.uservoice.com/forums/310213-developers/suggestions/38203849-add-endpoint-to-return-channels-where-the-user-is" class="underline text-amber-400">automatically retrieved from Twitch's API upon login</a>. At the moment I have to manually give permissions via the database.</li>
<li>Channels that are tracked where you have moderator access will be <a href="https://twitch.uservoice.com/forums/310213-developers/suggestions/38203849-add-endpoint-to-return-channels-where-the-user-is" class="underline text-amber-400">automatically retrieved from Twitch's API upon login</a>. At the moment I have to manually give permissions via the database, since this API is currently only "planned" by Twitch.</li>
<li>Different filtering options: Type of action (timeout, raid etc.), moderator (whodunit), user/viewer (next point).</li>
<li>See related moderation actions, for example if a specific viewer has been timed out or banned multiple times.</li>
<li>Searching by text.</li>
<li>Linking any affected viewer's to their Twitch viewer card.</li>
<li>Automatic conversion to local time. Currently it's displayed in UTC.</li>
<li>Automatic conversion to local time. Currently time &amp; date is displayed in UTC.</li>
<li>Ability to see a viewer's chat messages at the time of action, alongside messages before/after to see context.</li>
<li>Username history of a user</li>
<li>... and probably more I'll figure out as I go along.</li>
</ul>
<p class="mt-6">List of things that were planned and has been implemented in some form:</p>
<ul class="list-disc list-inside mt-2">
<li>Username history of a user - For any user that has been timed out, raided, etc. you can now click on their name (highlighted in orange/amber) and access their basic "viewer profile".</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Page Content -->
<main class="mb-auto bg-white dark:bg-gray-800 mt-4">
<main class="mb-auto mt-4">
{{ $slot }}
</main>
<!-- Footer -->
<footer class="bg-white dark:bg-gray-800 shadow bottom-0 mt-4">
<footer class="bg-gray-800 shadow bottom-0 mt-4">
<div class="max-w-full mx-auto py-6 px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center">
<div class="text-sm text-gray-500 dark:text-gray-400">
<p>TraceDash &mdash; v0.0.2 [Pre-Alpha Early Access™]</p>
<p>TraceDash &mdash; v0.0.3 [Pre-Alpha Early Access™]</p>
<p>Powered by an unhealthy amount of caffeine</p>
</div>

View File

@ -5,23 +5,23 @@
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-full mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 dark:text-gray-100">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
Viewer details for: {{ $username }} [{{ $id }}]
</h2>
<div class="max-w-full mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 dark:text-gray-100">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
Viewer details for: {{ $username }} [{{ $id }}]
</h2>
<p class="mt-4">Seen under the following usernames:</p>
<p class="mt-4">Seen under the following usernames:</p>
<!-- List style dots -->
<ul class="mt-2 list-disc list-inside">
@foreach ($usernames as $username)
<li>{{ $username }}</li>
@endforeach
</ul>
</div>
<!-- List style dots -->
<ul class="mt-2 list-disc list-inside">
@foreach ($messages as $username => $message)
<li class="mt-1">
{{ $username }} <span class="text-gray-400">&mdash; Last seen with this username: {{ date('Y-m-d', strtotime($message->timestamp)) }}</span>
</li>
@endforeach
</ul>
</div>
</div>
</div>

View File

@ -17,7 +17,7 @@
Route::get('/channel/{channel}', [DashboardController::class, 'channel'])
->name('dashboard.channel');
Route::get('/viewer/{viewerId}', [ViewerController::class, 'index'])
Route::get('/viewer/{viewer}', [ViewerController::class, 'index'])
->name('viewer');
Route::post('logout', [TwitchController::class, 'logout'])