f81237b23c
- Header: búsqueda móvil con panel deslizable (fixed bajo la topbar), comparte la lógica AJAX con desktop via initSearchWidget() - ChatController: index muestra solo amigos; show pasa isFriend y friendship a la vista; send valida amistad con abort_unless - chat/index: muestra amigos con @forelse y mensaje vacío si no tiene - chat/show: si no son amigos muestra banner de bloqueo con botón contextual (enviar solicitud / cancelar / aceptar según estado) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
211 lines
9.0 KiB
PHP
211 lines
9.0 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('content')
|
|
|
|
<div class="container-fluid">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header bg-primary text-white d-flex align-items-center justify-content-between">
|
|
<div class="d-flex align-items-center">
|
|
<img src="{{ $user->profile_photo_url }}"
|
|
class="rounded-circle mr-2"
|
|
width="38" height="38"
|
|
style="object-fit:cover">
|
|
<strong>{{ $user->name }}</strong>
|
|
</div>
|
|
<a href="{{ route('chat.index') }}" class="btn btn-sm btn-light shadow-sm">
|
|
<i class="fas fa-arrow-left fa-sm"></i> Regresar
|
|
</a>
|
|
</div>
|
|
|
|
<div class="card-body overflow-auto flex-grow-1 p-3" id="chat-messages">
|
|
@foreach($messages as $msg)
|
|
<div class="d-flex mb-2 {{ $msg->user_id === auth()->id() ? 'justify-content-end' : 'justify-content-start' }}">
|
|
<div class="px-3 py-2 rounded {{ $msg->user_id === auth()->id() ? 'bg-primary text-white' : 'bg-light' }}"
|
|
style="max-width:65%">
|
|
@if($msg->body)
|
|
<div>{{ $msg->body }}</div>
|
|
@endif
|
|
@if($msg->attachment)
|
|
@if($msg->attachment_type === 'image')
|
|
<img src="{{ route('files.serve', ['path' => $msg->attachment]) }}" class="img-fluid rounded mt-1" style="max-width:240px; cursor:pointer" onclick="window.open(this.src,'_blank')">
|
|
@elseif($msg->attachment_type === 'audio')
|
|
<audio controls class="mt-1" style="max-width:250px">
|
|
<source src="{{ route('files.serve', ['path' => $msg->attachment]) }}">
|
|
</audio>
|
|
@elseif($msg->attachment_type === 'video')
|
|
<video controls class="mt-1 rounded" style="max-width:250px">
|
|
<source src="{{ route('files.serve', ['path' => $msg->attachment]) }}">
|
|
</video>
|
|
@else
|
|
<a href="{{ route('files.serve', ['path' => $msg->attachment]) }}" target="_blank" class="btn btn-sm btn-light mt-1">
|
|
<i class="fas fa-file"></i> Ver archivo
|
|
</a>
|
|
@endif
|
|
@endif
|
|
<div class="text-right mt-1" style="font-size:0.7rem; opacity:0.7">
|
|
{{ $msg->created_at->format('H:i') }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
|
|
<div class="card-footer p-2">
|
|
@if($isFriend)
|
|
<form id="chat-form" enctype="multipart/form-data">
|
|
@csrf
|
|
<input type="hidden" name="receiver_id" value="{{ $user->id }}">
|
|
<div class="input-group">
|
|
<input type="text" name="body" id="chat-input"
|
|
class="form-control rounded-pill mr-2"
|
|
placeholder="Escribe un mensaje..." autocomplete="off">
|
|
<label class="btn btn-light mr-1 mb-0" title="Adjuntar archivo">
|
|
<i class="fas fa-paperclip"></i>
|
|
<input type="file" name="attachment" id="attachment" hidden>
|
|
</label>
|
|
<button type="submit" class="btn btn-primary rounded-pill px-4">
|
|
<i class="fas fa-paper-plane"></i>
|
|
</button>
|
|
</div>
|
|
<div id="attachment-preview" class="mt-1 text-muted small"></div>
|
|
</form>
|
|
@else
|
|
<div class="text-center py-3">
|
|
<p class="text-muted small mb-2">
|
|
<i class="fas fa-lock mr-1"></i>
|
|
Ya no son amigos. No puedes enviar mensajes.
|
|
</p>
|
|
@if($friendship && $friendship->status === 'pending' && $friendship->sender_id === auth()->id())
|
|
<span class="btn btn-sm btn-outline-secondary disabled">
|
|
<i class="fas fa-user-clock mr-1"></i> Solicitud enviada
|
|
</span>
|
|
@elseif($friendship && $friendship->status === 'pending' && $friendship->receiver_id === auth()->id())
|
|
<form action="{{ route('friendships.accept', $friendship) }}" method="POST" class="d-inline">
|
|
@csrf
|
|
<button class="btn btn-sm btn-success">
|
|
<i class="fas fa-check mr-1"></i> Aceptar solicitud
|
|
</button>
|
|
</form>
|
|
@else
|
|
<form action="{{ route('friendships.send', $user) }}" method="POST" class="d-inline">
|
|
@csrf
|
|
<button class="btn btn-sm btn-primary">
|
|
<i class="fas fa-user-plus mr-1"></i> Enviar solicitud de amistad
|
|
</button>
|
|
</form>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
@endsection
|
|
|
|
@section('scripts')
|
|
|
|
<style>
|
|
.sticky-footer { display: none !important; }
|
|
.card { height: calc(100vh - 160px); display: flex; flex-direction: column; }
|
|
#chat-messages { flex: 1; overflow-y: auto; }
|
|
</style>
|
|
|
|
<script>
|
|
const receiverId = {{ $user->id }};
|
|
const authId = {{ auth()->id() }};
|
|
const storageBase = '{{ url('/files') }}';
|
|
const chatMessages = document.getElementById('chat-messages');
|
|
|
|
// scroll al fondo
|
|
function scrollBottom() {
|
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
}
|
|
scrollBottom();
|
|
|
|
// preview archivo
|
|
document.getElementById('attachment').addEventListener('change', function() {
|
|
const name = this.files[0]?.name ?? '';
|
|
document.getElementById('attachment-preview').textContent = name ? '📎 ' + name : '';
|
|
});
|
|
|
|
// enviar mensaje
|
|
document.getElementById('chat-form').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const form = this;
|
|
const data = new FormData(form);
|
|
|
|
axios.post('{{ route("chat.send") }}', data, {
|
|
headers: { 'X-Socket-ID': window.Echo.socketId() }
|
|
}).then(response => {
|
|
appendMessage(response.data, true);
|
|
scrollBottom();
|
|
form.reset();
|
|
document.getElementById('attachment-preview').textContent = '';
|
|
// Notificar al header que esta conversación ya fue respondida
|
|
window.dispatchEvent(new CustomEvent('chat:replied', { detail: { receiverId } }));
|
|
});
|
|
});
|
|
|
|
function buildAttachmentHtml(msg, isMine) {
|
|
if (!msg.attachment) return '';
|
|
const url = storageBase + '/' + msg.attachment;
|
|
const linkClass = isMine ? 'text-white' : 'text-dark';
|
|
switch (msg.attachment_type) {
|
|
case 'image':
|
|
return `<img src="${url}" class="img-fluid rounded mt-1" style="max-width:240px; cursor:pointer"
|
|
onclick="window.open('${url}','_blank')">`;
|
|
case 'video':
|
|
return `<video controls class="mt-1 rounded" style="max-width:240px">
|
|
<source src="${url}">Tu navegador no soporta video.
|
|
</video>`;
|
|
case 'audio':
|
|
return `<audio controls class="mt-1" style="max-width:240px">
|
|
<source src="${url}">Tu navegador no soporta audio.
|
|
</audio>`;
|
|
default:
|
|
return `<a href="${url}" target="_blank" class="btn btn-sm ${isMine ? 'btn-light' : 'btn-secondary'} mt-1">
|
|
<i class="fas fa-file mr-1"></i>Ver archivo
|
|
</a>`;
|
|
}
|
|
}
|
|
|
|
function appendMessage(msg, isMine) {
|
|
const div = document.createElement('div');
|
|
div.className = `d-flex mb-2 ${isMine ? 'justify-content-end' : 'justify-content-start'}`;
|
|
|
|
const time = typeof msg.created_at === 'string' && msg.created_at.length > 5
|
|
? new Date(msg.created_at).toLocaleTimeString('es-MX', {hour:'2-digit', minute:'2-digit'})
|
|
: msg.created_at;
|
|
|
|
div.innerHTML = `
|
|
<div class="px-3 py-2 rounded ${isMine ? 'bg-primary text-white' : 'bg-light'}" style="max-width:65%">
|
|
${msg.body ? `<div>${msg.body}</div>` : ''}
|
|
${buildAttachmentHtml(msg, isMine)}
|
|
<div class="text-right mt-1" style="font-size:0.7rem; opacity:0.7">${time}</div>
|
|
</div>`;
|
|
chatMessages.appendChild(div);
|
|
}
|
|
|
|
// esperar a que Echo esté disponible
|
|
function waitForEcho(callback) {
|
|
if (window.Echo) {
|
|
callback();
|
|
} else {
|
|
setTimeout(() => waitForEcho(callback), 100);
|
|
}
|
|
}
|
|
|
|
waitForEcho(() => {
|
|
window.Echo.channel(`chat.${authId}`)
|
|
.listen('MessageSent', (e) => {
|
|
if (e.sender.id !== receiverId) return;
|
|
appendMessage(e, false);
|
|
scrollBottom();
|
|
});
|
|
});
|
|
</script>
|
|
@endsection
|
|
|
|
|