Initial commit: Animexe Laravel platform

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 00:01:48 +03:00
co-authored by Claude Opus 4.8
commit a63515cfc6
366 changed files with 74773 additions and 0 deletions
@@ -0,0 +1,103 @@
@extends('frontend.layouts.app')
@section('title', 'Mesajlar — Animexe')
@push('styles')
<style>
.msg-wrap{max-width:680px;margin:0 auto;padding:calc(var(--nav-h) + 28px) 16px 64px}
.msg-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}
.msg-header h1{font-size:1.3rem;font-weight:900;color:#fff}
.msg-list{display:flex;flex-direction:column;gap:4px}
.msg-row{
display:flex;align-items:center;gap:14px;
padding:14px 16px;border-radius:14px;
background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06);
text-decoration:none;transition:all .15s;cursor:pointer;
}
.msg-row:hover{background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.12)}
.msg-row.unread{background:rgba(0,245,255,.04);border-color:rgba(0,245,255,.12)}
.msg-ava{
width:46px;height:46px;border-radius:50%;flex-shrink:0;overflow:hidden;
background:linear-gradient(135deg,var(--accent2),var(--accent));
display:flex;align-items:center;justify-content:center;
font-size:1.1rem;font-weight:800;color:#fff;
}
.msg-ava img{width:100%;height:100%;object-fit:cover}
.msg-info{flex:1;min-width:0}
.msg-name{font-size:.9rem;font-weight:700;color:#fff;margin-bottom:3px;display:flex;align-items:center;gap:8px}
.msg-preview{font-size:.8rem;color:rgba(255,255,255,.4);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.msg-row.unread .msg-preview{color:rgba(255,255,255,.7)}
.msg-unread-dot{width:8px;height:8px;border-radius:50%;background:var(--accent2);flex-shrink:0}
.msg-time{font-size:.7rem;color:rgba(255,255,255,.25);flex-shrink:0;align-self:flex-start;margin-top:4px}
.msg-empty{text-align:center;padding:64px 20px;color:rgba(255,255,255,.2)}
.msg-empty i{font-size:2.8rem;display:block;margin-bottom:14px;opacity:.2}
.msg-empty p{font-size:.9rem}
</style>
@endpush
@section('content')
<div class="msg-wrap">
<div class="msg-header">
<h1><i class="bi bi-chat-dots" style="color:var(--accent2)"></i> Mesajlar</h1>
</div>
@if($conversations->isEmpty())
<div class="msg-empty">
<i class="bi bi-chat-square-dots"></i>
<p>Henüz mesajın yok.<br>Bir kullanıcının profiline git ve mesaj gönder.</p>
</div>
@else
<div class="msg-list">
@foreach($conversations as $conv)
@php
$other = $conv['other'];
$last = $conv['last_message'];
$unread= $conv['unread'];
@endphp
<a href="{{ route('messages.show', $conv['id']) }}" class="msg-row {{ $unread > 0 ? 'unread' : '' }}">
<div class="msg-ava">
@if($other?->avatar)
<img src="{{ \App\Support\MediaUrl::fromStoragePath($other->avatar) }}" alt="{{ $other->name }}">
@else
{{ mb_strtoupper(mb_substr($other?->name ?? '?', 0, 1)) }}
@endif
</div>
<div class="msg-info">
<div class="msg-name">
{{ $other?->name ?? 'Silinmiş Kullanıcı' }}
@if($unread > 0)
<span style="font-size:.65rem;background:var(--accent2);color:#05050e;padding:1px 7px;border-radius:20px;font-weight:800">{{ $unread }}</span>
@endif
</div>
<div class="msg-preview">
@if($last)
@if($last->user_id === auth()->id()) <span style="color:rgba(255,255,255,.25)">Sen: </span> @endif
@if(str_starts_with($last->body, 'ANIMESHARE::'))
@php try { $sd = json_decode(substr($last->body,12),true); } catch(\Throwable $e){ $sd=null; } @endphp
<i class="bi bi-play-circle" style="color:var(--accent2)"></i> {{ $sd['title'] ?? 'Anime' }} paylaştı
@elseif(str_starts_with($last->body, 'IMAGE::'))
<i class="bi bi-image"></i> Fotoğraf
@elseif(str_starts_with($last->body, 'GIF::'))
<i class="bi bi-file-play"></i> GIF
@else
{{ Str::limit($last->body, 60) }}
@endif
@else
<em>Henüz mesaj yok</em>
@endif
</div>
</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:6px">
@if($unread > 0)<div class="msg-unread-dot"></div>@endif
<span class="msg-time">
@if($conv['updated_at']){{ $conv['updated_at']->diffForHumans(null, true) }}@endif
</span>
</div>
</a>
@endforeach
</div>
@endif
</div>
@endsection
@@ -0,0 +1,655 @@
@extends('frontend.layouts.app')
@section('title', ($other?->name ?? 'Mesaj') . ' — Animexe')
@push('styles')
<style>
footer{display:none!important}
#page-content{padding:0!important}
#ai-fab,#ai-panel{display:none!important}
#topnav,#mob-bottom-nav{display:none!important}
.w-chat-support{display:none!important}
.chat-root{
display:flex;flex-direction:column;
position:fixed;
top:0;left:0;right:0;bottom:0;
background:#05050e;
}
/* ── Header ─────────────────────────────── */
.chat-hdr{
display:flex;align-items:center;gap:10px;
padding:10px 14px;
background:rgba(5,5,14,.95);backdrop-filter:blur(12px);
border-bottom:1px solid rgba(255,255,255,.07);
flex-shrink:0;z-index:10;
}
.chat-hdr-back{
width:36px;height:36px;border-radius:10px;
display:flex;align-items:center;justify-content:center;
background:rgba(255,255,255,.06);border:none;
color:rgba(255,255,255,.6);font-size:1rem;cursor:pointer;
text-decoration:none;flex-shrink:0;transition:all .15s;
}
.chat-hdr-back:hover{background:rgba(255,255,255,.12);color:#fff}
.chat-hdr-ava{
width:38px;height:38px;border-radius:50%;overflow:hidden;flex-shrink:0;
background:linear-gradient(135deg,var(--accent2),var(--accent));
display:flex;align-items:center;justify-content:center;
font-size:.9rem;font-weight:800;color:#fff;text-decoration:none;
}
.chat-hdr-ava img{width:100%;height:100%;object-fit:cover}
.chat-hdr-info{flex:1;min-width:0}
.chat-hdr-name{font-size:.9rem;font-weight:800;color:#fff;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.chat-hdr-sub{font-size:.68rem;color:rgba(255,255,255,.3)}
.chat-hdr-btn{
width:36px;height:36px;border-radius:10px;flex-shrink:0;
display:flex;align-items:center;justify-content:center;
background:rgba(255,255,255,.06);border:none;color:rgba(255,255,255,.5);
font-size:.9rem;cursor:pointer;text-decoration:none;transition:all .15s;
}
.chat-hdr-btn:hover{background:rgba(255,255,255,.12);color:#fff}
.chat-hdr-btn.call{color:#22c55e}
.chat-hdr-btn.call:hover{background:rgba(34,197,94,.15);color:#22c55e}
/* ── Messages area ──────────────────────── */
.chat-body{
flex:1;overflow-y:auto;
padding:14px 14px 8px;
display:flex;flex-direction:column;
gap:4px;
-webkit-overflow-scrolling:touch;
min-height:0;
}
.chat-body::-webkit-scrollbar{width:3px}
.chat-body::-webkit-scrollbar-thumb{background:rgba(255,255,255,.1);border-radius:2px}
/* ── Bubbles ────────────────────────────── */
.chat-msg{display:flex;gap:8px;max-width:80%;align-items:flex-end}
.chat-msg.mine{align-self:flex-end;flex-direction:row-reverse}
.chat-msg.theirs{align-self:flex-start}
.chat-msg-ava{
width:28px;height:28px;border-radius:50%;overflow:hidden;flex-shrink:0;
background:linear-gradient(135deg,var(--accent2),var(--accent));
display:flex;align-items:center;justify-content:center;
font-size:.65rem;font-weight:800;color:#fff;
}
.chat-msg-ava img{width:100%;height:100%;object-fit:cover}
.chat-msg.mine .chat-msg-ava{display:none}
.chat-bubble{
padding:9px 14px;border-radius:18px;
font-size:.88rem;line-height:1.5;word-break:break-word;max-width:100%;
}
.chat-msg.mine .chat-bubble{
background:var(--accent2);color:#05050e;font-weight:500;border-bottom-right-radius:5px;
}
.chat-msg.theirs .chat-bubble{
background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.08);
color:rgba(255,255,255,.9);border-bottom-left-radius:5px;
}
.chat-time{font-size:.6rem;color:rgba(255,255,255,.25);align-self:flex-end;margin-bottom:3px;flex-shrink:0}
.chat-date-sep{
text-align:center;font-size:.65rem;color:rgba(255,255,255,.2);
padding:12px 0 4px;font-weight:600;letter-spacing:.08em;
}
/* ── Media messages ─────────────────────── */
.chat-img{
max-width:220px;max-height:220px;border-radius:14px;
cursor:pointer;display:block;object-fit:cover;
transition:opacity .15s;border:1px solid rgba(255,255,255,.08);
}
.chat-img:hover{opacity:.88}
.chat-gif{max-width:220px;border-radius:14px;display:block}
/* ── Share card ─────────────────────────── */
.share-card{
display:flex;gap:10px;align-items:flex-start;
background:rgba(0,245,255,.06);border:1px solid rgba(0,245,255,.18);
border-radius:14px;padding:10px 12px;text-decoration:none;
max-width:260px;transition:all .15s;
}
.share-card:hover{background:rgba(0,245,255,.12);border-color:rgba(0,245,255,.35)}
.share-card-img{width:54px;height:78px;border-radius:8px;overflow:hidden;flex-shrink:0;background:rgba(255,255,255,.05)}
.share-card-img img{width:100%;height:100%;object-fit:cover;display:block}
.share-card-label{font-size:.58rem;color:var(--accent2);font-weight:700;letter-spacing:.08em;text-transform:uppercase;margin-bottom:4px}
.share-card-title{font-size:.8rem;font-weight:800;color:#fff;line-height:1.3;margin-bottom:3px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.share-card-ep{font-size:.7rem;color:rgba(255,255,255,.5);margin-bottom:3px}
.share-card-play{font-size:.68rem;color:var(--accent2);display:flex;align-items:center;gap:3px;margin-top:4px;font-weight:700}
/* ── Footer input ───────────────────────── */
.chat-footer{
padding:8px 10px;
background:rgba(5,5,14,.95);backdrop-filter:blur(12px);
border-top:1px solid rgba(255,255,255,.07);
flex-shrink:0;
padding-bottom:max(8px,env(safe-area-inset-bottom));
}
.chat-toolbar{
display:flex;align-items:center;gap:6px;
padding:4px 0 8px;
border-bottom:1px solid rgba(255,255,255,.06);
margin-bottom:6px;
}
.chat-tool-btn{
width:38px;height:38px;border-radius:10px;border:none;
background:rgba(255,255,255,.06);color:rgba(255,255,255,.45);
font-size:.88rem;cursor:pointer;
display:flex;align-items:center;justify-content:center;
transition:all .15s;flex-shrink:0;
-webkit-tap-highlight-color:transparent;
}
.chat-tool-btn:hover{background:rgba(255,255,255,.13);color:#fff}
.chat-tool-btn.active{background:rgba(0,245,255,.12);color:var(--accent2)}
.chat-form{display:flex;align-items:flex-end;gap:8px}
.chat-input{
flex:1;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1);
border-radius:20px;padding:10px 16px;color:#fff;font-size:.9rem;font-family:inherit;
resize:none;max-height:120px;overflow-y:auto;min-height:42px;
line-height:1.4;outline:none;-webkit-appearance:none;transition:border-color .15s;
}
.chat-input:focus{border-color:rgba(0,245,255,.35)}
.chat-input::placeholder{color:rgba(255,255,255,.25)}
.chat-send{
width:42px;height:42px;border-radius:50%;flex-shrink:0;
background:var(--accent2);border:none;color:#05050e;
font-size:1rem;cursor:pointer;
display:flex;align-items:center;justify-content:center;
transition:all .15s;-webkit-tap-highlight-color:transparent;
}
.chat-send:hover{background:#00d4de;transform:scale(1.05)}
.chat-send:disabled{opacity:.4;transform:none;cursor:default}
/* ── Emoji picker ───────────────────────── */
#chat-emoji-picker{
display:none;flex-wrap:wrap;gap:2px;
background:rgba(10,10,22,.98);border:1px solid rgba(255,255,255,.1);
border-radius:14px;padding:10px;
max-height:180px;overflow-y:auto;margin-bottom:6px;
scrollbar-width:thin;
}
#chat-emoji-picker.show{display:flex}
.chat-emoji-btn{
font-size:1.3rem;width:36px;height:36px;
display:flex;align-items:center;justify-content:center;
border:none;background:none;cursor:pointer;border-radius:8px;
transition:background .12s;-webkit-tap-highlight-color:transparent;
}
.chat-emoji-btn:hover{background:rgba(255,255,255,.1)}
/* ── GIF picker ─────────────────────────── */
#chat-gif-picker{
display:none;
background:rgba(10,10,22,.98);border:1px solid rgba(255,255,255,.1);
border-radius:14px;padding:8px;margin-bottom:6px;
}
#chat-gif-picker.show{display:block}
.chat-gif-search{
width:100%;padding:8px 12px;border-radius:10px;
background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.12);
color:#fff;font-size:.82rem;font-family:inherit;outline:none;
margin-bottom:8px;-webkit-appearance:none;
}
.chat-gif-search::placeholder{color:rgba(255,255,255,.3)}
.chat-gif-grid{
display:grid;grid-template-columns:repeat(3,1fr);gap:4px;
max-height:160px;overflow-y:auto;
}
.chat-gif-item{
aspect-ratio:1;background:rgba(255,255,255,.05);border-radius:8px;
overflow:hidden;cursor:pointer;transition:opacity .15s;
}
.chat-gif-item:hover{opacity:.8}
.chat-gif-item img{width:100%;height:100%;object-fit:cover;display:block}
.chat-gif-ph{padding:20px;text-align:center;color:rgba(255,255,255,.25);font-size:.78rem}
.chat-empty{
flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
color:rgba(255,255,255,.2);gap:10px;
}
.chat-empty i{font-size:2.4rem;opacity:.2}
.chat-empty p{font-size:.85rem}
@media(min-width:640px){
.chat-root{max-width:760px;left:50%;transform:translateX(-50%)}
.chat-msg{max-width:72%}
}
@media(max-width:640px){
.chat-msg{max-width:88%}
}
</style>
@endpush
@section('content')
@php $me = auth()->user(); @endphp
<div class="chat-root">
{{-- Header --}}
<div class="chat-hdr">
<a href="{{ route('messages.index') }}" class="chat-hdr-back">
<i class="bi bi-chevron-left"></i>
</a>
<a href="{{ $other ? route('user.profile', $other) : '#' }}" class="chat-hdr-ava">
@if($other?->avatar)
<img src="{{ \App\Support\MediaUrl::fromStoragePath($other->avatar) }}" alt="{{ $other->name }}">
@else
{{ mb_strtoupper(mb_substr($other?->name ?? '?', 0, 1)) }}
@endif
</a>
<div class="chat-hdr-info">
<div class="chat-hdr-name">{{ $other?->name ?? 'Silinmiş Kullanıcı' }}</div>
@if($other?->username)
<div class="chat-hdr-sub">@php echo '@' . e($other->username); @endphp</div>
@endif
</div>
@if($other)
{{-- Sesli arama --}}
<button class="chat-hdr-btn call" id="call-btn" data-user-id="{{ $other->id }}" title="Sesli ara">
<i class="bi bi-telephone-fill"></i>
</button>
{{-- Profil --}}
<a href="{{ route('user.profile', $other) }}" class="chat-hdr-btn" title="Profil">
<i class="bi bi-person"></i>
</a>
@endif
</div>
{{-- Messages --}}
<div class="chat-body" id="chat-body">
@if($messages->isEmpty())
<div class="chat-empty">
<i class="bi bi-chat-dots"></i>
<p>Henüz mesaj yok. İlk mesajı sen gönder!</p>
</div>
@else
@php $lastDate = null; @endphp
@foreach($messages as $msg)
@php
$msgDate = $msg->created_at->toDateString();
$isMine = $msg->user_id === $me->id;
@endphp
@if($msgDate !== $lastDate)
<div class="chat-date-sep">{{ $msg->created_at->translatedFormat('d F Y') }}</div>
@php $lastDate = $msgDate; @endphp
@endif
<div class="chat-msg {{ $isMine ? 'mine' : 'theirs' }}" data-id="{{ $msg->id }}">
@if(!$isMine)
<div class="chat-msg-ava">
@if($msg->user?->avatar)
<img src="{{ \App\Support\MediaUrl::fromStoragePath($msg->user->avatar) }}" alt="">
@else{{ mb_strtoupper(mb_substr($msg->user?->name ?? '?', 0, 1)) }}@endif
</div>
@endif
<div>
@if(str_starts_with($msg->body, 'ANIMESHARE::'))
@php try { $sd = json_decode(substr($msg->body,12),true); } catch(\Throwable $e){ $sd=null; } @endphp
@if($sd)
<a href="{{ url('/watch/'.($sd['slug']??'').'/'.($sd['s']??1).'/'.($sd['e']??1).(($sd['ts']??0)>0?'?t='.$sd['ts']:'')) }}" class="share-card">
<div class="share-card-img">
@if(!empty($sd['cover']))<img src="{{ $sd['cover'] }}" loading="lazy" alt="">@else<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.2);font-size:1.4rem"><i class="bi bi-play-circle"></i></div>@endif
</div>
<div>
<div class="share-card-label"><i class="bi bi-play-fill"></i> Anime Bölümü</div>
<div class="share-card-title">{{ $sd['title'] ?? '' }}</div>
<div class="share-card-ep">{{ $sd['s'] ?? '' }}. Sezon {{ $sd['e'] ?? '' }}. Bölüm</div>
<div class="share-card-play"><i class="bi bi-play-circle-fill"></i> İzle</div>
</div>
</a>
@else
<div class="chat-bubble">{{ $msg->body }}</div>
@endif
@elseif(str_starts_with($msg->body, 'IMAGE::'))
<img src="{{ substr($msg->body, 7) }}" class="chat-img" loading="lazy" alt="Resim"
onclick="window.open('{{ substr($msg->body, 7) }}','_blank')">
@elseif(str_starts_with($msg->body, 'GIF::'))
<img src="{{ substr($msg->body, 5) }}" class="chat-gif" loading="lazy" alt="GIF">
@else
<div class="chat-bubble">{{ $msg->body }}</div>
@endif
</div>
<span class="chat-time">{{ $msg->created_at->format('H:i') }}</span>
</div>
@endforeach
@endif
</div>
{{-- Footer --}}
<div class="chat-footer">
<div id="chat-emoji-picker"></div>
<div id="chat-gif-picker">
<input type="text" class="chat-gif-search" id="chat-gif-input" placeholder="GIF ara…">
<div class="chat-gif-grid" id="chat-gif-grid">
<div class="chat-gif-ph">GIF aramak için yaz</div>
</div>
</div>
<div class="chat-toolbar">
<button class="chat-tool-btn" id="chat-emoji-btn" title="Emoji" style="display:none">
<i class="bi bi-emoji-smile"></i>
</button>
<button class="chat-tool-btn" id="chat-gif-btn" title="GIF">
<span style="font-size:.65rem;font-weight:800;letter-spacing:.03em">GIF</span>
</button>
<label class="chat-tool-btn" title="Resim gönder" style="cursor:pointer">
<i class="bi bi-image"></i>
<input type="file" id="chat-img-input" accept="image/*" style="display:none">
</label>
<div style="flex:1"></div>
</div>
<div class="chat-form">
<textarea class="chat-input" id="chat-input"
placeholder="Mesaj yaz..." rows="1" maxlength="2000"
autocomplete="off" autocorrect="off"></textarea>
<button type="button" class="chat-send" id="chat-send">
<i class="bi bi-send-fill"></i>
</button>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
// Crisp ve diğer overlay widgetları kapat
try { $crisp.push(['do','chat:hide']); } catch(_){}
document.addEventListener('DOMContentLoaded', function(){
try { $crisp.push(['do','chat:hide']); } catch(_){}
// topnav/bottom-nav kesinlikle gizle (layout JS override için)
const tn = document.getElementById('topnav');
const bn = document.getElementById('mob-bottom-nav');
if(tn) tn.style.setProperty('display','none','important');
if(bn) bn.style.setProperty('display','none','important');
});
</script>
<script>
(function(){
'use strict';
const SEND_URL = '{{ route("messages.send", $conversation) }}';
const POLL_URL = '{{ route("messages.poll", $conversation) }}';
const UPLOAD_URL = '{{ route("messages.upload-image") }}';
const TENOR_KEY = '{{ env("TENOR_API_KEY","") }}';
const CSRF = document.querySelector('meta[name=csrf-token]')?.content || '';
const ME_ID = {{ $me->id }};
const MY_AVA = {!! json_encode($me->avatar ? \App\Support\MediaUrl::fromStoragePath($me->avatar) : null) !!};
const MY_NAME = {!! json_encode($me->name) !!};
const IS_DESKTOP = window.innerWidth >= 640;
let lastId = {{ $messages->last()?->id ?? 0 }};
let polling = null;
let emojiOpen = false;
let gifOpen = false;
let gifTimer = null;
const chatBody = document.getElementById('chat-body');
const inputEl = document.getElementById('chat-input');
const sendBtn = document.getElementById('chat-send');
/* ── Scroll ──────────────────────────────── */
function scrollBottom(force = false) {
const near = chatBody.scrollHeight - chatBody.scrollTop - chatBody.clientHeight < 160;
if (force || near) chatBody.scrollTop = chatBody.scrollHeight;
}
scrollBottom(true);
/* ── Input auto-grow ─────────────────────── */
inputEl.addEventListener('input', () => {
inputEl.style.height = 'auto';
inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px';
if (inputEl.value) { hideEmoji(); hideGif(); }
});
inputEl.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); doSend(); }
});
sendBtn.addEventListener('click', doSend);
/* ── Escape chars ────────────────────────── */
function escH(s) {
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
/* ── Render message body ─────────────────── */
function renderBody(body) {
if (!body) return '<div class="chat-bubble"></div>';
if (body.startsWith('ANIMESHARE::')) {
try {
const d = JSON.parse(body.slice(12));
const url = `/watch/${d.slug}/${d.s}/${d.e}${d.ts>0?'?t='+d.ts:''}`;
const img = d.cover
? `<img src="${escH(d.cover)}" loading="lazy" alt="">`
: `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.2)"><i class="bi bi-play-circle" style="font-size:1.4rem"></i></div>`;
return `<a href="${escH(url)}" class="share-card">
<div class="share-card-img">${img}</div>
<div>
<div class="share-card-label"><i class="bi bi-play-fill"></i> Anime Bölümü</div>
<div class="share-card-title">${escH(d.title||'')}</div>
<div class="share-card-ep">${escH(String(d.s))}. Sezon ${escH(String(d.e))}. Bölüm</div>
<div class="share-card-play"><i class="bi bi-play-circle-fill"></i> İzle</div>
</div>
</a>`;
} catch(_) {}
}
if (body.startsWith('IMAGE::')) {
const url = escH(body.slice(7));
return `<img src="${url}" class="chat-img" loading="lazy" alt="Resim" onclick="window.open('${url}','_blank')">`;
}
if (body.startsWith('GIF::')) {
return `<img src="${escH(body.slice(5))}" class="chat-gif" loading="lazy" alt="GIF">`;
}
return `<div class="chat-bubble">${escH(body)}</div>`;
}
/* ── Append message to DOM ───────────────── */
function appendMsg(id, body, isMine, time, avatar, name) {
if (document.querySelector(`.chat-msg[data-id="${id}"]`)) return;
document.querySelector('.chat-empty')?.remove();
const wrap = document.createElement('div');
wrap.className = 'chat-msg ' + (isMine ? 'mine' : 'theirs');
wrap.dataset.id = id;
let ava = '';
if (!isMine) {
ava = avatar
? `<div class="chat-msg-ava"><img src="${escH(avatar)}" alt=""></div>`
: `<div class="chat-msg-ava">${escH((name||'?').charAt(0).toUpperCase())}</div>`;
}
wrap.innerHTML = ava + `<div>${renderBody(body)}</div><span class="chat-time">${escH(time)}</span>`;
chatBody.appendChild(wrap);
scrollBottom();
}
/* ── Send text ───────────────────────────── */
async function doSend() {
const text = inputEl.value.trim();
if (!text) return;
sendBtn.disabled = true;
inputEl.value = '';
inputEl.style.height = 'auto';
try {
const res = await fetch(SEND_URL, {
method: 'POST',
headers: {'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json', 'Content-Type': 'application/json'},
body: JSON.stringify({ body: text }),
});
if (!res.ok) throw new Error();
const d = await res.json();
appendMsg(d.id, d.body, true, d.created_at, MY_AVA, MY_NAME);
if (d.id > lastId) lastId = d.id;
scrollBottom(true);
} catch(e) {
inputEl.value = text;
}
sendBtn.disabled = false;
inputEl.focus();
}
/* ── Send image ──────────────────────────── */
document.getElementById('chat-img-input')?.addEventListener('change', async function() {
const file = this.files[0];
if (!file) return;
this.value = '';
try {
const fd = new FormData();
fd.append('image', file);
const r = await fetch(UPLOAD_URL, {
method: 'POST',
headers: {'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json'},
body: fd,
});
const d = await r.json();
if (!d.url) throw new Error();
const r2 = await fetch(SEND_URL, {
method: 'POST',
headers: {'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json', 'Content-Type': 'application/json'},
body: JSON.stringify({ body: 'IMAGE::' + d.url }),
});
const d2 = await r2.json();
appendMsg(d2.id, d2.body, true, d2.created_at, MY_AVA, MY_NAME);
if (d2.id > lastId) lastId = d2.id;
scrollBottom(true);
} catch(e) {
if (window.showToast) showToast('Resim gönderilemedi.', 'error');
}
});
/* ── Send GIF ────────────────────────────── */
window.chatSendGif = async function(gifUrl) {
hideGif();
try {
const r = await fetch(SEND_URL, {
method: 'POST',
headers: {'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json', 'Content-Type': 'application/json'},
body: JSON.stringify({ body: 'GIF::' + gifUrl }),
});
const d = await r.json();
appendMsg(d.id, d.body, true, d.created_at, MY_AVA, MY_NAME);
if (d.id > lastId) lastId = d.id;
scrollBottom(true);
} catch(_) {}
};
/* ── Polling ─────────────────────────────── */
async function poll() {
try {
const res = await fetch(`${POLL_URL}?after=${lastId}`, {
headers: {'Accept': 'application/json', 'X-CSRF-TOKEN': CSRF}
});
const d = await res.json();
d.messages.forEach(m => {
if (m.user_id !== ME_ID) {
appendMsg(m.id, m.body, false, m.created_at, m.avatar, m.name);
}
if (m.id > lastId) lastId = m.id;
});
} catch(_) {}
}
function startPoll() { polling = setInterval(poll, 3000); }
function stopPoll() { clearInterval(polling); polling = null; }
document.addEventListener('visibilitychange', () => document.hidden ? stopPoll() : startPoll());
startPoll();
/* ── Emoji picker ────────────────────────── */
const EMOJIS = '😀😂😅😊😍🥰😘😎😭😤😡🥺🤔😴🤩🥳😱🙈🙄😏🤗😬😵👍👎❤️🔥💯✨🎉🎊🎮🎬🎵🍕🍜🍣🎃👻🦊🐱🐶🐼🌸🌙⭐🌈💎🚀🏆🎯🔮';
function initEmoji() {
const p = document.getElementById('chat-emoji-picker');
if (!p || p.dataset.built) return;
p.dataset.built = '1';
[...EMOJIS].forEach(em => {
const btn = document.createElement('button');
btn.className = 'chat-emoji-btn';
btn.textContent = em;
btn.addEventListener('click', () => {
const pos = inputEl.selectionStart;
inputEl.value = inputEl.value.slice(0, pos) + em + inputEl.value.slice(inputEl.selectionEnd);
inputEl.selectionStart = inputEl.selectionEnd = pos + em.length;
inputEl.dispatchEvent(new Event('input'));
inputEl.focus();
});
p.appendChild(btn);
});
}
function toggleEmoji() {
emojiOpen = !emojiOpen;
if (emojiOpen) { hideGif(); initEmoji(); }
document.getElementById('chat-emoji-picker').classList.toggle('show', emojiOpen);
document.getElementById('chat-emoji-btn').classList.toggle('active', emojiOpen);
}
function hideEmoji() {
emojiOpen = false;
document.getElementById('chat-emoji-picker')?.classList.remove('show');
document.getElementById('chat-emoji-btn')?.classList.remove('active');
}
document.getElementById('chat-emoji-btn')?.addEventListener('click', toggleEmoji);
// Desktop'ta emoji butonu göster
if (IS_DESKTOP) {
const eb = document.getElementById('chat-emoji-btn');
if (eb) eb.style.display = '';
}
/* ── GIF picker ──────────────────────────── */
async function searchGifs(q) {
const grid = document.getElementById('chat-gif-grid');
if (!grid) return;
if (!TENOR_KEY) {
grid.innerHTML = '<div class="chat-gif-ph">Tenor API key ayarlanmamış.</div>';
return;
}
grid.innerHTML = '<div class="chat-gif-ph">Yükleniyor…</div>';
try {
const ep = q
? `https://tenor.googleapis.com/v2/search?q=${encodeURIComponent(q)}&key=${TENOR_KEY}&limit=12&media_filter=gif`
: `https://tenor.googleapis.com/v2/featured?key=${TENOR_KEY}&limit=12&media_filter=gif`;
const r = await fetch(ep);
const d = await r.json();
const results = d.results || [];
if (!results.length) { grid.innerHTML = '<div class="chat-gif-ph">Sonuç yok.</div>'; return; }
grid.innerHTML = '';
results.forEach(g => {
const url = g.media_formats?.tinygif?.url || g.media_formats?.gif?.url || '';
if (!url) return;
const item = document.createElement('div');
item.className = 'chat-gif-item';
item.innerHTML = `<img src="${escH(url)}" alt="" loading="lazy">`;
item.addEventListener('click', () => window.chatSendGif(url));
grid.appendChild(item);
});
} catch(_) { grid.innerHTML = '<div class="chat-gif-ph">Yüklenemedi.</div>'; }
}
function toggleGif() {
gifOpen = !gifOpen;
if (gifOpen) { hideEmoji(); if (TENOR_KEY) searchGifs(''); setTimeout(() => document.getElementById('chat-gif-input')?.focus(), 80); }
document.getElementById('chat-gif-picker').classList.toggle('show', gifOpen);
document.getElementById('chat-gif-btn').classList.toggle('active', gifOpen);
}
function hideGif() {
gifOpen = false;
document.getElementById('chat-gif-picker')?.classList.remove('show');
document.getElementById('chat-gif-btn')?.classList.remove('active');
}
document.getElementById('chat-gif-btn')?.addEventListener('click', toggleGif);
document.getElementById('chat-gif-input')?.addEventListener('input', function() {
clearTimeout(gifTimer);
gifTimer = setTimeout(() => searchGifs(this.value.trim()), 380);
});
/* ── Sesli arama ─────────────────────────── */
document.getElementById('call-btn')?.addEventListener('click', async function() {
const userId = this.dataset.userId;
if (!userId || !window.cpStartCall) { if (window.showToast) showToast('Arama özelliği yüklenemedi.','error'); return; }
window.cpConvUserId = userId;
window.cpStartCall();
});
/* ── Auto focus ──────────────────────────── */
if (IS_DESKTOP) inputEl.focus();
})();
</script>
@endpush