findConversationOrFail($conversationId); $ttl = max(5, (int) config('messaging.typing.ttl_seconds', 8)); $this->store()->put($this->key($conversationId, (int) $request->user()->id), 1, now()->addSeconds($ttl)); if ((bool) config('messaging.realtime', false)) { event(new TypingStarted($conversationId, $request->user())); } return response()->json(['ok' => true]); } public function stop(Request $request, int $conversationId): JsonResponse { $this->findConversationOrFail($conversationId); $this->store()->forget($this->key($conversationId, (int) $request->user()->id)); if ((bool) config('messaging.realtime', false)) { event(new TypingStopped($conversationId, $request->user())); } return response()->json(['ok' => true]); } public function index(Request $request, int $conversationId): JsonResponse { $this->findConversationOrFail($conversationId); $userId = (int) $request->user()->id; $participants = ConversationParticipant::query() ->where('conversation_id', $conversationId) ->whereNull('left_at') ->where('user_id', '!=', $userId) ->with('user:id,username') ->get(); $typing = $participants ->filter(fn ($p) => $this->store()->has($this->key($conversationId, (int) $p->user_id))) ->map(fn ($p) => [ 'user_id' => (int) $p->user_id, 'username' => (string) ($p->user->username ?? ''), ]) ->values(); return response()->json(['typing' => $typing]); } private function assertParticipant(Request $request, int $conversationId): void { abort_unless( ConversationParticipant::query() ->where('conversation_id', $conversationId) ->where('user_id', $request->user()->id) ->whereNull('left_at') ->exists(), 403, 'You are not a participant of this conversation.' ); } private function key(int $conversationId, int $userId): string { return "typing:{$conversationId}:{$userId}"; } private function store(): Repository { $store = (string) config('messaging.typing.cache_store', 'redis'); if ($store === 'redis' && ! class_exists('Redis')) { return Cache::store(); } try { return Cache::store($store); } catch (\Throwable) { return Cache::store(); } } private function findConversationOrFail(int $conversationId): Conversation { $conversation = Conversation::query()->findOrFail($conversationId); $this->authorize('view', $conversation); return $conversation; } }