Skip to content

Commit

Permalink
laravel-chat: allow multiple chats
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasfroeller committed Jun 13, 2024
1 parent f1bb706 commit afc4f61
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 51 deletions.
230 changes: 186 additions & 44 deletions laravel-chat/app/Livewire/ChatClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

namespace App\Livewire;

# waiting for v3 release (Declaration of WebSocket\Client::setLogger(Psr\Log\LoggerInterface $logger): WebSocket\Client must be compatible with Psr\Log\LoggerAwareInterface::setLogger(Psr\Log\LoggerInterface $logger): void)
# waiting for v3 release (Declaration of WebSocket\Client::setLogger(Psr\Log\LoggerInterface $logger): WebSocket\Client must be compatible with Psr\Log\LoggerAwareInterface::setLogger(Psr\Log\LoggerInterface $logger): void)

// TODO: implement joining, only if user clicks chat in ui, to reduce server load and loading time

use Livewire\Component;
use Illuminate\Support\Facades\Http;
use WebSocket\Client as WebSocketClient;
use WebSocket\Middleware as WebSocketMiddleware;
use WebSocket\Connection as WebSocketConnection;
use WebSocket\Message\Message as WebSocketMessage;
use InvalidArgumentException;

class ChatDTO
{
Expand All @@ -22,75 +25,214 @@ class ChatDTO
public $created_at = '';
public $updated_at = '';
public $project_url = '';

public function __construct(array $data)
{
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
}

class ChatMessageDTO
{
public String $id = '';
public String $email = '';
public String $timestamp = '';
public String $text = '';

public function __construct(String $json)
{
$data = json_decode($json, true);

foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
}

class ChatConnectionsDTO
{
/**
* @var array<String, array {
* 'info' => `array {
* 'detail' => ChatDTO,
* 'messages' => ChatMessageDTO[]
* },
* 'websocket' => WebSocketClient,
* }>
*/
private array $chats;

public function setChats(array $chats, Bool $settingMessages = false, Bool $settingWebSocket = false): self
{
foreach ($chats as $chat) {
if (!isset($chat['info']['detail']) || !$chat['info']['detail'] instanceof ChatDTO) {
throw new InvalidArgumentException('Invalid chat detail');
}
if ($settingMessages) {
if (!isset($chat['info']['messages']) || !is_array($chat['info']['messages'])) {
throw new InvalidArgumentException('Invalid chat messages');
}

foreach ($chat['info']['messages'] as $message) {
if (!$message instanceof ChatMessageDTO) {
throw new InvalidArgumentException('Invalid chat message');
}
}
}
if ($settingWebSocket) {
if (!isset($chat['websocket']) || !$chat['websocket'] instanceof WebSocketClient) {
throw new InvalidArgumentException('Invalid websocket client');
}
}
}

$this->chats = $chats;
return $this;
}

public function getChats(): array
{
return $this->chats;
}
}

class ChatClient extends Component
{
public WebSocketMessage $message;
public String $monitorId = '$2y$12$W3pHWdAtePn1wjCm4.t4xO9lY9jOcu8/5SC0bDEsaAfSB8pKA5k.K';
public String $token;
/** @var ChatDTO[] */
public array $chats;
public String $output = '';
private WebSocketClient $websocket;
public String $email = '[email protected]';
public String $password = 'password';
public String | null $token = null;

public ?ChatConnectionsDTO $chats = null;

public function mount()
{
$this->chats = new ChatConnectionsDTO();

$this->login();
}

public static function getChatUrl($monitorHash, $auth)
{
return "ws://localhost:6969/chat/{$monitorHash}?auth={$auth}";
}

public static function getMonitorOfClient(WebSocketClient $client)
{
$url = $client->__toString();
$parts = parse_url($url);
$path = explode('/', $parts['path']);
$monitorHash = end($path);

return $monitorHash;
}

public static function parseChats($chats)
{
$chatMap = [];

foreach ($chats as $chat) {
$monitorHash = $chat['monitor_hash'];
$chatMap[$monitorHash] = [
'info' => [
'detail' => new ChatDTO($chat),
'messages' => [],
],
'websocket' => null,
];
}

return $chatMap;
}

public function login()
{
$response = Http::post('http://localhost:6969/login', [
'email' => '[email protected]',
'password' => 'password'
'email' => $this->email,
'password' => $this->password
]);

$json = $response->json();
$this->token = $json['token'];
$this->chats = $json['chats'];
if ($response && $response->ok()) {
$json = $response->json();
if (isset($json['token']) && isset($json['chats'])) {
$json = $response->json();
$this->token = $json['token'];
$chats = $json['chats'];

$chatMap = self::parseChats($chats);

$this->connectToWebSocket();
$this->chats->setChats($chatMap);
dump($this->chats);

$this->connectToWebSockets();
} else {
echo 'Error (invalid response body): ' . $response->body();
}
} else {
echo 'Error (response is not ok): ' . $response->body();
}
}

public function connectToWebSocket()
public function connectToWebSockets()
{
$monitorId = rawurlencode($this->monitorId);
$wsUri = "ws://localhost:6969/chat/{$monitorId}?auth={$this->token}";

$this->websocket = new WebSocketClient($wsUri);

$this->websocket // TODO: implement ping - ping every 2 or 3 seconds
->addMiddleware(new WebSocketMiddleware\CloseHandler())
->onText(function (WebSocketClient $client, WebSocketConnection $connection, WebSocketMessage $message) {
$this->output .= "<span class='text-right'>{$message->getContent()}</span>";
$this->emit('messageReceived', $message->getContent());
})
->onConnect(function (WebSocketClient $client, WebSocketConnection $connection) {
$this->output .= "<span class='sticky top-0 left-0 px-2 font-black bg-gray-300 text-lime-900'>CONNECTED</span>";
})
->onClose(function (WebSocketClient $client, WebSocketConnection $connection) {
$this->output .= "<span class='sticky top-0 left-0 px-2 font-black text-red-800 bg-gray-300'>CLOSED</span>";
})
->onError(function (WebSocketClient $client, WebSocketConnection $connection) {
$this->output .= "<span class='sticky top-0 left-0 px-2 font-black text-red-800 bg-gray-300'>ERROR</span>";
})
->start();
foreach ($this->chats as $chat) {
$monitorId = rawurlencode($chat['info']['detail']->monitor_hash);
$wsUri = self::getChatUrl($monitorId, $this->token);

$this->chats[$monitorId]['websocket'] = new WebSocketClient($wsUri);

$this->chats[$monitorId]['websocket'] // TODO: implement ping - ping every 2 or 3 seconds
->addMiddleware(new WebSocketMiddleware\CloseHandler())
->onText(function (WebSocketClient $client, WebSocketConnection $connection, WebSocketMessage $message) {
$monitorId = self::getMonitorOfClient($client);
$this->chats[$monitorId]['info']['messages']->array_push(new ChatMessageDTO($message->getContent()));

$messageAsHTML = "<span class='text-right'>{$message->getContent()}</span>";
$this->dispatch('messageReceived', [
'monitorId' => $monitorId,
'message' => $messageAsHTML
]);
})
->onConnect(function (WebSocketClient $client, WebSocketConnection $connection) {
$messageAsHTML = "<span class='sticky top-0 left-0 px-2 font-black bg-gray-300 text-lime-900'>CONNECTED</span>";
$this->dispatch('messageReceived', $messageAsHTML);
})
->onClose(function (WebSocketClient $client, WebSocketConnection $connection) {
$messageAsHTML = "<span class='sticky top-0 left-0 px-2 font-black text-red-800 bg-gray-300'>CLOSED</span>";
$this->dispatch('messageReceived', $messageAsHTML);
})
->onError(function (WebSocketClient $client, WebSocketConnection | null $connection) {
$messageAsHTML = "<span class='sticky top-0 left-0 px-2 font-black text-red-800 bg-gray-300'>ERROR</span>";
$this->dispatch('messageReceived', $messageAsHTML);
})
->start();
}
}

public function sendMessage($message)
public function sendMessage(String $monitorId, String $message)
{
if ($this->websocket && $message) {
$this->output .= "<span class='text-left'>{$message}</span>";
$this->websocket->text($message);
$this->message = '';
if (
!isset($this->chats[$monitorId]['websocket']) ||
!$this->chats[$monitorId]['websocket'] instanceof WebSocketClient ||
!$this->chats[$monitorId]['websocket']->isConnected()
) {
return;
}

$this->chats[$monitorId]['websocket']->text($message);

$messageAsHTML = "<span class='text-right'>{$message}</span>";
$this->dispatch('messageSent', [
'monitorId' => $monitorId,
'message' => $messageAsHTML
]);
}

public function closeConnection()
public function closeConnection(String $monitorId)
{
if ($this->websocket) {
$this->websocket->close();
if (isset($this->chats[$monitorId]['websocket']) && $this->chats[$monitorId]['websocket'] instanceof WebSocketClient) {
$this->chats[$monitorId]['websocket']->close();
}
}

Expand Down
8 changes: 4 additions & 4 deletions laravel-chat/public/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
define('LARAVEL_START', microtime(true));

// Determine if the application is in maintenance mode...
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
if (file_exists($maintenance = __DIR__ . '/../storage/framework/maintenance.php')) {
require $maintenance;
}

// Register the Composer autoloader...
require __DIR__.'/../vendor/autoload.php';
require __DIR__ . '/../vendor/autoload.php';

// Bootstrap Laravel and handle the request...
(require_once __DIR__.'/../bootstrap/app.php')
->handleRequest(Request::capture());
(require_once __DIR__ . '/../bootstrap/app.php')
->handleRequest(Request::capture()); // TODO: fix Property type not supported in Livewire for property: [{}]
5 changes: 2 additions & 3 deletions laravel-chat/resources/views/livewire/chat/client.blade.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<div class="p-8 prose">
<h1>Laravel Chat Client Demo</h1>

<div class="relative flex flex-col overflow-auto h-96" id="output">
{!! $output !!}
<div class="relative flex flex-col overflow-auto h-96" id="output"> <!-- TODO: listen to livewire event -->
</div>

<form wire:submit.prevent="sendMessage">
Expand All @@ -11,4 +10,4 @@
Send
</button>
</form>
</div>
</div>

0 comments on commit afc4f61

Please sign in to comment.