Initial commit

This commit is contained in:
Ronald Huynen
2026-03-23 21:37:59 +01:00
commit 2547717edb
2193 changed files with 972171 additions and 0 deletions

Binary file not shown.

View File

@@ -0,0 +1,4 @@
<div onclick="Livewire.dispatch('closeWireChatModal')">
{{ $slot }}
</div>

View File

@@ -0,0 +1,11 @@
@props([
'widget' => false
])
<x-wirechat::actions.open-modal
component="wirechat.new.chat"
:widget="$widget"
>
{{$slot}}
</x-wirechat::actions.open-modal>

View File

@@ -0,0 +1,11 @@
@props([
'widget' => false
])
<x-wirechat::actions.open-modal
component="wirechat.new.group"
:widget="$widget"
>
{{$slot}}
</x-wirechat::actions.open-modal>

View File

@@ -0,0 +1,16 @@
@props([
'component',
'conversation' => null,
'widget' => false
])
<div {{ $attributes }} onclick="Livewire.dispatch('openChatDrawer', {
component: '{{ $component }}',
arguments: {
conversation: `{{$conversation ?? null }}`,
widget: @js($widget)
}
})">
{{ $slot }}
</div>

View File

@@ -0,0 +1,16 @@
@props([
'component',
'conversation' => null,
'widget' => false
])
<div onclick="Livewire.dispatch('openWireChatModal', {
component: '{{ $component }}',
arguments: {
conversation:`{{$conversation ?? null }}`,
widget: @js($widget)
}
})">
{{ $slot }}
</div>

View File

@@ -0,0 +1,14 @@
@props([
'conversation' => null, //Should be conversation ID (Int)
'widget' => false
])
<x-wirechat::actions.open-chat-drawer
component="wirechat.chat.info"
dusk="show_chat_info"
conversation="{{$conversation}}"
:widget="$widget"
>
{{$slot}}
</x-wirechat::actions.open-chat-drawer>

View File

@@ -0,0 +1,14 @@
@props([
'conversation' => null, //Should be conversation ID (Int)
'widget' => false
])
<x-wirechat::actions.open-chat-drawer
component="wirechat.chat.group.info"
dusk="show_group_info"
conversation="{{$conversation}}"
:widget="$widget"
>
{{$slot}}
</x-wirechat::actions.open-chat-drawer>

View File

@@ -0,0 +1,67 @@
@props(['src' => null, 'story' => null, 'group' => false, 'disappearing' => false])
<div
{{ $attributes->merge([
'class' => "shrink-0 inline-flex items-center justify-center relative transition outline outline-1 outline-offset-1 outline-gray-500
overflow-visible rounded-full border border-[var(--wc-light-secondary)] text-gray-500 bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)]
dark:border-[var(--wc-dark-secondary)] text-base ",
])->class(
$story ? 'bg-linear-to-r p-[2px] ring-2 ring-white from-purple-400 via-pink-500 to-red-500 rounded-full' : ' ',
) }}>
@if ($src)
<img loading="lazy" @class([
'shrink-0 w-full h-full object-cover object-center rounded-full',
]) src="{{ $src }}" alt="" />
@endif
@if (!$src && $group==true)
{{-- <svg class="shrink-0 scale-90 w-full h-full rounded-full text-gray-300 bg-gray-100 dark:bg-gray-600" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16">
<path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6m-5.784 6A2.24 2.24 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.3 6.3 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1zM4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5" />
</svg> --}}
{{-- <svg class="shrink-0 scale-95 w-full h-full rounded-full text-gray-300 bg-gray-100 dark:bg-gray-600" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" data-slot="icon">
<path d="M8.5 4.5a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0ZM10.9 12.006c.11.542-.348.994-.9.994H2c-.553 0-1.01-.452-.902-.994a5.002 5.002 0 0 1 9.803 0ZM14.002 12h-1.59a2.556 2.556 0 0 0-.04-.29 6.476 6.476 0 0 0-1.167-2.603 3.002 3.002 0 0 1 3.633 1.911c.18.522-.283.982-.836.982ZM12 8a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"></path>
</svg> --}}
<svg class="shrink-0 p-px w-full h-full rounded-full text-gray-400 dark:text-gray-300 bg-gray-100 bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)]"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" data-slot="icon">
<path
d="M7 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM14.5 9a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM1.615 16.428a1.224 1.224 0 0 1-.569-1.175 6.002 6.002 0 0 1 11.908 0c.058.467-.172.92-.57 1.174A9.953 9.953 0 0 1 7 18a9.953 9.953 0 0 1-5.385-1.572ZM14.5 16h-.106c.07-.297.088-.611.048-.933a7.47 7.47 0 0 0-1.588-3.755 4.502 4.502 0 0 1 5.874 2.636.818.818 0 0 1-.36.98A7.465 7.465 0 0 1 14.5 16Z">
</path>
</svg>
@elseif(!$src)
<svg class="shrink-0 w-full h-full rounded-full" fill="currentColor" viewBox="0 0 24 24">
<path
d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
{{-- <svg class="shrink-0 w-full h-full mt-auto rounded-full" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd" />
</svg> --}}
@endif
@if ($disappearing)
<span dusk="disappearing_messages_icon"
class="absolute z-50 -bottom-1 bg-white -right-2 dark:bg-gray-800 rounded-full p-px">
<svg class="w-5 h-5" viewBox="0 0 36 36" height="36" width="36" preserveAspectRatio="xMidYMid meet"
fill="none">
<title>disappearing</title>
<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"
d="M18 31.5C18.0909 31.5 18.1817 31.4991 18.2722 31.4973C19.1005 31.4809 19.7586 30.7961 19.7422 29.9679C19.7258 29.1396 19.041 28.4815 18.2128 28.4979C18.142 28.4993 18.0711 28.5 18 28.5V31.5ZM18 7.5C18.0711 7.5 18.142 7.5007 18.2128 7.50211C19.041 7.51853 19.7258 6.86039 19.7422 6.03213C19.7586 5.20387 19.1005 4.51912 18.2722 4.5027C18.1817 4.5009 18.0909 4.5 18 4.5V7.5ZM24.5153 6.17374C23.7901 5.77341 22.8776 6.03683 22.4772 6.76211C22.0769 7.48739 22.3403 8.39988 23.0656 8.8002C23.1891 8.86838 23.3111 8.93898 23.4316 9.01195C24.1401 9.44118 25.0625 9.21475 25.4917 8.5062C25.921 7.79765 25.6945 6.87529 24.986 6.44605C24.8311 6.35223 24.6742 6.26144 24.5153 6.17374ZM29.554 11.014C29.1247 10.3055 28.2024 10.079 27.4938 10.5083C26.7852 10.9375 26.5588 11.8599 26.9881 12.5684C27.061 12.6889 27.1316 12.8109 27.1998 12.9344C27.6001 13.6597 28.5126 13.9231 29.2379 13.5228C29.9632 13.1224 30.2266 12.2099 29.8263 11.4847C29.7386 11.3258 29.6478 11.1689 29.554 11.014ZM31.4973 17.7278C31.4809 16.8995 30.7961 16.2414 29.9679 16.2578C29.1396 16.2742 28.4815 16.959 28.4979 17.7872C28.4993 17.858 28.5 17.9289 28.5 18C28.5 18.0711 28.4993 18.142 28.4979 18.2128C28.4815 19.041 29.1396 19.7258 29.9679 19.7422C30.7961 19.7586 31.4809 19.1005 31.4973 18.2722C31.4991 18.1817 31.5 18.0909 31.5 18C31.5 17.9091 31.4991 17.8183 31.4973 17.7278ZM29.8263 24.5153C30.2266 23.7901 29.9632 22.8776 29.2379 22.4772C28.5126 22.0769 27.6001 22.3403 27.1998 23.0656C27.1316 23.1891 27.061 23.3111 26.9881 23.4316C26.5588 24.1401 26.7852 25.0625 27.4938 25.4917C28.2024 25.921 29.1247 25.6945 29.554 24.986C29.6478 24.8311 29.7386 24.6742 29.8263 24.5153ZM24.986 29.554C25.6945 29.1247 25.921 28.2024 25.4917 27.4938C25.0625 26.7852 24.1401 26.5588 23.4316 26.9881C23.3111 27.061 23.1891 27.1316 23.0656 27.1998C22.3403 27.6001 22.0769 28.5126 22.4772 29.2379C22.8776 29.9632 23.7901 30.2266 24.5153 29.8263C24.6742 29.7386 24.8311 29.6478 24.986 29.554Z">
</path>
<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"
d="M18.0001 4.5C18 4.5 17.9999 4.5 17.9998 4.5C10.5439 4.5 4.49976 10.5442 4.49976 18C4.49976 25.4558 10.5439 31.5 17.9998 31.5C17.9999 31.5 18 31.5 18.0001 31.5V28.5C18 28.5 17.9999 28.5 17.9998 28.5C12.2008 28.5 7.49976 23.799 7.49976 18C7.49976 12.201 12.2008 7.5 17.9998 7.5C17.9999 7.5 18 7.5 18.0001 7.5V4.5Z">
</path>
<path fill="currentColor"
d="M23.3247 12.0107C23.669 11.7525 24.1507 11.7867 24.455 12.091V12.091C24.7593 12.3953 24.7935 12.877 24.5353 13.2213L19.9714 19.3066C19.2589 20.2566 17.8701 20.3553 17.0304 19.5156V19.5156C16.1907 18.6759 16.2894 17.2871 17.2394 16.5746L23.3247 12.0107Z">
</path>
</svg>
</span>
@endif
</div>

View File

@@ -0,0 +1,4 @@
<div
{{$attributes->merge([ 'class'=>"w-full h-2 shadow-xs bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)] "])}} >
</div>

View File

@@ -0,0 +1 @@
<div {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-[var(--wc-light-primary)] dark:hover:bg-[var(--wc-dark-primary)] focus:outline-hidden focus:bg-[var(--wc-light-primary)] dark:focus:bg-[var(--wc-dark-primary)] transition duration-150 ease-in-out']) }}>{{ $slot }}</div>

View File

@@ -0,0 +1 @@
<a {{ $attributes->merge(['class' => 'block w-full cursor-pointer px-4 py-3 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)] hover:bg-[var(--wc-light-primary)] dark:hover:bg-[var(--wc-dark-primary)] focus:outline-hidden focus:bg-[var(--wc-light-primary)] dark:focus:bg-[var(--wc-dark-primary)] transition duration-150 ease-in-out']) }}>{{ $slot }}</a>

View File

@@ -0,0 +1,46 @@
@props(['align' => 'right', 'width' => '48', 'contentClasses' => ''])
@php
switch ($align) {
case 'left':
$alignmentClasses = 'ltr:origin-top-left rtl:origin-top-right start-0';
break;
case 'top':
$alignmentClasses = 'origin-top';
break;
case 'right':
default:
$alignmentClasses = 'ltr:origin-top-right rtl:origin-top-left end-0';
break;
}
switch ($width) {
case '48':
$width = 'w-48';
break;
}
@endphp
<div x-ref="button" class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
<div @click="open = ! open">
{{ $trigger }}
</div>
<div x-show="open"
x-anchor="$refs.button"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
{{-- class="absolute z-50 mt-2 shadow-lg {{ $alignmentClasses }}" --}}
{{$attributes->merge(['class'=>"rounded-lg absolute z-50 mt-2 shadow-lg w-48 bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)] rounded-md border border-[var(--wc-light-secondary)] dark:border-[var(--wc-dark-secondary)] shadow-sm overflow-hidden"])}}
style="display: none;"
@click="open = false">
<div>
{{ $content }}
</div>
</div>
</div>

View File

@@ -0,0 +1,16 @@
@use('Namu\WireChat\Facades\WireChat')
{{-- Define previous message outside the loop --}}
<div class="w-full flex items-center ">
<span {{$attributes->merge([ 'class'=>"mx-auto w-5 h-5 "])}} >
<svg aria-hidden="true" class="w-full h-full text-gray-200 animate-spin dark:text-gray-600 "
viewBox="0 0 100 101" fill="var(--wc-brand-primary)" xmlns="http://www.w3.org/2000/svg">
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor" />
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill" />
</svg>
</span>
</div>

View File

@@ -0,0 +1,133 @@
@use('Namu\WireChat\Facades\WireChat')
@php
$activeGuard = session('active_guard', 'web');
$authUser = Auth::guard($activeGuard)->user();
@endphp
@if($authUser && WireChat::notificationsEnabled())
<div dusk="notification_manager"
x-data="{
showNotification(e) {
const message = e.message;
const redirect_url = e.redirect_url;
if (Notification.permission !== 'granted') {
return;
}
let title = message.sendable?.display_name || 'User';
let body = message.body;
let icon = message.sendable?.cover_url;
if (message.conversation.type == 'group') {
title = message.conversation?.group?.name;
body = message.sendable?.display_name + ': ' + message.body;
icon = message.conversation?.group?.cover_url;
}
const options = {
body: body,
icon: icon,
vibrate: [200, 100, 200],
tag: 'wirechat-notification-' + message.conversation_id,
renotify: true,
data: {
url: redirect_url,
type: 'SHOW_NOTIFICATION',
tag: 'wirechat-notification-' + message.conversation_id
}
};
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then((registration )=> {
// Service worker is fully ready
registration.active.postMessage({
type: 'SHOW_NOTIFICATION',
title: title,
options: options
});
})
.catch(error => {
// Fallback to regular notifications
this.newNotification(title, options);
});
} else {
this.newNotification(title, options);
}
},
newNotification(title,options){
const notification= new Notification(title, options);
notification.onclick = (event) => {
event.preventDefault();
const convId = message.conversation_id || 'default';
const windowName = 'wirechat-conversation';
const url = event.currentTarget.data.url;
const openedWindow = window.open(url, windowName);
if (openedWindow) {
openedWindow.focus();
}
//Close current notification
event.currentTarget.close();
};
},
registerServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(`{{asset(config('wirechat.notifcations.main_sw_script','sw.js'))}}`)
.then(reg => {})
.catch(err => {});
}
}
}"
x-init="
registerServiceWorker();
userId = @js($authUser->id);
encodedType = @js(\Namu\WireChat\Helpers\MorphClassResolver::encode($authUser->getMorphClass()));
{{-- We listen to notify participant event --}}
Echo.private(`participant.${encodedType}.${userId}`)
.listen('.Namu\\WireChat\\Events\\NotifyParticipant', (e) => {
{{--Ignore if user is currently open in the chat --}}
if (e.redirect_url !== window.location.href) {
if (!('Notification' in window)) {
} else if (Notification.permission === 'granted') {
showNotification(e);
} else if (Notification.permission !== 'denied') {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
showNotification(e);
}
});
}
}
});
document.addEventListener('chat-opened', (event) => {
const conversation = event.detail.conversation;
const tag = 'wirechat-notification-' + conversation;
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
type: 'CLOSE_NOTIFICATION',
tag: tag
});
}
});
">
</div>
@endif

View File

@@ -0,0 +1,67 @@
<div class="h-[calc(100vh)] flex flex-col bg-white dark:bg-gray-900 h-full">
<header class="w-full bg-gray-50 dark:bg-gray-900 animate-pulse h-16 sticky gap-5 inset-x-0 items-center flex p-5 top-0 z-10 border-gray-100 dark:border-gray-700 border-b">
<div class="rounded-full bg-gray-100 dark:bg-slate-800/40 h-9 w-9 animate-pulse ">
</div>
<div class="bg-gray-100 dark:bg-slate-800/40 h-4 w-72 animate-pulse rounded-xl">
</div>
<div class="rounded-full bg-gray-100 dark:bg-slate-800/40 h-9 w-2 ml-auto animate-pulse ">
</div>
</header>
<main class=" bg-white dark:bg-gray-900 flex flex-col animate-pulse grow h-10/12 h-full">
<div class="bg-gray-100 dark:bg-slate-800/40 h-9 w-48 mt-12 mx-auto animate-pulse rounded-xl">
</div>
<div class="m-auto">
{{-- Snipper --}}
<span
style="
width: 15px;
height: 15px;
border-radius: 50%;
display: inline-block;
position: relative;
border: 10px solid;
border-color: rgba(230, 228, 228, 0.063) rgba(237, 235, 235, 0.094) rgba(255, 255, 255, 0.104) rgba(255, 255, 255, 0.23);
box-sizing: border-box;
"
class=" animate-spin ">
</span>
</div>
</main>
<!-- Loading spinner... -->
<foooter class=" sticky bottom-0 w-full h-16 flex items-center gap-3 p-4 dark:bg-gray-900 bg-gray-50 h-10 animate-pulse border-t dark:border-gray-700">
<span class="rounded-full bg-gray-100 dark:bg-slate-800/40 h-9 w-9 animate-pulse ">
</span>
<span class="rounded-full bg-gray-100 dark:bg-slate-800/40 h-9 w-7 animate-pulse ">
</span>
<div class="bg-gray-100 dark:bg-slate-800/40 h-8 w-11/12 animate-pulse rounded-xl">
</div>
<span class="rounded-full bg-gray-100 dark:bg-slate-800/40 h-9 w-9 animate-pulse ">
</span>
</foooter>
</div>

View File

@@ -0,0 +1,76 @@
@props([
'position'=>'bottom',
'popoverOffset'=>'20'
]
)
<div x-data="{
popoverOpen: false,
popoverArrow: false,
popoverPosition: 'top',
popoverHeight: 0,
popoverOffset: 40,
popoverHeightCalculate() {
this.$refs.popover.classList.add('invisible');
this.popoverOpen=true;
let that=this;
$nextTick(function(){
that.popoverHeight = that.$refs.popover.offsetHeight;
that.popoverOpen=false;
that.$refs.popover.classList.remove('invisible');
that.$refs.popoverInner.setAttribute('x-transition', '');
that.popoverPositionCalculate();
});
},
popoverPositionCalculate(){
if(window.innerHeight < (this.$refs.popoverButton.getBoundingClientRect().top + this.$refs.popoverButton.offsetHeight + this.popoverOffset + this.popoverHeight)){
this.popoverPosition = 'top';
} else {
this.popoverPosition = 'bottom';
}
}
}"
x-init="
that = this;
window.addEventListener('resize', function(){
popoverPositionCalculate();
});
$watch('popoverOpen', function(value){
if(value){
popoverPositionCalculate();
let el = document.getElementById('width');
if(el){
el.focus();
}
}
});
"
class="relative overflow-visible">
<button {{$trigger->attributes->class(["flex items-center cursor-pointer hover:scale-105 transition-transform justify-center disabled:cursor-progress"] )}} type="button" x-ref="popoverButton" @click="popoverOpen=!popoverOpen">
{{$trigger}}
</button>
<div x-ref="popover"
x-anchor.offset.17="$refs.popoverButton"
x-show="popoverOpen"
x-init="setTimeout(function(){ popoverHeightCalculate(); }, 100);"
@click.away="popoverOpen=false;"
@keydown.escape.window="popoverOpen=false"
class=" min-w-[13rem] max-w-fit "
x-cloak
@click="popoverOpen=false" >
<div
x-ref="popoverInner" x-show="popoverOpen" class="w-full p-2 bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-secondary)] border border-[var(--wc-light-secondary)] dark:border-[var(--wc-dark-primary)] rounded-lg shadow-sm ">
<div x-show="popoverArrow && popoverPosition == 'bottom'" class="absolute top-0 inline-block w-5 mt-px overflow-hidden -translate-x-2 -translate-y-2.5 left-1/2"><div class="w-2.5 h-2.5 origin-bottom-left transform rotate-45 bg-white border-t border-l rounded-xs"></div></div>
<div x-show="popoverArrow && popoverPosition == 'top'" class="absolute bottom-0 inline-block w-5 mb-px overflow-hidden -translate-x-2 translate-y-2.5 left-1/2"><div class="w-2.5 h-2.5 origin-top-left transform -rotate-45 bg-white border-b border-l rounded-xs"></div></div>
<div class="grid gap-4">
{{$slot}}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,83 @@
<div x-data="{
bannerVisible: false,
type:'default',
message:' ',
bannerVisibleAfter: 300,
counter: 3000,
timer: null,
closeToast:function(){
this.bannerVisible = false;
clearInterval(this.timer);
}
}"
@wirechat-toast.window="
message = $event.detail.message;
type = $event.detail.type;
bannerVisible = true;
counter = 3000;
if (timer) clearInterval(timer);
timer = setInterval(() => {
if (counter <= 0) {
bannerVisible = false;
clearInterval(timer);
} else {
counter -= 100;
}
}, 100);
"
@mouseenter="clearInterval(timer)"
@mouseleave="
timer = setInterval(() => {
if (counter <= 0) {
bannerVisible = false;
clearInterval(timer);
} else {
counter -= 100;
}
}, 100);
"
x-show="bannerVisible"
x-transition:enter="transition ease-out duration-500"
x-transition:enter-start="-translate-y-10"
x-transition:enter-end="translate-y-0"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="translate-y-0"
x-transition:leave-end="-translate-y-10"
class="fixed sm:top-2 top-0 z-50 inset-x-0 sm:max-w-md mx-auto sm:ml-auto sm:mx-0 w-full h-auto py-2.5 duration-300 ease-out bg-white shadow-md sm:border rounded-md " x-cloak>
<div class="flex items-center justify-between w-full h-full px-3 mx-auto max-w-7xl ">
<div class="flex items-center gap-3 w-full h-full ">
<span x-show="type=='warning'" x-cloak>
<svg class="w-4 h-4 sm:w-5 sm:h-5 text-yellow-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" >
<path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clip-rule="evenodd" />
</svg>
</span>
<span x-show="type=='danger'" x-cloak>
<svg class="w-4 h-4 sm:w-5 sm:h-5 text-rose-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 0 1 .67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 1 1-.671-1.34l.041-.022ZM12 9a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clip-rule="evenodd" />
</svg>
</span>
<span x-show="type=='success'" x-cloak>
<svg class="w-4 h-4 sm:w-5 sm:h-5 text-green-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" >
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clip-rule="evenodd" />
</svg>
</span>
<p class="text-xs text-black " x-text="message"></p>
</div>
<button @click="closeToast()" class="flex items-center shrink-0 translate-x-1 ease-out duration-150 justify-center w-6 h-6 p-1.5 text-black rounded-full hover:bg-neutral-100">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-full h-full"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
</div>

View File

@@ -0,0 +1,73 @@
@props([
'source'=>null,
'controls'=>true,
'cover'=>true,
'height'=>"auto",
'showToggleSound'=>true,
])
<div x-data="{playing:false,muted:false}"
class="relative "
@click.outside="$refs.player.pause()"
x-intersect:leave="$refs.player.pause()">
<video x-ref="player" src="{{$source}}" @play="playing=true" @pause="playing=false"
class=" w-auto dark:bg-gray-600 border rounded-xl border-gray-50 dark:border-gray-700 rounded-xl {{$cover==true?'object-cover':''}} {{$height}}">
your browser does not support html5 video
</video>
@if ($controls==true)
{{-- play --}}
<div x-cloak x-show="!playing" @click="$refs.player.play()" class="absolute z-10 inset-0 flex items-center justify-center w-full h-full cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play-fill w-16 h-16 text-white" viewBox="0 0 16 16">
<path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z"/>
</svg>
</div>
{{-- pause button --}}
<div x-show="playing" @click="$refs.player.pause()" class="absolute z-10 inset-0 flex items-center justify-center w-full h-full cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pause-fill w-16 h-16 text-white invisible" viewBox="0 0 16 16">
<path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z"/>
</svg>
</div>
{{-- mute --}}
@if ($showToggleSound==true)
<div class="absolute z-100 bottom-2 right-2 m-4 bg-gray-900 text-white rounded-lg p-1 cursor-pointer">
{{-- mute --}}
<svg x-cloak x-show="!muted" @click="$refs.player.muted=true;muted=true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-volume-mute-fill w-h w-4" viewBox="0 0 16 16">
<path d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zm7.137 2.096a.5.5 0 0 1 0 .708L12.207 8l1.647 1.646a.5.5 0 0 1-.708.708L11.5 8.707l-1.646 1.647a.5.5 0 0 1-.708-.708L10.793 8 9.146 6.354a.5.5 0 1 1 .708-.708L11.5 7.293l1.646-1.647a.5.5 0 0 1 .708 0z"/>
</svg>
<svg x-show="muted" @click="$refs.player.muted=false;muted=false" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-volume-off-fill w-4 h-4" viewBox="0 0 16 16">
<path d="M10.717 3.55A.5.5 0 0 1 11 4v8a.5.5 0 0 1-.812.39L7.825 10.5H5.5A.5.5 0 0 1 5 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06z"/>
</svg>
{{-- unmute --}}
</div>
@endif
@endif
</div>

View File

@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" data-theme="@themeId">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title ?? config('app.name', 'Laravel') }}</title>
<!--THEME:--ADD TO TOP OT PREVENT FLICKERING -->
<script>
/* Function to apply or remove the dark theme */
function updateTheme(isDark) {
if (isDark) {
// document.documentElement.classList.add('dark'); // Add dark class to the root element
} else {
document.documentElement.classList.remove('dark');
}
}
/* Check the initial theme preference */
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
updateTheme(darkModeMediaQuery.matches);
/* listen to changed in (prefers-color-scheme: dark) */
darkModeMediaQuery.addEventListener('change', (event) => {
updateTheme(event.matches);
});
/* Add This to update theme when page is wire navigated */
document.addEventListener('livewire:navigated', () => {
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
updateTheme(darkModeMediaQuery.matches); // Re-apply the theme based on system preference
});
</script>
<!-- Fonts -->
{{-- <link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" /> --}}
<!-- Dynamic Theme CSS Custom Properties -->
<style>
:root {
{!! theme_css_vars() !!}
}
</style>
@vite(['resources/css/app.css', 'resources/css/fonts.css', 'resources/js/app.js'])
@livewireStyles
@wirechatStyles
@wireUiScripts
<style>
/* Theme-aware typography */
body {
font-family: var(--font-family-body, 'Poppins', sans-serif) !important;
}
/* Theme-aware heading styles */
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-family-heading, 'Oswald', sans-serif) !important;
text-transform: var(--heading-transform, uppercase) !important;
}
/* Apply theme-specific CSS custom properties */
:root {
@themeCssVars
}
</style>
</head>
<body class="font-sans antialiased flex flex-col min-h-screen">
<x-jetstream.banner />
<x-jetstream.toaster />
<div class="flex-grow bg-gray-100">
@livewire('navigation-menu')
<x-notifications position="bottom-end" />
<header class="bg-theme-brand shadow lg:mt-16">
<!-- System Anounnucement -->
@livewire('system-announcement', ['type' => 'SiteContents\SystemAnnouncement' ?? null, 'limit' => 1])
<!-- Header --->
<div class="max-w-7xl mx-auto pt-1 pb-2 px-4 sm:px-6 lg:px-8">
<div class="mt-2 text-xl font-semibold leading-tight text-gray-100">
{{ __('Chat messenger') }}
</div>
</div>
</header>
<div class="bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)]">
<!-- Page Content -->
<main class="h-[calc(100vh_-_3rem)] lg:h-[calc(100vh_-_7rem)] max-w-7xl mx-auto overflow-hidden bg-white shadow-xl">
{{ $slot }}
</main>
</div>
</div>
@auth
@livewire('account-info-modal')
@endauth
@livewireScripts
@wirechatAssets
</body>
</html>

View File

@@ -0,0 +1,325 @@
{{-- Import helper function to use in chatbox --}}
@use('Namu\WireChat\Helpers\Helper')
@use('Namu\WireChat\Facades\WireChat')
@php
$primaryColor = WireChat::getColor();
@endphp
@assets
<style>
emoji-picker {
width: 100% !important;
height: 100%;
}
/* Emoji picker configuration */
emoji-picker {
--background: none !important;
--border-radius: 12px;
--input-border-color: rgb(229 229 229);
--input-padding: 0.45rem;
--outline-color: none;
--outline-size: 1px;
--num-columns: 8;
/* Mobile-first default */
--emoji-padding: 0.7rem;
--emoji-size: 1.5rem;
/* Smaller size for mobile */
--border-color: none;
--indicator-color: #9ca3af;
}
@media screen and (min-width: 600px) {
emoji-picker {
--num-columns: 10;
/* Increase columns for larger screens */
--emoji-size: 1.8rem;
/* Larger size for desktop */
}
}
@media screen and (min-width: 900px) {
emoji-picker {
--num-columns: 16;
/* Increase columns for larger screens */
--emoji-size: 1.9rem;
/* Larger size for desktop */
}
}
/* Dark mode using prefers-color-scheme */
@media (prefers-color-scheme: dark) {
emoji-picker {
--background: none !important;
--input-border-color: var(--wc-dark-border);
--outline-color: none;
--outline-size: 1px;
--border-color: none;
--input-font-color: white;
--indicator-color: var(--wc-dark-accent);
--button-hover-background: var(--wc-dark-accent);
}
}
/* Ensure dark mode takes precedence */
.dark emoji-picker {
--background: none !important;
--input-border-color: var(--wc-dark-border);
--outline-color: none;
--outline-size: 1px;
--border-color: none;
--input-font-color: white;
--indicator-color: var(--wc-dark-accent);
--button-hover-background: var(--wc-dark-accent);
}
</style>
@endassets
<div x-data="{
initializing: true,
conversationId:@js($conversation->id),
conversationElement: document.getElementById('conversation'),
loadEmojiPicker() {
if (!document.head.querySelector('script[src*=\'/js/vendor/emoji-picker-element/index.js\']')) {
// Clear old IndexedDB database that may have cached CDN data
if (window.indexedDB) {
indexedDB.deleteDatabase('emoji-picker-element');
}
let script = document.createElement('script');
script.type = 'module';
script.async = true; // Load asynchronously
// Add cache-busting parameter to force reload of updated files
script.src = '/js/vendor/emoji-picker-element/index.js?v=' + Date.now();
// After the script loads, configure all emoji pickers
script.onload = () => {
setTimeout(() => {
document.querySelectorAll('emoji-picker').forEach(picker => {
picker.dataSource = '/js/vendor/emoji-picker-element-data/en/emojibase/data.json';
});
}, 100);
};
document.head.appendChild(script);
}
},
get isWidget() {
return $wire.widget == true;
}
}"
x-init="setTimeout(() => {
requestAnimationFrame(() => {
initializing = false;
$wire.dispatch('focus-input-field');
loadEmojiPicker();
{{-- if (isWidget) { --}}
//NotifyListeners about chat opened
$wire.dispatch('chat-opened',{conversation:conversationId});
{{-- } --}}
});
}, 120);"
class="w-full transition bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] overflow-hidden h-full relative" style="contain:content">
<div class=" flex flex-col grow h-full min-h-0 relative ">
{{-- ---------- --}}
{{-- --Header-- --}}
{{-- ---------- --}}
@include('wirechat::livewire.chat.partials.header', [ 'conversation' => $conversation, 'receiver' => $receiver])
{{-- ---------- --}}
{{-- -Body----- --}}
{{-- ---------- --}}
<div class="flex-1 min-h-0">
@include('wirechat::livewire.chat.partials.body', [ 'conversation' => $conversation, 'authParticipant' => $authParticipant, 'loadedMessages' => $loadedMessages, 'isPrivate' => $conversation->isPrivate(), 'isGroup' => $conversation->isGroup(), 'receiver' => $receiver])
</div>
{{-- ---------- --}}
{{-- -Footer--- --}}
{{-- ---------- --}}
<livewire:wire-chat.typing-indicator :conversation-id="$conversation->id" />
@include('wirechat::livewire.chat.partials.footer', [ 'conversation' => $conversation, 'authParticipant' => $authParticipant, 'media' => $media, 'files' => $files, 'replyMessage' => $replyMessage])
</div>
<livewire:wirechat.chat.drawer />
{{-- Add this script directly to your WireChat chat template --}}
<script>
document.addEventListener('DOMContentLoaded', function() {
function setupWireChatTyping() {
const typingIndicators = document.querySelectorAll('.wirechat-typing-indicator');
if (typingIndicators.length === 0) {
return;
}
typingIndicators.forEach((indicator, index) => {
const wireId = indicator.getAttribute('wire:id');
if (!wireId) {
return;
}
// Find WireChat specific inputs
const messageInputs = document.querySelectorAll(
'input[wire\\:model*="message"], ' +
'textarea[wire\\:model*="message"], ' +
'input[placeholder*="message"], ' +
'textarea[placeholder*="message"], ' +
'input[placeholder*="Type"], ' +
'textarea[placeholder*="Type"]'
);
if (messageInputs.length === 0) {
messageInputs = document.querySelectorAll('input[type="text"], textarea');
}
let typingTimer;
let isTyping = false;
function startTyping() {
if (!isTyping) {
isTyping = true;
try {
const component = window.Livewire.find(wireId);
if (component && typeof component.call === 'function') {
component.call('startTyping');
} else {
}
} catch (e) {
}
} else {
}
// Reset timer
clearTimeout(typingTimer);
typingTimer = setTimeout(() => {
stopTyping();
}, 3000);
}
function stopTyping() {
if (isTyping) {
isTyping = false;
clearTimeout(typingTimer);
try {
const component = window.Livewire.find(wireId);
if (component && typeof component.call === 'function') {
component.call('stopTyping');
}
} catch (e) {
}
}
}
// Add event listeners with improved logic
messageInputs.forEach((input, inputIndex) => {
const inputDesc = input.placeholder || input.getAttribute('wire:model') || `input-${inputIndex}`;
// Input event - most reliable for typing detection
input.addEventListener('input', function(e) {
startTyping();
});
// Keydown event - for immediate response
input.addEventListener('keydown', function(e) {
// Only log actual typing keys to reduce noise
if (e.key.length === 1 || e.key === 'Backspace' || e.key === 'Delete') {
startTyping();
} else if (e.key === 'Enter' && !e.shiftKey) {
stopTyping();
}
});
// Focus event - just for logging
input.addEventListener('focus', function() {
});
// Blur event - only stop if not typing recently
input.addEventListener('blur', function() {
// Add a small delay before stopping to prevent immediate stop
setTimeout(() => {
if (isTyping) {
stopTyping();
}
}, 100);
});
});
});
}
// Setup with delay to ensure Livewire is ready
setTimeout(setupWireChatTyping, 500);
// Re-setup on navigation
document.addEventListener('livewire:navigated', function() {
setTimeout(setupWireChatTyping, 500);
});
});
// Global test functions
window.testWireChatTyping = {
start: function() {
const indicators = document.querySelectorAll('.wirechat-typing-indicator');
indicators.forEach(indicator => {
const wireId = indicator.getAttribute('wire:id');
if (wireId) {
try {
const component = window.Livewire.find(wireId);
component.call('startTyping');
} catch (e) {
}
}
});
},
stop: function() {
const indicators = document.querySelectorAll('.wirechat-typing-indicator');
indicators.forEach(indicator => {
const wireId = indicator.getAttribute('wire:id');
if (wireId) {
try {
const component = window.Livewire.find(wireId);
component.call('stopTyping');
} catch (e) {
}
}
});
},
checkRedis: function() {
// This would need to be done server-side, but we can check component state
const indicators = document.querySelectorAll('.wirechat-typing-indicator');
indicators.forEach(indicator => {
const wireId = indicator.getAttribute('wire:id');
if (wireId) {
try {
const component = window.Livewire.find(wireId);
component.call('debug');
} catch (e) {
}
}
});
}
};
</script>
</div>

View File

@@ -0,0 +1,201 @@
<div>
@script
<script>
window.ChatDrawer = () => {
return {
show: false,
showActiveComponent: true,
activeDrawerComponent: false,
componentHistory: [],
listeners: [],
//current component attributes
closeOnEscape: false,
closeOnEscapeIsForceful: false,
dispatchCloseEvent: false,
destroyOnClose: false,
closeModalOnClickAway:false,
closeChatDrawerOnEscape(trigger) {
///Only proceed if the trigger is for ChatDrawer
if (trigger.modalType !== 'ChatDrawer') {
return;
}
//check if canCloseOnEsp
if (this.closeOnEscape === false) {
return;
}
//Fire closingModalOnEscape:event to parent
if (!this.closingModal('closingModalOnEscape')) {
return;
}
//check if should also close all children modal when this current on is closed
const force = this.closeOnEscapeIsForceful === true;
this.closeDrawer(force);
},
closingModal(eventName) {
const componentName = this.$wire.get('drawerComponents')[this.activeDrawerComponent].name;
var params = {
id: this.activeDrawerComponent,
closing: true,
};
Livewire.dispatchTo(componentName, eventName, params);
return params.closing;
},
closeDrawer(force = false, skipPreviousModals = 0, destroySkipped = false) {
if (this.show === false) {
return;
}
//Check if should dispatch events
if (this.dispatchCloseEvent === true) {
const componentName = this.$wire.get('drawerComponents')[this.activeDrawerComponent].name;
Livewire.dispatch('chatDrawerClosed', {
name: componentName
});
}
//Check if should completley destroy component on close
//Meaning state won't be retained if component is opened again
if (this.destroyOnClose === true) {
Livewire.dispatch('destroyChatDrawer', {
id: this.activeDrawerComponent
});
}
const id = this.componentHistory.pop();
if (id && !force) {
if (id) {
this.setActiveDrawerComponent(id, true);
} else {
this.setShowPropertyTo(false);
}
} else {
this.setShowPropertyTo(false);
}
},
setActiveDrawerComponent(id, skip = false) {
this.setShowPropertyTo(true);
if (this.activeDrawerComponent === id) {
return;
}
if (this.activeDrawerComponent !== false && skip === false) {
this.componentHistory.push(this.activeDrawerComponent);
}
let focusableTimeout = 50;
if (this.activeDrawerComponent === false) {
this.activeDrawerComponent = id
this.showActiveComponent = true;
} else {
this.showActiveComponent = false;
focusableTimeout = 400;
setTimeout(() => {
this.activeDrawerComponent = id;
this.showActiveComponent = true;
}, 300);
}
// Fetch modal attributes and set Alpine properties
const attributes = this.$wire.get('drawerComponents')[id]?.modalAttributes || {};
this.closeOnEscape = attributes.closeOnEscape ?? false;
this.closeOnEscapeIsForceful = attributes.closeOnEscapeIsForceful ?? false;
this.dispatchCloseEvent = attributes.dispatchCloseEvent ?? false;
this.destroyOnClose = attributes.destroyOnClose ?? true;
this.closeModalOnClickAway = attributes.closeModalOnClickAway ?? false;
this.$nextTick(() => {
let focusable = this.$refs[id]?.querySelector('[autofocus]');
if (focusable) {
setTimeout(() => {
focusable.focus();
}, focusableTimeout);
}
});
},
setShowPropertyTo(show) {
this.show = show;
if (show) {
document.body.classList.add('overflow-y-hidden');
} else {
document.body.classList.remove('overflow-y-hidden');
setTimeout(() => {
this.activeDrawerComponent = false;
this.$wire.resetState();
}, 300);
}
},
init() {
/*! Changed the event to closeChatDrawer in order to not interfere with the main modal */
this.listeners.push(Livewire.on('closeChatDrawer', (data) => { this.closeDrawer(data?.force ?? false, data?.skipPreviousModals ?? 0, data ?.destroySkipped ?? false); }));
/*! Changed listener name to activeChatDrawerComponentChanged to not interfer with main modal*/
this.listeners.push(Livewire.on('activeChatDrawerComponentChanged', ({id}) => {
this.setActiveDrawerComponent(id);
}));
},
destroy() {
this.listeners.forEach((listener) => {
listener();
});
}
};
}
</script>
@endscript
<div
data-modal-type="ChatDrawer"
id="chat-drawer"
x-data="ChatDrawer()" x-on:close.stop="setShowPropertyTo(false)"
x-on:keydown.escape.stop="closeChatDrawerOnEscape({ modalType: 'ChatDrawer', event: $event }); "
x-show="show"
class="fixed bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] dark:text-white opacity-100 inset-0 z-50 h-full overflow-y-auto" style="display: none;"
aria-modal="true"
tabindex="0"
>
<div class="justify-center text-center relative">
<div x-show="show && showActiveComponent" x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 -translate-x-full" x-transition:enter-end="opacity-100 translate-x-0"
x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100 translate-x-0"
x-transition:leave-end="opacity-0 -translate-x-full"
class="w-auto transition-all " id="chatmodal-container"
x-trap.noscroll="show && showActiveComponent" aria-modal="true">
@forelse($drawerComponents as $id => $component)
<div x-show.immediate="activeDrawerComponent == '{{ $id }}'" x-ref="{{ $id }}"
wire:key="{{ $id }}">
@livewire($component['name'], $component['arguments'], key($id))
</div>
@empty
@endforelse
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,156 @@
<div class="bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] dark:text-white border border-[var(--wc-light-secondary)] dark:border-[var(--wc-dark-secondary)] overflow-visible">
<header class=" sticky top-0 bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] z-10 p-12 pb-2">
<div class="flex items-center pb-2">
<x-wirechat::actions.close-modal>
<button
class="p-2 ml-0 text-gray-600 hover:bg-[var(--wc-light-secondary)] dark:hover:bg-[var(--wc-dark-secondary)] dark:hover:text-white rounded-full hover:text-gray-800">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class=" w-5 w-5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>
</button>
</x-wirechat::actions.close-modal>
<h3 class="text-sm mx-auto font-semibold " ><span>{{__('wirechat::chat.group.add_members.heading.label')}} </span> {{$newTotalCount}} / {{$maxGroupMembers}}</h3>
<x-jetstream.button
wire:click="save"
wire:loading.attr="disabled"
wire:target='save'
@disabled(count($selectedMembers)==0)
type="button"
class="text-xs py-1.5 px-3">
{{__('wirechat::chat.group.add_members.actions.save.label')}}
</x-jetstream.button>
</div>
{{-- Member limit error --}}
<div
x-data="{ showError:false }"
x-on:show-member-limit-error.window="
showError=true;
setTimeout(()=>{ showError=false; },1500);
"
class="text-red-500 text-sm mx-auto ">
<span x-transition x-show="showError">
{{__('wirechat::chat.group.add_members.messages.members_limit_error',['count'=>$maxGroupMembers])}}
</span>
</div>
<section class="flex flex-wrap items-center px-0 border-b border-[var(--wc-light-secondary)] dark:border-[var(--wc-dark-secondary)]">
<input type="search" id="users-search-field" wire:model.live.debounce='search' autocomplete="off"
placeholder="{{ __('wirechat::chat.group.add_members.inputs.search.placeholder') }}"
class=" w-full border-0 w-auto dark:bg-none dark:bg-transparent outline-hidden focus:outline-hidden bg-none rounded-lg focus:ring-0 hover:ring-0">
</section>
<section class=" overflow-x-hidden my-2 ">
<ul style="-ms-overflow-style: none;scrollbar-width: none;
"
class="flex w-full overflow-x-auto gap-3">
@if ($selectedMembers)
@foreach ($selectedMembers as $key => $member)
<li class="flex items-center text-nowrap min-w-fit px-2 py-1 text-sm font-medium text-gray-800 bg-[var(--wc-light-secondary)] rounded-sm dark:bg-[var(--wc-dark-secondary)] dark:text-gray-300"
wire:key="selected-member-{{ $member->id }}">
{{ $member->display_name }}
<button type="button"
wire:click="toggleMember('{{ $member->id }}',{{ json_encode(get_class($member)) }})"
class="flex items-center p-1 ms-2 text-sm text-gray-400 bg-transparent rounded-xs hover:bg-gray-200 hover:text-gray-900 dark:hover:bg-gray-600 dark:hover:text-gray-300"
aria-label="Remove">
<svg class="w-2 h-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Remove badge</span>
</button>
</li>
@endforeach
@endif
</ul>
</section>
</header>
<div class="relative w-full px-12">
{{-- <h5 class="text font-semibold text-gray-800 dark:text-gray-100">Recent Chats</h5> --}}
<section class="my-4">
@if ($users)
<ul class="overflow-auto flex flex-col">
@foreach ($users as $key => $user)
@php
$isAlreadyAParticipant= $user->belongsToConversation($conversation);
@endphp
<li wire:key="users-{{$key}}" class="flex cursor-pointer group gap-2 items-center p-2">
<label
{{-- The wire:click attribute is only rendered if $isAlreadyAParticipant is false. --}}
@if (!$isAlreadyAParticipant)
wire:click="toggleMember('{{ $user->id }}', {{ json_encode(get_class($user)) }})"
@endif
class="flex cursor-pointer gap-2 items-center w-full">
<x-wirechat::avatar src="{{$user->cover_url}}" class="w-10 h-10" />
<div @class(['opacity-70' => $isAlreadyAParticipant, 'flex-1 min-w-0']) >
<p
@class(['transition-all truncate', 'group-hover:underline ' => !$isAlreadyAParticipant])>
{{ $user->display_name }}</p>
<span
@class(['text-gray-600 dark:text-gray-400 text-sm'])>
@if ($isAlreadyAParticipant)
{{__('wirechat::chat.group.add_members.messages.member_already_exists')}}
@else
@php
$location = $user->getLocationFirst();
@endphp
@if ($location && isset($location['name_short']))
{{ $location['name_short'] }}
@endif
@endif
</span>
</div>
<div class="ml-auto">
@if ($selectedMembers->contains(fn($member) => $member->id == $user->id && get_class($member) == get_class($user)) || $isAlreadyAParticipant)
<div class="w-6 h-6 bg-theme-brand rounded flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" class="w-4 h-4 text-theme-background">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</div>
@else
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="currentColor" class="bi bi-plus-square-dotted w-6 h-6 text-gray-400"
viewBox="0 0 16 16">
<path
d="M2.5 0q-.25 0-.487.048l.194.98A1.5 1.5 0 0 1 2.5 1h.458V0zm2.292 0h-.917v1h.917zm1.833 0h-.917v1h.917zm1.833 0h-.916v1h.916zm1.834 0h-.917v1h.917zm1.833 0h-.917v1h.917zM13.5 0h-.458v1h.458q.151 0 .293.029l.194-.981A2.5 2.5 0 0 0 13.5 0m2.079 1.11a2.5 2.5 0 0 0-.69-.689l-.556.831q.248.167.415.415l.83-.556zM1.11.421a2.5 2.5 0 0 0-.689.69l.831.556c.11-.164.251-.305.415-.415zM16 2.5q0-.25-.048-.487l-.98.194q.027.141.028.293v.458h1zM.048 2.013A2.5 2.5 0 0 0 0 2.5v.458h1V2.5q0-.151.029-.293zM0 3.875v.917h1v-.917zm16 .917v-.917h-1v.917zM0 5.708v.917h1v-.917zm16 .917v-.917h-1v.917zM0 7.542v.916h1v-.916zm15 .916h1v-.916h-1zM0 9.375v.917h1v-.917zm16 .917v-.917h-1v.917zm-16 .916v.917h1v-.917zm16 .917v-.917h-1v.917zm-16 .917v.458q0 .25.048.487l.98-.194A1.5 1.5 0 0 1 1 13.5v-.458zm16 .458v-.458h-1v.458q0 .151-.029.293l.981.194Q16 13.75 16 13.5M.421 14.89c.183.272.417.506.69.689l.556-.831a1.5 1.5 0 0 1-.415-.415zm14.469.689c.272-.183.506-.417.689-.69l-.831-.556c-.11.164-.251.305-.415.415l.556.83zm-12.877.373Q2.25 16 2.5 16h.458v-1H2.5q-.151 0-.293-.029zM13.5 16q.25 0 .487-.048l-.194-.98A1.5 1.5 0 0 1 13.5 15h-.458v1zm-9.625 0h.917v-1h-.917zm1.833 0h.917v-1h-.917zm1.834-1v1h.916v-1zm1.833 1h.917v-1h-.917zm1.833 0h.917v-1h-.917zM8.5 4.5a.5.5 0 0 0-1 0v3h-3a.5.5 0 0 0 0 1h3v3a.5.5 0 0 0 1 0v-3h3a.5.5 0 0 0 0-1h-3z" />
</svg>
@endif
</div>
</label>
</li>
@endforeach
</ul>
@endif
</section>
</div>
</div>

View File

@@ -0,0 +1,304 @@
<div id="group-info-modal" class="bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] min-h-screen">
@php
$authIsAdminInGroup = $participant?->isAdmin();
$authIsOwner = $participant?->isOwner();
$isGroup = $conversation?->isGroup();
$group = $conversation?->group;
@endphp
<section class="cursor-pointer flex gap-4 z-10 items-center p-5 sticky top-0 bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] ">
<button wire:click="$dispatch('closeChatDrawer')" class="focus:outline-hidden cursor-pointer"> <svg class="w-7 h-7"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg> </button>
<h3>{{__('wirechat::chat.group.info.heading.label')}}</h3>
</section>
{{-- Details --}}
<header>
{{-- Edit group form --}}
@if ($authIsAdminInGroup || $group?->allowsMembersToEditGroupInfo())
<div @dusk="edit_group_information_section" class="flex flex-col items-center gap-5 py-5 px-4 ">
{{-- Avatar --}}
<section class="mx-auto items-center justify-center grid">
<div @dusk="edit_avatar_label" class="relative h-32 w-32 overflow-clip mx-auto rounded-full">
<label wire:target="photo" wire:loading.class="cursor-not-allowed" for="photo"
class=" cursor-pointer w-full h-full">
<x-wirechat::avatar wire:loading.class="cursor-not-allowed" group="{{ $isGroup }}"
:src="$cover_url" class="w-full h-full absolute inset-0" />
</label>
<input accept=".jpg,.jpeg,.png,.webp" wire:loading.attr="disabled" id="photo"
wire:model="photo" dusk="add_photo_field" type="file" hidden>
@if (empty($cover_url))
{{-- penceil --}}
<label wire:target="photo" wire:loading.class="cursor-not-allowed"
wire:loading.class.remove="cursor-pointer" for="photo"
class=" cursor-pointer bottom-0 inset-x-0 bg-gray-500/40 hover:bg-gray-500/80 dark:bg-white/40 dark:hover:bg-gray-700 transition-colors text-gray-600 flex items-center justify-center absolute ">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="size-6 w-5 h-5">
<path
d="M21.731 2.269a2.625 2.625 0 0 0-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 0 0 0-3.712ZM19.513 8.199l-3.712-3.712-12.15 12.15a5.25 5.25 0 0 0-1.32 2.214l-.8 2.685a.75.75 0 0 0 .933.933l2.685-.8a5.25 5.25 0 0 0 2.214-1.32L19.513 8.2Z" />
</svg>
</label>
@else
<button type="button" wire:target="photo" wire:loading.attr="disabled"
class="disabled:cursor-not-allowed bottom-0 inset-x-0 bg-gray-500/40 hover:bg-gray-500/80 m-0 p-0 border-0 dark:bg-white/40 dark:hover:bg-gray-700 transition-colors text-red-800 flex items-center justify-center absolute "
wire:confirm="Are you sure you want to delete photo ?" wire:click="deletePhoto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="size-6 w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</button>
@endif
</div>
@error('photo')
<span class="text-red-500">{{ $message }}</span>
@enderror
</section>
{{-- Form --}}
<div class="space-y-3 grid overflow-x-hidden">
{{-- Form to update Group name --}}
<form @dusk="edit_group_name_form" wire:submit="updateGroupName" x-data="{ editing: false }"
class=" justify-center flex items-center w-full gap-5 px-5 items-center">
@csrf
{{-- Left side input --}}
<div class=" max-w-[90%] grid h-auto">
<div x-show="!editing">
<h4 dusk="form_group_name_when_not_editing" class="font-medium break-all whitespace-pre-line text-2xl ">{{ $groupName }} </h4>
</div>
<input x-cloak maxlength="110" x-show="editing" id='groupName' type="text"
wire:model='groupName'
class="resize-none text-2xl font-medium border-0 px-0 py-0 py-0 border-b border-[var(--wc-light-border)] dark:border-[var(--wc-dark-border)] bg-inherit dark:text-white outline-hidden w-full focus:outline-hidden focus:ring-0 hover:ring-0">
@error('groupName')
<p class="text-red-500 inline">{{ $message }}</p>
@enderror
</div>
{{-- Right Side --}}
<span class=" items-center">
<button type="button" @click="editing=true" x-show="!editing">
{{-- pencil/edit --}}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="size-6 w-5 h-5">
<path
d="M21.731 2.269a2.625 2.625 0 0 0-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 0 0 0-3.712ZM19.513 8.199l-3.712-3.712-12.15 12.15a5.25 5.25 0 0 0-1.32 2.214l-.8 2.685a.75.75 0 0 0 .933.933l2.685-.8a5.25 5.25 0 0 0 2.214-1.32L19.513 8.2Z" />
</svg>
</button>
<button x-cloak @click="editing=false" x-show="editing">
{{-- check/submit --}}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="currentColor" class="bi bi-check-lg w-5 h-5" viewBox="0 0 16 16">
<path
d="M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425z" />
</svg>
</button>
</span>
</form>
{{-- Members count --}}
<p class="mx-auto"> {{ __('wirechat::chat.group.info.labels.members') }} {{ $totalParticipants }} </p>
</div>
{{-- About --}}
<section class=" px-8 py-5 ">
<div @dusk="edit_description_section" x-data="{ editing: false }" @click.outside="editing=false"
class="grid grid-cols-12 items-center">
{{-- Left side input --}}
<span class="col-span-11">
<div x-show="!editing">
@if (empty($description))
<p class="text-sm" style="color: var(--wirechat-primary-color)">{{ __('wirechat::chat.group.info.labels.add_description') }} </p>
@else
<p class="font-medium break-all whitespace-pre-line ">{{ $description }}
</p>
@endif
</div>
<textarea x-cloak maxlength="501" x-show="editing" id='description' type="text" wire:model.blur='description'
class="resize-none font-medium w-full border-0 px-0 py-0 py-0 border-b border-[var(--wc-light-border)] dark:border-[var(--wc-dark-border)] bg-inherit dark:text-white outline-hidden w-full focus:outline-hidden focus:ring-0 hover:ring-0">
</textarea>
@error('description')
<p class="text-red-500">{{ $message }}</p>
@enderror
</span>
{{-- Right Side --}}
<span class="col-span-1 flex items-center justify-end">
<button @click="editing=true" x-show="!editing">
{{-- pencil/edit --}}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="size-6 w-5 h-5">
<path
d="M21.731 2.269a2.625 2.625 0 0 0-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 0 0 0-3.712ZM19.513 8.199l-3.712-3.712-12.15 12.15a5.25 5.25 0 0 0-1.32 2.214l-.8 2.685a.75.75 0 0 0 .933.933l2.685-.8a5.25 5.25 0 0 0 2.214-1.32L19.513 8.2Z" />
</svg>
</button>
<button x-cloak @click="editing=false" x-show="editing">
{{-- check --}}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="currentColor" class="bi bi-check-lg w-5 h-5" viewBox="0 0 16 16">
<path
d="M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425z" />
</svg>
</button>
</span>
</div>
</section>
</div>
@else
{{-- Plain group information --}}
<div @dusk="non_editable_group_information_section" class="flex flex-col items-center gap-5 py-5 px-4 ">
<x-wirechat::avatar :src="$cover_url" class=" h-32 w-32 mx-auto" />
<h4 dusk="group_name" class="font-medium break-all whitespace-pre-line text-2xl ">{{ $groupName }} </h4>
<p class="mx-auto">{{ __('wirechat::chat.group.info.labels.members') }} {{ $totalParticipants }} </p>
<p class="font-medium break-all whitespace-pre-line ">{{ $description }} </p>
</div>
@endif
</header>
<x-wirechat::divider />
{{-- Disappearing Messages Settings --}}
@if(timebank_config('wirechat.disappearing_messages.enabled', true))
<section class="px-8 py-5">
@livewire('wire-chat.disappearing-messages-settings', ['conversationId' => $conversation->id], key('disappearing-'.$conversation->id))
</section>
<x-wirechat::divider />
@endif
{{-- Members section --}}
<section class="my-4 text-left space-y-3">
{{-- Actiion button to trigger opening members modal --}}
<x-wirechat::actions.open-modal component="wirechat.chat.group.members"
conversation="{{ $conversation?->id }}" widget="{{ $this->isWidget() }}">
{{-- Members count --}}
<button class="cursor-pointer flex w-full justify-between items-center px-8 focus:outline-hidden ">
<span class="text-gray-600 dark:text-gray-300">{{ __('wirechat::chat.group.info.labels.members') }} {{ $totalParticipants }}</span>
{{-- Search icon --}}
<span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-6 w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round"
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg>
</span>
</button>
</x-wirechat::actions.open-modal>
{{-- Add Members --}}
@if ($authIsAdminInGroup || $group?->allowsMembersToAddOthers())
<x-wirechat::actions.open-modal component="wirechat.chat.group.add-members"
conversation="{{ $conversation?->id }}" widget="{{ $this->isWidget() }}">
<button @dusk="open_add_members_modal_button"
class="cursor-pointer w-full py-5 px-8 hover:bg-[var(--wc-light-secondary)] dark:hover:bg-[var(--wc-dark-secondary)] focus:outline-hidden transition flex gap-3 items-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="size-6 w-5 h-5">
<path
d="M5.25 6.375a4.125 4.125 0 1 1 8.25 0 4.125 4.125 0 0 1-8.25 0ZM2.25 19.125a7.125 7.125 0 0 1 14.25 0v.003l-.001.119a.75.75 0 0 1-.363.63 13.067 13.067 0 0 1-6.761 1.873c-2.472 0-4.786-.684-6.76-1.873a.75.75 0 0 1-.364-.63l-.001-.122ZM18.75 7.5a.75.75 0 0 0-1.5 0v2.25H15a.75.75 0 0 0 0 1.5h2.25v2.25a.75.75 0 0 0 1.5 0v-2.25H21a.75.75 0 0 0 0-1.5h-2.25V7.5Z" />
</svg>
<span>{{ __('wirechat::chat.group.info.actions.add_members.label') }}</span>
</button>
</x-wirechat::actions.open-modal>
@endif
</section>
<x-wirechat::divider />
{{-- Footer section --}}
<footer class="flex flex-col justify-start w-full">
@if ($authIsOwner)
{{-- Delete group --}}
<button wire:confirm="{{ __('wirechat::chat.group.info.actions.delete_group.confirmation_message') }}" wire:click="deleteGroup"
class="cursor-pointer w-full py-5 px-8 hover:bg-[var(--wc-light-secondary)] dark:hover:bg-[var(--wc-dark-secondary)] transition text-start space-y-2 gap-3 text-red-500">
<div class="flex gap-3 items-center ">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-6 w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
<span>{{ __('wirechat::chat.group.info.actions.delete_group.label') }}</span>
</div>
<p class="dark:text-white/60 text-sm text-gray-600/80">@lang('wirechat::chat.group.info.actions.delete_group.helper_text')</p>
</button>
{{-- Permissions --}}
<div>
<x-wirechat::actions.open-chat-drawer component='wirechat.chat.group.permissions'
conversation="{{ $conversation?->id }}">
<button
class="cursor-pointer w-full py-5 px-8 hover:bg-[var(--wc-light-secondary)] dark:hover:bg-[var(--wc-dark-secondary)] transition text-start space-y-2 gap-3 dark:text-white/90">
<div class="flex gap-3 items-center ">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="size-6 w-5 h-5 dark:text-gray-400">
<path stroke-linecap="round" stroke-linejoin="round"
d="M4.5 12a7.5 7.5 0 0 0 15 0m-15 0a7.5 7.5 0 1 1 15 0m-15 0H3m16.5 0H21m-1.5 0H12m-8.457 3.077 1.41-.513m14.095-5.13 1.41-.513M5.106 17.785l1.15-.964m11.49-9.642 1.149-.964M7.501 19.795l.75-1.3m7.5-12.99.75-1.3m-6.063 16.658.26-1.477m2.605-14.772.26-1.477m0 17.726-.26-1.477M10.698 4.614l-.26-1.477M16.5 19.794l-.75-1.299M7.5 4.205 12 12m6.894 5.785-1.149-.964M6.256 7.178l-1.15-.964m15.352 8.864-1.41-.513M4.954 9.435l-1.41-.514M12.002 12l-3.75 6.495" />
</svg>
<span>@lang('wirechat::chat.group.info.actions.group_permissions.label')</span>
</div>
</button>
</x-wirechat::actions.open-chat-drawer>
</div>
@else
{{-- Exit Group --}}
<button wire:confirm="{{ __('wirechat::chat.group.info.actions.exit_group.confirmation_message') }}" wire:click="exitConversation"
class="cursor-pointer w-full py-5 px-8 hover:bg-[var(--wc-light-secondary)] dark:hover:bg-[var(--wc-dark-secondary)] transition flex gap-3 items-center text-red-500">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-box-arrow-right w-5 h-5" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0z" />
<path fill-rule="evenodd"
d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708z" />
</svg>
<span>@lang('wirechat::chat.group.info.actions.exit_group.label')</span>
</button>
@endif
</footer>
</div>

View File

@@ -0,0 +1,162 @@
@php
$authIsAdminInGroup= $participant?->isAdmin();
$authIsOwner= $participant?->isOwner();
$isGroup= $conversation?->isGroup();
@endphp
<div x-ref="members"
class="h-[calc(100vh_-_6rem)] sm:h-[450px] bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] dark:text-white border border-[var(--wc-light-secondary)] dark:border-[var(--wc-dark-secondary)] overflow-y-auto overflow-x-hidden ">
<header class=" sticky top-0 bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] z-10 p-2">
<div class="flex items-center justify-center pb-2">
<x-wirechat::actions.close-modal>
<button dusk="close_modal_button"
class="p-2 ml-0 text-gray-600 hover:bg-[var(--wc-light-secondary)] dark:hover:bg-[var(--wc-dark-secondary)] dark:hover:text-white rounded-full hover:text-gray-800 ">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class=" w-5 w-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>
</button>
</x-wirechat::actions.close-modal>
<h3 class=" mx-auto font-semibold ">{{__('wirechat::chat.group.members.heading.label')}} </h3>
</div>
{{-- Member limit error --}}
<section class="flex flex-wrap items-center px-0 border-b dark:border-[var(--wc-dark-secondary)]">
<input type="search" id="users-search-field" wire:model.live.debounce='search' autocomplete="off"
placeholder="{{__('wirechat::chat.group.members.inputs.search.placeholder')}}"
class=" w-full border-0 w-auto dark:bg-[var(--wc-dark-primary)] outline-hidden focus:outline-hidden bg-[var(--wc-dark-parimary)] rounded-lg focus:ring-0 hover:ring-0">
</section>
</header>
<div class="relative w-full p-2 ">
{{-- <h5 class="text font-semibold text-gray-800 dark:text-gray-100">Recent Chats</h5> --}}
<section class="my-4 grid">
@if (count($participants)!=0)
<ul class="overflow-auto flex flex-col">
@foreach ($participants as $key => $participant)
@php
$loopParticipantIsAuth =
$participant->participantable_id == auth()->id() &&
$participant->participantable_type == auth()->user()->getMorphClass();
@endphp
<li x-data="{ open: false }" x-ref="button" @click="open = ! open" x-init="$watch('open', value => {
$refs.members.style.overflow = value ? 'hidden' : '';
})"
aria-modal="true"
tabindex="0"
x-on:keydown.escape.stop="open=false"
@click.away ="open=false;" wire:key="users-{{ $key }}"
:class="!open || 'bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)]'"
class="flex cursor-pointer group gap-2 items-center overflow-x-hidden p-2 py-3">
<label class="flex cursor-pointer gap-2 items-center w-full">
<x-wirechat::avatar src="{{ $participant->participantable->cover_url }}"
class="w-10 h-10" />
<div class="grid grid-cols-12 w-full ">
<h6 @class(['transition-all truncate group-hover:underline col-span-10' ])>
{{ $loopParticipantIsAuth ? 'You' : $participant->participantable->display_name }}</h6>
@if ($participant->isOwner()|| $participant->isAdmin())
<span style="background-color: var(--wirechat-primary-color);" class=" flex items-center col-span-2 text-white text-xs font-medium ml-auto px-2.5 py-px rounded-sm ">
{{$participant->isOwner()? __('wirechat::chat.group.members.labels.owner'): __('wirechat::chat.group.members.labels.admin')}}
</span>
@endif
</div>
<div x-show="open" x-anchor.bottom-end="$refs.button"
class="ml-auto bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)] border-[var(--wc-light-primary) dark:border-[var(--wc-dark-primary)] py-4 shadow-sm border rounded-md grid space-y-2 w-52">
{{-- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 text-gray-600 dark:text-gray-300 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg> --}}
<x-wirechat::dropdown-button wire:click="sendMessage('{{ $participant->id }}')"
class="truncate ">
@if ($loopParticipantIsAuth)
{{__('wirechat::chat.group.members.actions.send_message_to_yourself.label')}}
@else
{{__('wirechat::chat.group.members.actions.send_message_to_member.label',['member'=>$participant->participantable?->display_name ])}}
@endif
</x-wirechat::dropdown-button>
@if ($authIsAdminInGroup || $authIsOwner)
{{-- Only show admin actions to owner of group and if is not the current loop --}}
{{--AND We only want to show admin actions if participant is not owner --}}
@if ($authIsOwner && !$loopParticipantIsAuth)
@if ($participant->isAdmin())
<x-wirechat::dropdown-button
wire:click="dismissAdmin('{{ $participant->id }}')"
wire:confirm="{{__('wirechat::chat.group.members.actions.dismiss_admin.confirmation_message',['member'=>$participant->participantable?->display_name])}}"
class=" ">
{{__('wirechat::chat.group.members.actions.dismiss_admin.label')}}
</x-wirechat::dropdown-button>
@else
<x-wirechat::dropdown-button
wire:click="makeAdmin('{{ $participant->id }}')"
wire:confirm="{{__('wirechat::chat.group.members.actions.make_admin.confirmation_message',['member'=>$participant->participantable?->display_name])}}"
class=" ">
{{__('wirechat::chat.group.members.actions.make_admin.label')}}
</x-wirechat::dropdown-button>
@endif
@endif
{{--AND We only want to show remove actions if participant is not owner of conversation because we don't want to remove owner--}}
@if (!$participant->isOwner() && !$loopParticipantIsAuth && !$participant->isAdmin())
<x-wirechat::dropdown-button
wire:click="removeFromGroup('{{ $participant->id }}')"
wire:confirm="{{__('wirechat::chat.group.members.actions.remove_from_group.confirmation_message',['member'=>$participant->participantable?->display_name])}}"
class="text-red-500 ">
{{__('wirechat::chat.group.members.actions.remove_from_group.label')}}
</x-wirechat::dropdown-button>
@endif
@else
@endif
</div>
</label>
</li>
@endforeach
</ul>
{{-- Load more button --}}
@if ($canLoadMore)
<section class="w-full justify-center flex my-3">
<button dusk="loadMoreButton" @click="$wire.loadMore()"
class=" text-sm dark:text-white hover:text-gray-700 transition-colors dark:hover:text-gray-500 dark:gray-200">
{{__('wirechat::chat.group.members.actions.load_more.label')}}
</button>
</section>
@endif
@else
<span class="m-auto">{{__('wirechat::chat.group.members.labels.no_members_found')}}</span>
@endif
</section>
</div>
</div>

View File

@@ -0,0 +1,148 @@
<div class="bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] overfo opaticy-100 min-h-screen w-full">
<section class="flex gap-4 z-10 items-center p-5 sticky top-0 bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] ">
<button wire:click="$dispatch('closeChatDrawer')" class="focus:outline-hidden"> <svg class="w-7 h-7"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg> </button>
<h3>{{__('wirechat::chat.group.permisssions.heading.label')}} </h3>
</section>
<div class="">
<section >
<h5 class="w-full text-start py-4 bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)] px-4">
{{__('wirechat::chat.group.permisssions.labels.members_can')}}:
</h5>
<ul class="space-y-2">
{{-- Edit Group Settings --}}
<li class="w-full flex p-5">
<span class="w-12">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-6 w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
</svg>
</span>
<span class="w-full text-start">
<h5 class="font-medium">{{__('wirechat::chat.group.permisssions.actions.edit_group_information.label')}}</h5>
<p>@lang('wirechat::chat.group.permisssions.actions.edit_group_information.helper_text')</p>
</span>
<span class="w-12">
<label class="inline-flex items-center cursor-pointer">
<input wire:model.live.debounce="allow_members_to_edit_group_info" type="checkbox" class="sr-only peer">
<div class="relative w-11 h-6 peer-focus:outline-hidden rounded-full peer
bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)]
shadow-2xs
peer-checked:border-[var(--wc-brand-primary)]
peer-checked:after:translate-x-full peer-checked:rtl:after:-translate-x-full
peer-checked:dark:border-[var(--wc-dark-primary)] peer-checked:border-[var(--wc-light-primary)]
after:content-[''] after:absolute after:top-[2px]
after:start-[2px] after:bg-white dark:after:bg-gray-100 after:shadow
peer-checked:after:bg-[var(--wc-brand-primary)] peer-checked:dark:after:bg-[var(--wc-brand-primary)]
peer-checked:after:border-[var(--wc-brand-primary)] peer-checked:dark:after:border-[var(--wc-brand-primary)]
dark:after:border-[var(--wc-dark-primary)] after:border-[var(--wc-light-primary)] after:border
after:rounded-full after:h-5 after:w-5 after:transition-all ease-in-out">
</div>
</label>
</span>
</li>
{{-- Send Messages --}}
<li class="w-full flex items-center p-5">
<span class="w-12">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.6" stroke="currentColor" class="size-6 w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.625 9.75a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375m-13.5 3.01c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.184-4.183a1.14 1.14 0 0 1 .778-.332 48.294 48.294 0 0 0 5.83-.498c1.585-.233 2.708-1.626 2.708-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z" />
</svg>
</span>
<span class="w-full text-start">
<h5 class="font-medium">@lang('wirechat::chat.group.permisssions.actions.send_messages.label')</h5>
</span>
<span class="w-12">
<label class="inline-flex items-center cursor-pointer">
<input wire:model.live.debounce="allow_members_to_send_messages" type="checkbox" class="sr-only peer">
<div class="relative w-11 h-6 peer-focus:outline-hidden rounded-full peer
bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)]
shadow-2xs
peer-checked:border-[var(--wc-brand-primary)]
peer-checked:after:translate-x-full peer-checked:rtl:after:-translate-x-full
peer-checked:dark:border-[var(--wc-dark-primary)] peer-checked:border-[var(--wc-light-primary)]
after:content-[''] after:absolute after:top-[2px]
after:start-[2px] after:bg-white dark:after:bg-gray-100 after:shadow
peer-checked:after:bg-[var(--wc-brand-primary)] peer-checked:dark:after:bg-[var(--wc-brand-primary)]
peer-checked:after:border-[var(--wc-brand-primary)] peer-checked:dark:after:border-[var(--wc-brand-primary)]
dark:after:border-[var(--wc-dark-primary)] after:border-[var(--wc-light-primary)] after:border
after:rounded-full after:h-5 after:w-5 after:transition-all ease-in-out">
</div>
</label>
</span>
</li>
{{-- Add other members --}}
<li class="w-full flex items-center p-5">
<span class="w-12">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6 w-6 h-6 text-gray-500 dark:text-white/90">
<path d="M5.25 6.375a4.125 4.125 0 1 1 8.25 0 4.125 4.125 0 0 1-8.25 0ZM2.25 19.125a7.125 7.125 0 0 1 14.25 0v.003l-.001.119a.75.75 0 0 1-.363.63 13.067 13.067 0 0 1-6.761 1.873c-2.472 0-4.786-.684-6.76-1.873a.75.75 0 0 1-.364-.63l-.001-.122ZM18.75 7.5a.75.75 0 0 0-1.5 0v2.25H15a.75.75 0 0 0 0 1.5h2.25v2.25a.75.75 0 0 0 1.5 0v-2.25H21a.75.75 0 0 0 0-1.5h-2.25V7.5Z" />
</svg>
</span>
<span class="w-full text-start">
<h5 class="font-medium">@lang('wirechat::chat.group.permisssions.actions.add_other_members.label')</h5>
</span>
<span class="w-12">
<label class="inline-flex items-center cursor-pointer">
<input wire:model.live.debounce="allow_members_to_add_others" type="checkbox" class="sr-only peer">
<div class="relative w-11 h-6 peer-focus:outline-hidden rounded-full peer
bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)]
shadow-2xs
peer-checked:border-[var(--wc-brand-primary)]
peer-checked:after:translate-x-full peer-checked:rtl:after:-translate-x-full
peer-checked:dark:border-[var(--wc-dark-primary)] peer-checked:border-[var(--wc-light-primary)]
after:content-[''] after:absolute after:top-[2px]
after:start-[2px] after:bg-white dark:after:bg-gray-100 after:shadow
peer-checked:after:bg-[var(--wc-brand-primary)] peer-checked:dark:after:bg-[var(--wc-brand-primary)]
peer-checked:after:border-[var(--wc-brand-primary)] peer-checked:dark:after:border-[var(--wc-brand-primary)]
dark:after:border-[var(--wc-dark-primary)] after:border-[var(--wc-light-primary)] after:border
after:rounded-full after:h-5 after:w-5 after:transition-all ease-in-out">
</div>
</label>
</span>
</li>
</ul>
</section>
</div>
</div>

View File

@@ -0,0 +1,64 @@
<div id="info-modal" class="bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] min-h-screen">
<section class="flex gap-4 z-10 items-center p-5 sticky top-0 bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] ">
<button wire:click="$dispatch('closeChatDrawer')" class="focus:outline-hidden cursor-pointer"> <svg class="w-7 h-7"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg> </button>
<h3>{{ __('wirechat::chat.info.heading.label') }}</h3>
</section>
{{-- Details --}}
<header>
<div class="flex flex-col items-center gap-5 ">
<div class="mx-auto items-center justify-center grid">
<a href="{{ $receiver?->profile_url }}">
<x-wirechat::avatar :src="$cover_url" class=" h-32 w-32 mx-auto" />
</a>
</div>
<div class=" grid ">
<a class="px-8 py-5 " @dusk="receiver_name" href="{{ $receiver?->profile_url }}">
<h5 class="text-2xl">{{ $receiver?->display_name }}</h5>
</a>
</div>
</div>
</header>
<x-wirechat::divider />
{{-- Disappearing Messages Settings --}}
@if(timebank_config('wirechat.disappearing_messages.enabled', true))
<section class="px-8 py-5">
@livewire('wire-chat.disappearing-messages-settings', ['conversationId' => $conversation->id], key('disappearing-'.$conversation->id))
</section>
<x-wirechat::divider />
@endif
{{-- Footer section --}}
<section class="flex flex-col justify-start w-full">
{{-- Only show if is not group --}}
<button wire:confirm="{{ __('wirechat::chat.info.actions.delete_chat.confirmation_message') }}" wire:click="deleteChat"
class=" w-full cursor-pointer py-5 px-8 hover:bg-[var(--wc-light-secondary)] dark:hover:bg-[var(--wc-dark-secondary)] transition flex gap-3 items-center text-red-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-6 w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
<span>{{ __('wirechat::chat.info.actions.delete_chat.label') }}</span>
</button>
</section>
</div>

View File

@@ -0,0 +1,364 @@
<main x-data="{
height: 0,
previousHeight: 0,
updateScrollPosition: function() {
// Calculate the difference in height
newHeight = $el.scrollHeight;
{{-- console.log('old height' + height);
console.log('new height' + document.getElementById('conversation').scrollHeight); --}}
heightDifference = newHeight - height;
{{-- console.log('conversationElement.scrollTop ' + conversationElement.scrollTop);
console.log('heightDifference' + heightDifference); --}}
$el.scrollTop += heightDifference;
// Update the previous height to the new height
height = newHeight;
}
}"
x-init="
setTimeout(() => {
requestAnimationFrame(() => {
this.height = $el.scrollHeight;
$el.scrollTop = this.height;
});
}, 300); //! Add delay so height can be update at right time
"
@scroll ="
scrollTop= $el.scrollTop;
if((scrollTop<=0) && $wire.canLoadMore){
$wire.loadMore();
}
"
@update-height.window="
requestAnimationFrame(() => {
updateScrollPosition();
});
"
@scroll-bottom.window="
requestAnimationFrame(() => {
{{-- overflow-y: hidden; is used to hide the vertical scrollbar initially. --}}
$el.style.overflowY='hidden';
{{-- scroll the element down --}}
$el.scrollTop = $el.scrollHeight;
{{-- After updating the chat height, overflowY is set back to 'auto',
which allows the browser to determine whether to display the scrollbar
based on the content height. --}}
$el.style.overflowY='auto';
});
"
x-cloak
class='flex flex-col h-full relative gap-2 gap-y-4 p-4 md:p-5 lg:p-8 grow overscroll-contain overflow-x-hidden w-full my-auto'
style="contain: content" >
<div x-cloak wire:loading.delay.class.remove="invisible" wire:target="loadMore" class="invisible transition-all duration-300 ">
<x-wirechat::loading-spin />
</div>
{{-- Define previous message outside the loop --}}
@php
$previousMessage = null;
@endphp
<!--Message-->
@if ($loadedMessages)
{{-- @dd($loadedMessages) --}}
@foreach ($loadedMessages as $date => $messageGroup)
{{-- Date --}}
<div class="sticky top-0 uppercase p-2 shadow-xs px-2.5 z-50 rounded-xl border dark:border-[var(--wc-dark-primary)] border-[var(--wc-light-primary)] text-sm flex text-center justify-center bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)] dark:text-white w-28 mx-auto ">
{{ $date }}
</div>
@foreach ($messageGroup as $key => $message)
{{-- @dd($message) --}}
@php
$belongsToAuth = $message->belongsToAuth();
$parent = $message->parent ?? null;
$attachment = $message->attachment ?? null;
$isEmoji = $message->isEmoji();
// keep track of previous message
// The ($key -1 ) will get the previous message from loaded
// messages since $key is directly linked to $message
if ($key > 0) {
$previousMessage = $messageGroup->get($key - 1);
}
// Get the next message
$nextMessage = $key < $messageGroup->count() - 1 ? $messageGroup->get($key + 1) : null;
@endphp
<div class="flex gap-2" wire:key="message-{{ $key }}" >
{{-- Message user Avatar --}}
{{-- Hide avatar if message belongs to auth --}}
@if (!$belongsToAuth && !$isPrivate)
<div @class([
'shrink-0 mb-auto -mb-2',
// Hide avatar if the next message is from the same user
'invisible' =>
$previousMessage &&
$message?->sendable?->is($previousMessage?->sendable),
])>
<x-wirechat::avatar src="{{ $message->sendable?->cover_url ?? null }}" class="h-8 w-8" />
</div>
@endif
{{-- we use w-[95%] to leave space for the image --}}
<div class="w-[95%] mx-auto">
<div @class([
'max-w-[85%] md:max-w-[78%] flex flex-col gap-y-2 ',
'ml-auto' => $belongsToAuth])>
{{-- Show parent/reply message --}}
@if ($parent != null)
<div @class([
'max-w-fit flex flex-col gap-y-2',
'ml-auto' => $belongsToAuth,
// 'ml-9 sm:ml-10' => !$belongsToAuth,
])>
@php
$sender = $message?->ownedBy($this->auth)
? __('wirechat::chat.labels.you')
: ($message->sendable?->display_name ?? __('wirechat::chat.labels.user'));
$receiver = $parent?->ownedBy($this->auth)
? __('wirechat::chat.labels.you')
: ($parent->sendable?->display_name ?? __('wirechat::chat.labels.user'));
@endphp
<h6 class="text-xs text-gray-500 dark:text-gray-300 px-2">
@if ($parent?->ownedBy($this->auth) && $message?->ownedBy($this->auth))
{{ __('wirechat::chat.labels.you_replied_to_yourself') }}
@elseif ($parent?->ownedBy($this->auth))
{{ __('wirechat::chat.labels.participant_replied_to_you', ['sender' => $sender]) }}
@elseif ($message?->ownedBy($parent->sendable))
{{ __('wirechat::chat.labels.participant_replied_to_themself', ['sender' => $sender]) }}
@else
{{ __('wirechat::chat.labels.participant_replied_other_participant', ['sender' => $sender, 'receiver' => $receiver]) }}
@endif
</h6>
<div @class([
'px-1 border-[var(--wc-light-secondary)] dark:border-[var(--wc-dark-accent)] overflow-hidden ',
' border-r-4 ml-auto' => $belongsToAuth,
' border-l-4 mr-auto ' => !$belongsToAuth,
])>
<p
class=" bg-[var(--wc-light-secondary)] dark:text-white dark:bg-[var(--wc-dark-secondary)] text-black line-clamp-1 text-sm rounded-full max-w-fit px-3 py-1 ">
{{ $parent?->body != '' ? $parent?->body : ($parent->hasAttachment() ? __('wirechat::chat.labels.attachment') : '') }}
</p>
</div>
</div>
@endif
{{-- Body section --}}
<div @class([
'flex gap-1 md:gap-4 group transition-transform ',
'justify-end' => $belongsToAuth,
])>
{{-- Message Actions --}}
@if (($isGroup && $conversation->group?->allowsMembersToSendMessages()) || $authParticipant->isAdmin())
<div dusk="message_actions" @class([ 'my-auto flex w-auto items-center gap-2', 'order-1' => $belongsToAuth, 'order-3' => !$belongsToAuth, ])>
{{-- reply button --}}
<button wire:click="setReply('{{ encrypt($message->id) }}')"
class=" invisible group-hover:visible hover:scale-110 transition-transform">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="currentColor" class="bi bi-reply-fill w-4 h-4 dark:text-white"
viewBox="0 0 16 16">
<path
d="M5.921 11.9 1.353 8.62a.72.72 0 0 1 0-1.238L5.921 4.1A.716.716 0 0 1 7 4.719V6c1.5 0 6 0 7 8-2.5-4.5-7-4-7-4v1.281c0 .56-.606.898-1.079.62z" />
</svg>
</button>
{{-- Dropdown actions button --}}
<x-wirechat::dropdown class="w-40" align="{{ $belongsToAuth ? 'right' : 'left' }}"
width="48">
<x-slot name="trigger">
{{-- Dots --}}
<button class="invisible group-hover:visible hover:scale-110 transition-transform">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="currentColor"
class="bi bi-three-dots h-3 w-3 text-gray-700 dark:text-white"
viewBox="0 0 16 16">
<path
d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3m5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3m5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3" />
</svg>
</button>
</x-slot>
<x-slot name="content">
{{-- Keep message (if disappearing messages enabled) --}}
@if (timebank_config('wirechat.disappearing_messages.enabled', true) &&
timebank_config('wirechat.disappearing_messages.allow_users_to_keep', true))
<button dusk="keep_message_button" wire:click="keepMessage('{{ encrypt($message->id) }}')" class="w-full text-start">
<x-wirechat::dropdown-link>
@if ($message->kept_at)
<span class="flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3l18 18"/>
</svg>
{{ __('Unkeep message') }}
</span>
@else
<span class="flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"/>
</svg>
{{ __('Keep message') }}
</span>
@endif
</x-wirechat::dropdown-link>
</button>
@endif
@if ($message->ownedBy($this->auth)|| ($authParticipant->isAdmin() && $isGroup))
<button dusk="delete_message_for_everyone" wire:click="deleteForEveryone('{{ encrypt($message->id) }}')"
wire:confirm="{{ __('wirechat::chat.actions.delete_for_everyone.confirmation_message') }}" class="w-full text-start">
<x-wirechat::dropdown-link>
@lang('wirechat::chat.actions.delete_for_everyone.label')
</x-wirechat::dropdown-link>
</button>
@endif
{{-- Dont show delete for me if is group --}}
@if (!$isGroup)
<button dusk="delete_message_for_me" wire:click="deleteForMe('{{ encrypt($message->id) }}')"
wire:confirm="{{ __('wirechat::chat.actions.delete_for_me.confirmation_message') }}" class="w-full text-start">
<x-wirechat::dropdown-link>
@lang('wirechat::chat.actions.delete_for_me.label')
</x-wirechat::dropdown-link>
</button>
@endif
<button dusk="reply_to_message_button" wire:click="setReply('{{ encrypt($message->id) }}')"class="w-full text-start">
<x-wirechat::dropdown-link>
@lang('wirechat::chat.actions.reply.label')
</x-wirechat::dropdown-link>
</button>
</x-slot>
</x-wirechat::dropdown>
</div>
@endif
{{-- Kept Message Indicator (Bookmark) --}}
@if (timebank_config('wirechat.disappearing_messages.enabled', true) &&
timebank_config('wirechat.disappearing_messages.allow_users_to_keep', true) &&
$message->kept_at)
<div class="flex items-start pt-0.5 order-2">
<svg class="w-4 h-4 text-gray-700 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24" title="{{ __('Message kept - will not auto-delete') }}">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"/>
</svg>
</div>
@endif
{{-- Message body --}}
<div @class([
'flex flex-col gap-2 max-w-[95%] relative',
'order-3' => $belongsToAuth,
'order-1' => !$belongsToAuth,
])>
{{-- Show sender name is message does not belong to auth and conversation is group --}}
{{-- -------------------- --}}
{{-- Attachment section --}}
{{-- -------------------- --}}
@if ($attachment)
@if (!$belongsToAuth && $isGroup)
<div style="color: var(--wc-brand-primary);" @class([
'shrink-0 font-medium text-sm sm:text-base',
// Hide avatar if the next message is from the same user
'hidden' => $message?->sendable?->is($previousMessage?->sendable),
])>
{{ $message->sendable?->display_name }}
</div>
@endif
{{-- Attachemnt is Application/ --}}
@if (str()->startsWith($attachment->mime_type, 'application/'))
@include('wirechat::livewire.chat.partials.file', [ 'attachment' => $attachment ])
@endif
{{-- Attachemnt is Video/ --}}
@if (str()->startsWith($attachment->mime_type, 'video/'))
<x-wirechat::video height="max-h-[400px]" :cover="false" source="{{ $attachment?->url }}" />
@endif
{{-- Attachemnt is image/ --}}
@if (str()->startsWith($attachment->mime_type, 'image/'))
@include('wirechat::livewire.chat.partials.image', [ 'previousMessage' => $previousMessage, 'message' => $message, 'nextMessage' => $nextMessage, 'belongsToAuth' => $belongsToAuth, 'attachment' => $attachment ])
@endif
@endif
{{-- if message is emoji then don't show the styled messagebody layout --}}
@if ($isEmoji)
<p class="text-5xl dark:text-white ">
{{ $message->body }}
</p>
@endif
{{-- -------------------- --}}
{{-- Message body section --}}
{{-- If message is not emoji then show the message body styles --}}
{{-- -------------------- --}}
@if ($message->body && !$isEmoji)
@include('wirechat::livewire.chat.partials.message', [ 'previousMessage' => $previousMessage, 'message' => $message, 'nextMessage' => $nextMessage, 'belongsToAuth' => $belongsToAuth, 'isGroup' => $isGroup, 'attachment' => $attachment])
@endif
</div>
</div>
</div>
</div>
</div>
@endforeach
@endforeach
@endif
</main>

View File

@@ -0,0 +1,32 @@
<div class="flex items-center group overflow-hidden border border-[var(--wc-light-secondary)] dark:border-[var(--wc-dark-secondary)] rounded-xl">
<span class=" p-2">
{{-- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-pdf-fill w-11 h-11 text-gray-600" viewBox="0 0 16 16">
<path d="M5.523 10.424q.21-.124.459-.238a8 8 0 0 1-.45.606c-.28.337-.498.516-.635.572l-.035.012a.3.3 0 0 1-.026-.044c-.056-.11-.054-.216.04-.36.106-.165.319-.354.647-.548m2.455-1.647q-.178.037-.356.078a21 21 0 0 0 .5-1.05 12 12 0 0 0 .51.858q-.326.048-.654.114m2.525.939a4 4 0 0 1-.435-.41q.344.007.612.054c.317.057.466.147.518.209a.1.1 0 0 1 .026.064.44.44 0 0 1-.06.2.3.3 0 0 1-.094.124.1.1 0 0 1-.069.015c-.09-.003-.258-.066-.498-.256M8.278 4.97c-.04.244-.108.524-.2.829a5 5 0 0 1-.089-.346c-.076-.353-.087-.63-.046-.822.038-.177.11-.248.196-.283a.5.5 0 0 1 .145-.04c.013.03.028.092.032.198q.008.183-.038.465z"/>
<path fill-rule="evenodd" d="M4 0h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2m.165 11.668c.09.18.23.343.438.419.207.075.412.04.58-.03.318-.13.635-.436.926-.786.333-.401.683-.927 1.021-1.51a11.6 11.6 0 0 1 1.997-.406c.3.383.61.713.91.95.28.22.603.403.934.417a.86.86 0 0 0 .51-.138c.155-.101.27-.247.354-.416.09-.181.145-.37.138-.563a.84.84 0 0 0-.2-.518c-.226-.27-.596-.4-.96-.465a5.8 5.8 0 0 0-1.335-.05 11 11 0 0 1-.98-1.686c.25-.66.437-1.284.52-1.794.036-.218.055-.426.048-.614a1.24 1.24 0 0 0-.127-.538.7.7 0 0 0-.477-.365c-.202-.043-.41 0-.601.077-.377.15-.576.47-.651.823-.073.34-.04.736.046 1.136.088.406.238.848.43 1.295a20 20 0 0 1-1.062 2.227 7.7 7.7 0 0 0-1.482.645c-.37.22-.699.48-.897.787-.21.326-.275.714-.08 1.103"/>
</svg> --}}
{{-- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-text-fill w-9 h-10 text-gray-500" viewBox="0 0 16 16">
<path d="M9.293 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.707A1 1 0 0 0 13.707 4L10 .293A1 1 0 0 0 9.293 0M9.5 3.5v-2l3 3h-2a1 1 0 0 1-1-1M4.5 9a.5.5 0 0 1 0-1h7a.5.5 0 0 1 0 1zM4 10.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5m.5 2.5a.5.5 0 0 1 0-1h4a.5.5 0 0 1 0 1z"/>
</svg> --}}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 text-gray-500">
<path d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625Z" />
<path d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z" />
</svg>
</span>
<p class="mt-auto p-2 text-gray-600 dark:text-gray-100 text-sm">
{{$attachment->original_name}}
</p>
<button class="px-3 bg-gray-50 bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)] transition-colors ease-in-out dark:hover:text-blue-500 hover:text-blue-500 dark:text-white p-1 mt-auto h-full">
<a download="{{$attachment->original_name}}" href="{{$attachment?->url}}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download w-5 h-5 " viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z"/>
</svg>
</a>
</button>
</div>

View File

@@ -0,0 +1,618 @@
@use('Namu\WireChat\Helpers\Helper')
<footer class="shrink-0 h-auto relative sticky bottom-0 mt-auto">
{{-- Check if group allows :sending messages --}}
@if ($conversation->isGroup() && !$conversation->group?->allowsMembersToSendMessages() && !$authParticipant->isAdmin())
<div
class="dark:bg-[var(--wc-dark-secondary)] bg-[var(--wc-light-secondary)] w-full text-center text-gray-600 dark:text-gray-200 justify-center text-sm flex py-4 ">
Only admins can send messages
</div>
@else
<div id="chat-footer" x-data="{ 'openEmojiPicker': false }"
class=" px-3 md:px-1 border-t shadow-sm bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)] z-50 border-[var(--wc-light-primary)] dark:border-[var(--wc-dark-primary)] flex flex-col gap-3 items-center w-full mx-auto">
{{-- Emoji section , we put it seperate to avoid interfering as overlay for form when opened --}}
<section wire:ignore x-cloak x-show="openEmojiPicker" x-transition:enter="transition ease-out duration-180 transform"
x-transition:enter-start=" translate-y-full" x-transition:enter-end=" translate-y-0"
x-transition:leave="transition ease-in duration-180 transform" x-transition:leave-start=" translate-y-0"
x-transition:leave-end="translate-y-full"
class="w-full flex hidden sm:flex py-2 sm:px-4 py-1.5 border-b border-[var(--wc-light-primary)] dark:border-[var(--wc-dark-primary)] h-96 min-w-full">
<emoji-picker dusk="emoji-picker" style="width: 100%"
class=" flex w-full h-full rounded-xl"
x-init="$el.dataSource = '/js/vendor/emoji-picker-element-data/en/emojibase/data.json'"></emoji-picker>
</section>
{{-- form and detail section --}}
<section
class=" py-2 sm:px-4 py-1.5 z-50 dark:bg-[var(--wc-dark-secondary)] bg-[var(--wc-light-secondary)] flex flex-col gap-3 items-center w-full mx-auto">
{{-- Media preview section --}}
<section x-show="$wire.media.length>0 ||$wire.files.length>0" x-cloak
class=" flex flex-col w-full gap-3" wire:loading.class="animate-pulse" wire:target="sendMessage">
@if (count($media) > 0)
<div x-data="attachments('media')">
{{-- todo: Implement error handling fromserver during file uploads --}}
{{--
@error('media')
<span class="flex text-sm text-red-500 pb-2 bg-gray-100 p-2 w-full justify-between">
{{$message}}
<button @click="$wire.resetAttachmentErrors()">X</button>
</span>
@enderror --}}
{{-- todo:Show progress when uploading files --}}
{{-- <div x-show="isUploading" class="w-full">
<progress class="w-full h-1 rounded-lg" max="100" x-bind:value="progress"></progress>
</div> --}}
<section
class=" flex overflow-x-scroll ms-overflow-style-none items-center w-full col-span-12 py-2 gap-5 "
style=" scrollbar-width: none; -ms-overflow-style: none;">
{{-- Loop through media for preview --}}
@foreach ($media as $key => $mediaItem)
@if (str()->startsWith($mediaItem->getMimeType(), 'image/'))
<div class="relative h-24 sm:h-36 aspect-4/3 ">
{{-- Delete image --}}
<button wire:loading.attr="disabled"
class="disabled:cursor-progress absolute -top-2 -right-2 z-10 dark:text-gray-50"
@click="removeUpload('{{ $mediaItem->getFilename() }}')">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="currentColor" class="bi bi-x-circle" viewBox="0 0 16 16">
<path
d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16" />
<path
d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708" />
</svg>
</button>
<img class="h-full w-full rounded-lg object-scale-down"
src="{{ $mediaItem->temporaryUrl() }}" alt="mediaItem">
</div>
@endif
{{-- Attachemnt is Video/ --}}
@if (str()->startsWith($mediaItem->getMimeType(), 'video/'))
<div class="relative h-24 sm:h-36 ">
<button wire:loading.attr="disabled"
class="disabled:cursor-progress absolute -top-2 -right-2 z-10 dark:text-gray-50"
@click="removeUpload('{{ $mediaItem->getFilename() }}')">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="currentColor" class="bi bi-x-circle" viewBox="0 0 16 16">
<path
d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16" />
<path
d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708" />
</svg>
</button>
<x-wirechat::video height="h-24 sm:h-36 " :cover="false"
:showToggleSound="false" :source="$mediaItem->temporaryUrl()" />
</div>
@endif
@endforeach
<label wire:loading.class="cursor-progress"
class="shrink-0 cursor-pointer relative w-16 h-14 rounded-lg bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-primary)] hover:bg-[var(--wc-light-primary)] dark:hover:bg-[var(--wc-dark-primary)] border border-[var(--wc-light-secondary)] dark:border-[var(--wc-dark-secondary)] flex text-center justify-center ">
<input wire:loading.attr="disabled"
@change="handleFileSelect(event,{{ count($media) }})" type="file" multiple
accept="{{ Helper::formattedMediaMimesForAcceptAttribute() }}" class="sr-only">
<span class="m-auto ">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="w-7 h-7 text-gray-600 dark:text-gray-100">
<path fill-rule="evenodd"
d="M1.5 6a2.25 2.25 0 0 1 2.25-2.25h16.5A2.25 2.25 0 0 1 22.5 6v12a2.25 2.25 0 0 1-2.25 2.25H3.75A2.25 2.25 0 0 1 1.5 18V6ZM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0 0 21 18v-1.94l-2.69-2.689a1.5 1.5 0 0 0-2.12 0l-.88.879.97.97a.75.75 0 1 1-1.06 1.06l-5.16-5.159a1.5 1.5 0 0 0-2.12 0L3 16.061Zm10.125-7.81a1.125 1.125 0 1 1 2.25 0 1.125 1.125 0 0 1-2.25 0Z"
clip-rule="evenodd" />
</svg>
</span>
</label>
</section>
</div>
@endif
{{-- ----------------------- --}}
{{-- Files preview section --}}
@if (count($files) > 0)
<section x-data="attachments('files')"
class="flex overflow-x-scroll ms-overflow-style-none items-center w-full col-span-12 py-2 gap-5 "
style=" scrollbar-width: none; -ms-overflow-style: none;">
{{-- Loop through files for preview --}}
@foreach ($files as $key => $file)
<div class="relative shrink-0">
{{-- Delete file button --}}
<button wire:loading.attr="disabled"
class="disabled:cursor-progress absolute -top-2 -right-2 z-10"
@click="removeUpload('{{ $file->getFilename() }}')">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="currentColor"
class="bi bi-x-circle dark:text-white dark:hover:text-red-500 hover:text-red-500 transition-colors"
viewBox="0 0 16 16">
<path
d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16" />
<path
d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708" />
</svg>
</button>
{{-- File details --}}
<div
class="flex items-center group overflow-hidden bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] hover:border-[var(--wc-light-primary)] dark:hover:border-[var(--wc-dark-primary)] border border-[var(--wc-light-secondary)] dark:border-[var(--wc-dark-secondary)] rounded-xl">
<span class=" p-2">
{{-- document svg:HI --}}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="currentColor" class="w-8 h-8 text-gray-500 dark:text-gray-100">
<path
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625Z" />
<path
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z" />
</svg>
</span>
<p class="mt-auto p-2 text-gray-600 dark:text-gray-100 text-sm">
{{ $file->getClientOriginalName() }}
</p>
</div>
</div>
@endforeach
{{-- Add more files --}}
{{-- TODO @if "( count($media)< $MAXFILES )" to hide upload button when maz files exceeded --}}
<label wire:loading.class="cursor-progress"
class="cursor-pointer shrink-0 relative w-16 h-14 rounded-lg bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] hover:border-[var(--wc-light-primary)] dark:hover:border-[var(--wc-dark-primary)] border border-[var(--wc-light-secondary)] dark:border-[var(--wc-dark-secondary)] transition-colors flex text-center justify-center ">
<input wire:loading.attr="disabled"
@change="handleFileSelect(event,{{ count($files) }})" type="file" multiple
accept="{{ Helper::formattedFileMimesForAcceptAttribute() }}" class="sr-only"
hidden>
<span class=" m-auto">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="w-6 h-6 dark:text-gray-50">
<path fill-rule="evenodd"
d="M12 3.75a.75.75 0 0 1 .75.75v6.75h6.75a.75.75 0 0 1 0 1.5h-6.75v6.75a.75.75 0 0 1-1.5 0v-6.75H4.5a.75.75 0 0 1 0-1.5h6.75V4.5a.75.75 0 0 1 .75-.75Z"
clip-rule="evenodd" />
</svg>
</span>
</label>
</section>
@endif
</section>
{{-- Replying to --}}
@if ($replyMessage != null)
<section class="p-px py-1 w-full col-span-12">
<div class="flex justify-between items-center dark:text-white">
<h6 class="text-sm">
{{ $replyMessage?->ownedBy($this->auth) ? __('wirechat::chat.labels.replying_to_yourself'): __('wirechat::chat.labels.replying_to',['participant'=>$replyMessage->sendable?->name]) }}
</h6>
<button wire:loading.attr="disabled" wire:click="removeReply()"
class="disabled:cursor-progress">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div>
{{-- Message being replied to --}}
<p class="truncate text-sm text-gray-500 dark:text-gray-200 max-w-md">
{{ $replyMessage->body != '' ? $replyMessage->body : ($replyMessage->hasAttachment() ? 'Attachment' : '') }}
</p>
</section>
@endif
<form x-data="{
'body': $wire.entangle('body'),
insertNewLine: function(textarea) {
{{-- Get the current cursor position --}}
var startPos = textarea.selectionStart;
var endPos = textarea.selectionEnd;
{{-- Insert a line break character at the cursor position --}}
var text = textarea.value;
var newText = text.substring(0, startPos) + '\n' + text.substring(endPos, text.length);
{{-- Update the textarea value and cursor position --}}
textarea.value = newText;
textarea.selectionStart = startPos + 1; // Set cursor position after the inserted newline
textarea.selectionEnd = startPos + 1;
{{-- update height of element smoothly --}}
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
}
}" x-init="{{-- Emoji picture click event listener --}}
document.querySelector('emoji-picker')
.addEventListener('emoji-click', event => {
// Get the emoji unicode from the event
const emoji = event.detail['unicode'];
// Get the current value and cursor position
const inputField = $refs.body;
const inputFieldValue = inputField._x_model.get() ?? '';
const startPos = inputField.selectionStart;
const endPos = inputField.selectionEnd;
// Insert the emoji at the current cursor position
const newValue = inputFieldValue.substring(0, startPos) + emoji + inputFieldValue.substring(endPos);
// Update the value and move cursor after the emoji
inputField._x_model.set(newValue);
inputField.setSelectionRange(startPos + emoji.length, startPos + emoji.length);
});"
@submit.prevent="((body && body?.trim().length > 0) || ($wire.media && $wire.media.length > 0)|| ($wire.files && $wire.files.length > 0)) ? ($wire.sendMessage(), openEmojiPicker = false) : null"
method="POST" autocapitalize="off" @class(['flex items-center col-span-12 w-full gap-2 gap-5'])>
@csrf
<input type="hidden" autocomplete="false" style="display: none">
{{-- Emoji Triggger icon --}}
<div class="w-10 hidden sm:flex max-w-fit items-center">
<button wire:loading.attr="disabled" type="button" dusk="emoji-trigger-button"
@click="openEmojiPicker = ! openEmojiPicker" x-ref="emojibutton"
class="cursor-pointer hover:scale-105 transition-transform disabled:cursor-progress rounded-full p-px dark:border-gray-700">
<svg x-bind:style="openEmojiPicker && { color: 'var(--wc-brand-primary)' }"
viewBox="0 0 24 24" height="24" width="24"
preserveAspectRatio="xMidYMid meet"
class="w-7 h-7 text-gray-600 dark:text-gray-300 srtoke-[1.3] dark:stroke-[1.2]"
version="1.1" x="0px" y="0px" enable-background="new 0 0 24 24">
<title>smiley</title>
<path fill="currentColor"
d="M9.153,11.603c0.795,0,1.439-0.879,1.439-1.962S9.948,7.679,9.153,7.679 S7.714,8.558,7.714,9.641S8.358,11.603,9.153,11.603z M5.949,12.965c-0.026-0.307-0.131,5.218,6.063,5.551 c6.066-0.25,6.066-5.551,6.066-5.551C12,14.381,5.949,12.965,5.949,12.965z M17.312,14.073c0,0-0.669,1.959-5.051,1.959 c-3.505,0-5.388-1.164-5.607-1.959C6.654,14.073,12.566,15.128,17.312,14.073z M11.804,1.011c-6.195,0-10.826,5.022-10.826,11.217 s4.826,10.761,11.021,10.761S23.02,18.423,23.02,12.228C23.021,6.033,17.999,1.011,11.804,1.011z M12,21.354 c-5.273,0-9.381-3.886-9.381-9.159s3.942-9.548,9.215-9.548s9.548,4.275,9.548,9.548C21.381,17.467,17.273,21.354,12,21.354z M15.108,11.603c0.795,0,1.439-0.879,1.439-1.962s-0.644-1.962-1.439-1.962s-1.439,0.879-1.439,1.962S14.313,11.603,15.108,11.603z">
</path>
</svg>
</button>
</div>
{{-- Show upload pop if media or file are empty --}}
{{-- Also only show upload popup if allowed in configuration --}}
@if (count($this->media) == 0 &&
count($this->files) == 0 &&
(config('wirechat.allow_file_attachments', true) || config('wirechat.allow_media_attachments', true)))
<x-wirechat::popover position="top" popoverOffset="70">
<x-slot name="trigger" wire:loading.attr="disabled">
<span dusk="upload-trigger-button">
{{-- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-7 h-7 dark:text-white/90">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg> --}}
{{-- <svg xmlns="http://www.w3.org/2000/svg"
width="16" height="16" fill="currentColor"
class="bi bi-plus-lg w-6 h-6 text-gray-600 dark:text-white/90" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2" />
</svg> --}}
{{-- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.3" stroke="currentColor" class="size-6 w-7 h-7 text-gray-600 dark:text-white/90">
<path stroke-linecap="round" stroke-linejoin="round" d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" />
</svg> --}}
<svg class="size-6 w-7 h-7 text-gray-600 dark:text-white/60"
xmlns="http://www.w3.org/2000/svg" width="36" height="36"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2"
stroke-linecap="round" stroke-linejoin="round" class="ai ai-Attach">
<path
d="M6 7.91V16a6 6 0 0 0 6 6v0a6 6 0 0 0 6-6V6a4 4 0 0 0-4-4v0a4 4 0 0 0-4 4v9.182a2 2 0 0 0 2 2v0a2 2 0 0 0 2-2V8" />
</svg>
</span>
</x-slot>
{{-- content --}}
<div class="grid gap-2 w-full ">
{{-- Upload Files --}}
@if (config('wirechat.allow_file_attachments', true))
<label wire:loading.class="cursor-progress" x-data="attachments('files')"
class="cursor-pointer">
<input wire:loading.attr="disabled" wire:target="sendMessage"
dusk="file-upload-input"
@change="handleFileSelect(event, {{ count($files) }})" type="file"
multiple accept="{{ Helper::formattedFileMimesForAcceptAttribute() }}"
class="sr-only" style="display: none">
<div
class="w-full flex items-center gap-3 px-1.5 py-2 rounded-md hover:bg-[var(--wc-light-primary)] dark:hover:bg-[var(--wc-dark-primary)] cursor-pointer">
<span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="currentColor" style="color: var(--wc-brand-primary);"
class="bi bi-folder-fill w-6 h-6" viewBox="0 0 16 16">
<path
d="M9.828 3h3.982a2 2 0 0 1 1.992 2.181l-.637 7A2 2 0 0 1 13.174 14H2.825a2 2 0 0 1-1.991-1.819l-.637-7a2 2 0 0 1 .342-1.31L.5 3a2 2 0 0 1 2-2h3.672a2 2 0 0 1 1.414.586l.828.828A2 2 0 0 0 9.828 3m-8.322.12q.322-.119.684-.12h5.396l-.707-.707A1 1 0 0 0 6.172 2H2.5a1 1 0 0 0-1 .981z" />
</svg>
</span>
<span class=" dark:text-white">
@lang('wirechat::chat.actions.upload_file.label')
</span>
</div>
</label>
@endif
{{-- Upload Media --}}
@if (config('wirechat.allow_media_attachments', true))
<label wire:loading.class="cursor-progress" x-data="attachments('media')"
class="cursor-pointer">
{{-- Trigger image upload --}}
<input dusk="media-upload-input" wire:loading.attr="disabled"
wire:target="sendMessage"
@change="handleFileSelect(event, {{ count($media) }})" type="file"
multiple accept="{{ Helper::formattedMediaMimesForAcceptAttribute() }}"
class="sr-only" style="display: none">
<div
class="w-full flex items-center gap-3 px-1.5 py-2 rounded-md hover:bg-[var(--wc-light-primary)] dark:hover:bg-[var(--wc-dark-primary)] cursor-pointer">
<span class="">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="currentColor" class="w-6 h-6"
style="color: var(--wc-brand-primary);">
<path fill-rule="evenodd"
d="M1.5 6a2.25 2.25 0 0 1 2.25-2.25h16.5A2.25 2.25 0 0 1 22.5 6v12a2.25 2.25 0 0 1-2.25 2.25H3.75A2.25 2.25 0 0 1 1.5 18V6ZM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0 0 21 18v-1.94l-2.69-2.689a1.5 1.5 0 0 0-2.12 0l-.88.879.97.97a.75.75 0 1 1-1.06 1.06l-5.16-5.159a1.5 1.5 0 0 0-2.12 0L3 16.061Zm10.125-7.81a1.125 1.125 0 1 1 2.25 0 1.125 1.125 0 0 1-2.25 0Z"
clip-rule="evenodd" />
</svg>
</span>
<span class=" dark:text-white">
@lang('wirechat::chat.actions.upload_media.label')
</span>
</div>
</label>
@endif
</div>
</x-wirechat::popover>
@endif
{{-- --------------- --}}
{{-- TextArea Input --}}
{{-- --------------- --}}
<div @class(['flex gap-2 sm:px-2 w-full'])>
<textarea @focus-input-field.window="$el.focus()" autocomplete="off" x-model='body' x-ref="body"
wire:loading.delay.longest.attr="disabled" wire:target="sendMessage" id="chat-input-field" autofocus
type="text" name="message" placeholder="{{ __('wirechat::chat.inputs.message.placeholder') }}" maxlength="1700" rows="1"
@input="$el.style.height = 'auto'; $el.style.height = $el.scrollHeight + 'px';"
@keydown.shift.enter.prevent="insertNewLine($el)" {{-- @keydown.enter.prevent prevents the
default behavior of Enter key press only if Shift is not held down. --}} @keydown.enter.prevent=""
@keyup.enter.prevent="$event.shiftKey ? null : (((body && body?.trim().length > 0) || ($wire.media && $wire.media.length > 0)) ? ($wire.sendMessage(), openEmojiPicker = false) : null)"
class="w-full disabled:cursor-progress resize-none h-auto max-h-20 sm:max-h-72 flex grow border-0 outline-0 focus:border-0 focus:ring-0 hover:ring-0 rounded-lg dark:text-white bg-none dark:bg-inherit focus:outline-hidden "
x-init="document.querySelector('emoji-picker')
.addEventListener('emoji-click', event => {
const emoji = event.detail['unicode'];
const inputField = $refs.body;
// Get the current cursor position (start and end)
const startPos = inputField.selectionStart;
const endPos = inputField.selectionEnd;
// Get current value of the input field
const currentValue = inputField.value;
// Insert the emoji at the cursor position, preserving line breaks and spaces
const newValue = currentValue.substring(0, startPos) + emoji + currentValue.substring(endPos);
// Update Alpine.js model (x-model='body') with the new value
inputField._x_model.set(newValue);
// Set the cursor position after the inserted emoji
inputField.setSelectionRange(startPos + emoji.length, startPos + emoji.length);
// Ensure the textarea resizes correctly after adding the emoji
inputField.style.height = 'auto';
inputField.style.height = inputField.scrollHeight + 'px';
});"></textarea>
</div>
{{-- --------------- --}}
{{-- input Actions --}}
{{-- --------------- --}}
<div x-cloak @class(['w-[5%] justify-end min-w-max items-center gap-2 '])>
{{-- Submit button --}}
<button
x-show="((body?.trim()?.length>0) || $wire.media.length > 0 || $wire.files.length > 0 )"
wire:loading.attr="disabled" wire:target="sendMessage" type="submit"
id="sendMessageButton" class="cursor-pointer hover:text-[var(--wc-brand-primary)] transition-color ml-auto disabled:cursor-progress cursor-pointer font-bold">
<svg class="w-7 h-7 dark:text-gray-200" xmlns="http://www.w3.org/2000/svg"
width="36" height="36" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round" class="ai ai-Send">
<path
d="M9.912 12H4L2.023 4.135A.662.662 0 0 1 2 3.995c-.022-.721.772-1.221 1.46-.891L22 12 3.46 20.896c-.68.327-1.464-.159-1.46-.867a.66.66 0 0 1 .033-.186L3.5 15" />
</svg>
</button>
{{-- send Like button --}}
{{-- <button
x-show="!((body?.trim()?.length>0) || $wire.media.length > 0 || $wire.files.length > 0 )"
wire:loading.attr="disabled" wire:target="sendMessage" wire:click='sendLike()'
type="button" class="hover:scale-105 transition-transform cursor-pointer group disabled:cursor-progress">
<!-- outlined heart -->
<span class=" group-hover:hidden transition">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor"
class="w-7 h-7 text-gray-600 dark:text-white/90 stroke-[1.4] dark:stroke-[1.4]">
<path stroke-linecap="round" stroke-linejoin="round"
d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
</svg>
</span>
<!-- filled heart -->
<span class="hidden group-hover:block transition " x-bounce>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="size-6 w-7 h-7 text-red-500">
<path
d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z" />
</svg>
</span>
</button> --}}
</div>
</form>
</section>
@script
<script>
Alpine.data('attachments', (type = "media") => ({
// State variables
isDropping: false, // Tracks if a file is being dragged over the drop area
type: type, // Type of file being uploaded (e.g., "media" or "file")
isUploading: false, // Indicates if files are currently uploading
MAXFILES: @json(config('wirechat.attachments.max_uploads', 5)), // Maximum number of files allowed
maxSize: @json(config('wirechat.attachments.media_max_upload_size', 12288)) * 1024, // Max size per file (in bytes)
allowedFileTypes: type === 'media' ? @json(config('wirechat.attachments.media_mimes')) :
@json(config('wirechat.attachments.file_mimes')), // Allowed MIME types based on type
progress: 0, // Progress of the current upload (0-100)
wireModel: type, // The Livewire model to bind to
// Handle file selection from the input field
handleFileSelect(event, count) {
if (event.target.files.length) {
const files = event.target.files;
// Validate selected files and upload if valid
this.validateFiles(files, count)
.then((validFiles) => {
if (validFiles.length > 0) {
this.uploadFiles(validFiles);
} else {
}
})
.catch((error) => {
});
}
},
// Upload files using Livewire's upload
uploadFiles(files) {
this.isUploading = true;
this.progress = 0;
// Initialize per-file progress tracking
const fileProgress = Array.from(files).map(() => 0);
files.forEach((file, index) => {
$wire.upload(
`${this.wireModel}`, // Livewire model
file, // Single file
() => {
fileProgress[index] = 100; // Mark this file as complete
// this.isUploading = false;
this.progress = Math.round((fileProgress.reduce((a, b) => a + b, 0)) / files.length);
},
(error) => {
// this.isUploading = false;
fileProgress[index] = -1; // Mark as failed
$dispatch('wirechat-toast', { type: 'error', message: `Validation error: ${error}` });
},
(event) => {
fileProgress[index] = event.detail.progress; // Update per-file progress
this.progress = Math.round((fileProgress.reduce((a, b) => a + b, 0)) / files.length); // Overall progress
}
);
});
},
// Upload files using Livewire's uploadMultiple method
// Remove an uploaded file from Livewire
removeUpload(filename) {
$wire.removeUpload(this.wireModel, filename);
},
// Validate selected files against constraints
validateFiles(files, count) {
const totalFiles = count + files.length; // Total file count including existing uploads
// Check if total file count exceeds the maximum allowed
if (totalFiles > this.MAXFILES) {
files = Array.from(files).slice(0, this.MAXFILES -
count); // Limit files to the allowed number
$dispatch('wirechat-toast', {
type: 'warning',
message: @js(__('wirechat::validation.max.array', ['attribute' => __('wirechat::chat.inputs.media.label'),'max'=>config('wirechat.attachments.max_uploads', 5)]))
});
}
// Filter invalid files based on size and type
const invalidFiles = Array.from(files).filter((file) => {
const fileType = file.type.split('/')[1].toLowerCase(); // Extract file extension
return file.size > this.maxSize || !this.allowedFileTypes.includes(
fileType); // Check size and type
});
// Filter valid files
const validFiles = Array.from(files).filter((file) => {
const fileType = file.type.split('/')[1].toLowerCase();
return file.size <= this.maxSize && this.allowedFileTypes.includes(fileType);
});
// Handle invalid files by showing appropriate error messages
if (invalidFiles.length > 0) {
invalidFiles.forEach((file) => {
if (file.size > this.maxSize) {
$dispatch('wirechat-toast', {
type: 'warning',
message: @js(__('wirechat::validation.max.file', ['attribute' => __('wirechat::chat.inputs.media.label'),'max'=>config('wirechat.attachments.media_max_upload_size', 12288)]))
// message: `File size exceeds the maximum limit (${this.maxSize / 1024 / 1024}MB): ${file.name}`
});
} else {
const extension = file.name.split('.').pop().toLowerCase();
$dispatch('wirechat-toast', {
type: 'warning',
message: @js(__('wirechat::validation.mimes', [ 'attribute' => __('wirechat::chat.inputs.media.label'), 'values' => implode(', ', config('wirechat.attachments.media_mimes')) ]))
// message: `One or more Files not uploaded: .${extension} (type not allowed)`
});
}
});
}
return Promise.resolve(validFiles); // Return valid files for further processing
}
}));
</script>
@endscript
</div>
@endif
</footer>

View File

@@ -0,0 +1,179 @@
@use('Namu\WireChat\Facades\WireChat')
@php
$group = $conversation->group;
@endphp
<header
class="w-full sticky inset-x-0 flex pb-[5px] pt-[7px] top-0 z-10 dark:bg-[var(--wc-dark-secondary)] bg-[var(--wc-light-secondary)] border-[var(--wc-light-primary)] dark:border-[var(--wc-dark-secondary)] border-b">
<div class=" flex w-full items-center px-3 py-3 lg:px-4 gap-2 md:gap-5 ">
{{-- Return --}}
<a @if ($this->isWidget()) @click="$dispatch('close-chat',{conversation: {{json_encode($conversation->id)}} })"
dusk="return_to_home_button_dispatch"
@else
href="{{ route(WireChat::indexRouteName(), $conversation->id) }}"
dusk="return_to_home_button_link" @endif
@class([
'shrink-0 cursor-pointer dark:text-white',
'lg:hidden' => !$this->isWidget(),
]) id="chatReturn">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.6"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
</a>
{{-- Receiver wirechat::Avatar --}}
<section class="grid grid-cols-12 w-full">
<div class="shrink-0 col-span-11 w-full overflow-h-hidden relative">
{{-- Group --}}
@if ($conversation->isGroup())
<x-wirechat::actions.show-group-info conversation="{{ $conversation->id }}"
widget="{{ $this->isWidget() }}">
<div class="flex items-center gap-2 cursor-pointer w-full">
<x-wirechat::avatar :group="true" :src="$group?->cover_url ?? null "
class="h-8 w-8 lg:w-10 lg:h-10 " />
<h6 class="font-bold text-base text-gray-800 dark:text-white truncate">
{{ $group?->name }}
</h6>
@if(timebank_config('wirechat.disappearing_messages.enabled', true))
@php
$durationInDays = timebank_config('wirechat.disappearing_messages.duration', 30);
@endphp
<p class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap mx-2">
{{ __('Messages deleted after :days days', ['days' => $durationInDays]) }}
</p>
@endif
</div>
</x-wirechat::actions.show-group-info>
@else
{{-- Not Group --}}
<x-wirechat::actions.show-chat-info conversation="{{ $conversation->id }}"
widget="{{ $this->isWidget() }}">
<div class="flex items-center gap-2 cursor-pointer w-full">
<x-wirechat::avatar :group="false" :src="$receiver?->cover_url ?? null"
class="h-8 w-8 lg:w-10 lg:h-10 " />
<div class="flex flex-col min-w-0">
<h6 class="font-bold text-base text-gray-800 dark:text-white truncate">
{{ $receiver?->display_name }} @if ($conversation->isSelfConversation())
({{ __('wirechat::chat.labels.you') }})
@endif
</h6>
<livewire:profile-status-badge
:profileId="$receiver?->id"
:guard="$receiver instanceof \App\Models\Organization ? 'organization' : ($receiver instanceof \App\Models\Bank ? 'bank' : ($receiver instanceof \App\Models\Admin ? 'admin' : 'web'))"
/>
</div>
</div>
@if(timebank_config('wirechat.disappearing_messages.enabled', true))
@php
$durationInDays = timebank_config('wirechat.disappearing_messages.duration', 30);
@endphp
<div class="text-xs text-gray-500 dark:text-gray-400 whitespace-normal mt-1 mx-auto">
{{ __('messages.wirechat.messages_deleted_after', ['days' => $durationInDays]) }}
{{ __('Click on message actions (three dots) to keep.')}}
</div>
@endif
</x-wirechat::actions.show-chat-info>
@endif
</div>
{{-- Header Actions --}}
<div class="flex gap-2 items-center ml-auto col-span-1">
<x-wirechat::dropdown align="right" width="48">
<x-slot name="trigger">
<button class="cursor-pointer inline-flex px-0 text-gray-700 dark:text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.9" stroke="currentColor" class="size-6 w-7 h-7">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z" />
</svg>
</button>
</x-slot>
<x-slot name="content">
@if ($conversation->isGroup())
{{-- Open group info button --}}
<x-wirechat::actions.show-group-info conversation="{{ $conversation->id }}"
widget="{{ $this->isWidget() }}">
<button class="w-full text-start">
<x-wirechat::dropdown-link>
{{ __('wirechat::chat.actions.open_group_info.label') }}
</x-wirechat::dropdown-link>
</button>
</x-wirechat::actions.show-group-info>
@else
{{-- Open chat info button --}}
<x-wirechat::actions.show-chat-info conversation="{{ $conversation->id }}"
widget="{{ $this->isWidget() }}">
<button class="w-full text-start">
<x-wirechat::dropdown-link>
{{ __('wirechat::chat.actions.open_chat_info.label') }}
</x-wirechat::dropdown-link>
</button>
</x-wirechat::actions.show-chat-info>
@endif
@if ($this->isWidget())
<x-wirechat::dropdown-link @click="$dispatch('close-chat',{conversation: {{json_encode($conversation->id)}} })">
@lang('wirechat::chat.actions.close_chat.label')
</x-wirechat::dropdown-link>
@else
<x-wirechat::dropdown-link href="{{ route(WireChat::indexRouteName()) }}" class="shrink-0">
@lang('wirechat::chat.actions.close_chat.label')
</x-wirechat::dropdown-link>
@endif
{{-- Only show delete and clear if conversation is NOT group --}}
@if (!$conversation->isGroup())
<button class="w-full" wire:click="clearConversation"
wire:confirm="{{ __('wirechat::chat.actions.clear_chat.confirmation_message') }}">
<x-wirechat::dropdown-link>
@lang('wirechat::chat.actions.clear_chat.label')
</x-wirechat::dropdown-link>
</button>
<button wire:click="deleteConversation"
wire:confirm="{{ __('wirechat::chat.actions.delete_chat.confirmation_message') }}"
class="w-full text-start">
<x-wirechat::dropdown-link class="text-red-500 dark:text-red-500">
@lang('wirechat::chat.actions.delete_chat.label')
</x-wirechat::dropdown-link>
</button>
@endif
@if ($conversation->isGroup() && !$this->auth->isOwnerOf($conversation))
<button wire:click="exitConversation"
wire:confirm="{{ __('wirechat::chat.actions.exit_group.confirmation_message') }}"
class="w-full text-start ">
<x-wirechat::dropdown-link class="text-red-500 dark:text-gray-500">
@lang('wirechat::chat.actions.exit_group.label')
</x-wirechat::dropdown-link>
</button>
@endif
</x-slot>
</x-wirechat::dropdown>
</div>
</section>
</div>
</header>

View File

@@ -0,0 +1,43 @@
@php
$isSameAsNext = ($message?->sendable_id === $nextMessage?->sendable_id) && ($message?->sendable_type === $nextMessage?->sendable_type);
$isNotSameAsNext = !$isSameAsNext;
$isSameAsPrevious = ($message?->sendable_id === $previousMessage?->sendable_id) && ($message?->sendable_type === $previousMessage?->sendable_type);
$isNotSameAsPrevious = !$isSameAsPrevious;
@endphp
<img @class([
'max-w-max h-[200px] min-h-[210px] bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)] object-scale-down grow-0 shrink overflow-hidden rounded-3xl',
'rounded-br-md rounded-tr-2xl' => ($isSameAsNext && $isNotSameAsPrevious && $belongsToAuth),
// Middle message on RIGHT
'rounded-r-md' => ($isSameAsPrevious && $belongsToAuth),
// Standalone message RIGHT
'rounded-br-xl rounded-r-xl' => ($isNotSameAsPrevious && $isNotSameAsNext && $belongsToAuth),
// Last Message on RIGHT
'rounded-br-2xl' => ($isNotSameAsNext && $belongsToAuth),
// LEFT
// First message on LEFT
'rounded-bl-md rounded-tl-2xl' => ($isSameAsNext && $isNotSameAsPrevious && !$belongsToAuth),
// Middle message on LEFT
'rounded-l-md' => ($isSameAsPrevious && !$belongsToAuth),
// Standalone message LEFT
'rounded-bl-xl rounded-l-xl' => ($isNotSameAsPrevious && $isNotSameAsNext && !$belongsToAuth),
// Last message on LEFT
'rounded-bl-2xl' => ($isNotSameAsNext && !$belongsToAuth),
])
loading="lazy" src="{{$attachment?->url}}" alt="{{ __('wirechat::chat.labels.attachment') }}">

View File

@@ -0,0 +1,114 @@
@use('Namu\WireChat\Facades\WireChat')
@php
$isSameAsNext = ($message?->sendable_id === $nextMessage?->sendable_id) && ($message?->sendable_type === $nextMessage?->sendable_type);
$isNotSameAsNext = !$isSameAsNext;
$isSameAsPrevious = ($message?->sendable_id === $previousMessage?->sendable_id) && ($message?->sendable_type === $previousMessage?->sendable_type);
$isNotSameAsPrevious = !$isSameAsPrevious;
@endphp
<div
{{-- We use style here to make it easy for dynamic and safe injection --}}
@style([
'background-color:var(--wc-brand-primary)' => $belongsToAuth==true
])
@class([
'flex flex-wrap max-w-fit text-[15px] border border-gray-200/40 dark:border-none rounded-xl p-2.5 flex flex-col text-black bg-[#f6f6f8fb]',
'text-white' => $belongsToAuth, // Background color for messages sent by the authenticated user
'bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)] dark:text-white' => !$belongsToAuth,
// Message styles based on position and ownership
// RIGHT
// First message on RIGHT
'rounded-br-md rounded-tr-2xl' => ($isSameAsNext && $isNotSameAsPrevious && $belongsToAuth),
// Middle message on RIGHT
'rounded-r-md' => ($isSameAsPrevious && $belongsToAuth),
// Standalone message RIGHT
'rounded-br-xl rounded-r-xl' => ($isNotSameAsPrevious && $isNotSameAsNext && $belongsToAuth),
// Last Message on RIGHT
'rounded-br-2xl' => ($isNotSameAsNext && $belongsToAuth),
// LEFT
// First message on LEFT
'rounded-bl-md rounded-tl-2xl' => ($isSameAsNext && $isNotSameAsPrevious && !$belongsToAuth),
// Middle message on LEFT
'rounded-l-md' => ($isSameAsPrevious && !$belongsToAuth),
// Standalone message LEFT
'rounded-bl-xl rounded-l-xl' => ($isNotSameAsPrevious && $isNotSameAsNext && !$belongsToAuth),
// Last message on LEFT
'rounded-bl-2xl' => ($isNotSameAsNext && !$belongsToAuth),
])
>
@if (!$belongsToAuth && $isGroup)
<div
@class([
'shrink-0 font-medium text-black',
// Hide avatar if the next message is from the same user
'hidden' => $isSameAsPrevious
])>
{{ $message?->sendable?->display_name }}
</div>
@endif
@php
// Check if the body is a valid internal URL so a href link can be rendered
$appUrl = rtrim(config('app.url'), '/');
$body = trim($message?->body ?? '');
$isInternalUrl = false;
if (filter_var($body, FILTER_VALIDATE_URL)) {
$isInternalUrl = str_starts_with($body, $appUrl);
}
// Check if body is a transaction statement URL
$transactionShowPattern = '#^' . preg_quote($appUrl, '#') . '/[a-z]{2}/statement/\d+$#';
$isTransactionLink = preg_match($transactionShowPattern, $body);
@endphp
@if ($isTransactionLink)
<pre class="whitespace-pre-line tracking-normal text-sm border pt-3 border-white rounded-lg md:text-base dark:text-white lg:tracking-normal"
style="font-family: inherit;">
<a href="{{ $body }}" class="underline">
<div class="flex flex-col items-center gap-0 w-fit max-h-fit">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-8">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0 1 3.75 9.375v-4.5ZM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 0 1-1.125-1.125v-4.5ZM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0 1 13.5 9.375v-4.5Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 6.75h.75v.75h-.75v-.75ZM6.75 16.5h.75v.75h-.75v-.75ZM16.5 6.75h.75v.75h-.75v-.75ZM13.5 13.5h.75v.75h-.75v-.75ZM13.5 19.5h.75v.75h-.75v-.75ZM19.5 13.5h.75v.75h-.75v-.75ZM19.5 19.5h.75v.75h-.75v-.75ZM16.5 16.5h.75v.75h-.75v-.75Z" />
</svg>
<span class="text-center mx-6">
{{ __('View transaction') }}
</span>
</div>
</a>
</pre>
@else
<pre class="whitespace-pre-line tracking-normal text-sm md:text-base dark:text-white lg:tracking-normal"
style="font-family: inherit;">
@if ($isInternalUrl)
<a href="{{ $body }}" class="underline">
{{ $body }}
</a>
@else
{{ $body }}
@endif
</pre>
@endif
{{-- Display the created time based on different conditions --}}
<span
@class(['text-[11px] ml-auto ', 'text-gray-700 dark:text-gray-300' => !$belongsToAuth,'text-gray-100' => $belongsToAuth])>
@php
// If the message was created today, show only the time (e.g., 1:00 AM)
echo $message?->created_at->format('H:i');
@endphp
</span>
</div>

View File

@@ -0,0 +1,57 @@
@use('Namu\WireChat\Facades\WireChat')
<div x-data="{ selectedConversationId: '{{ request()->conversation ?? $selectedConversationId }}' }"
x-on:open-chat.window="selectedConversationId= $event.detail.conversation; $wire.selectedConversationId= $event.detail.conversation;"
x-init=" setTimeout(() => {
conversationElement = document.getElementById('conversation-' + selectedConversationId);
// Scroll to the conversation element
if (conversationElement) {
conversationElement.scrollIntoView({ behavior: 'smooth' });
}
}, 200);"
class="flex flex-col bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] transition-all h-full overflow-hidden w-full sm:p-3">
@php
/* Show header if any of these conditions are true */
$showHeader = $showNewChatModalButton || $allowChatsSearch || $showHomeRouteButton || !empty($title);
@endphp
{{-- include header --}}
@includeWhen($showHeader, 'wirechat::livewire.chats.partials.header')
<main x-data
@scroll.self.debounce="
{{-- Detect when scrolled to the bottom --}}
// Calculate scroll values
scrollTop = $el.scrollTop;
scrollHeight = $el.scrollHeight;
clientHeight = $el.clientHeight;
// Check if the user is at the bottom of the scrollable element
if ((scrollTop + clientHeight) >= (scrollHeight - 1) && $wire.canLoadMore) {
// Trigger load more if we're at the bottom
await $nextTick();
$wire.loadMore();
}
"
class=" overflow-y-auto py-2 grow h-full relative " style="contain:content">
{{-- loading indicator --}}
@if (count($conversations) > 0)
{{-- include list item --}}
@include('wirechat::livewire.chats.partials.list')
{{-- include load more if true --}}
@includeWhen($canLoadMore, 'wirechat::livewire.chats.partials.load-more-button')
@else
<div class="w-full flex items-center h-full justify-center">
<h6 class=" font-bold text-gray-700 dark:text-white">{{ __('wirechat::chats.labels.no_conversations_yet') }}</h6>
</div>
@endif
</main>
</div>

View File

@@ -0,0 +1,71 @@
@use('Namu\WireChat\Facades\WireChat')
<header class="px-3 z-10 sticky top-0 w-full py-2 " dusk="header">
{{-- Title/name and Icon --}}
<section class=" justify-between flex items-center pb-2">
@if (isset($title))
<div class="flex items-center gap-2 truncate " wire:ignore>
<h2 class=" text-2xl font-bold dark:text-white" dusk="title">{{$title}}</h2>
</div>
@endif
<div class="flex gap-x-3 items-center ">
@if ($showNewChatModalButton)
<x-wirechat::actions.new-chat widget="{{$this->isWidget()}}">
<button id="open-new-chat-modal-button" class=" flex items-center gap-2 focus:outline-hidden">
<span class="text-sm font-medium text-gray-900 dark:text-gray-300">{{ __('wirechat::new.chat.labels.heading') }}</span>
<svg class="w-8 h-8 -mb-1 text-gray-900 cursor-pointer hover:text-gray-700 dark:hover:text-gray-200 dark:text-gray-300"
xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor">
<path
d="M12.875 5C9.225 5 7.4 5 6.242 6.103a4 4 0 0 0-.139.139C5 7.4 5 9.225 5 12.875V17c0 .943 0 1.414.293 1.707S6.057 19 7 19h4.125c3.65 0 5.475 0 6.633-1.103a4 4 0 0 0 .139-.139C19 16.6 19 14.775 19 11.125" />
<path stroke-linecap="round" stroke-linejoin="round" d="M9 10h6m-6 4h3m7-6V2m-3 3h6" />
</g>
</svg>
{{-- <svg class="w-7 h-7 -mb-1 stroke-[0.1] stroke-none text-gray-500 hover:text-gray-900 dark:hover:text-gray-200 dark:text-gray-300" stroke="currentColor" viewBox="0 0 24 24" height="24" width="24" preserveAspectRatio="xMidYMid meet" fill="none"> <path d="M9.53277 12.9911H11.5086V14.9671C11.5086 15.3999 11.7634 15.8175 12.1762 15.9488C12.8608 16.1661 13.4909 15.6613 13.4909 15.009V12.9911H15.4672C15.9005 12.9911 16.3181 12.7358 16.449 12.3226C16.6659 11.6381 16.1606 11.0089 15.5086 11.0089H13.4909V9.03332C13.4909 8.60007 13.2361 8.18252 12.8233 8.05119C12.1391 7.83391 11.5086 8.33872 11.5086 8.991V11.0089H9.49088C8.83941 11.0089 8.33411 11.6381 8.55097 12.3226C8.68144 12.7358 9.09947 12.9911 9.53277 12.9911Z" fill="currentColor"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M0.944298 5.52617L2.99998 8.84848V17.3333C2.99998 18.8061 4.19389 20 5.66665 20H19.3333C20.8061 20 22 18.8061 22 17.3333V6.66667C22 5.19391 20.8061 4 19.3333 4H1.79468C1.01126 4 0.532088 4.85997 0.944298 5.52617ZM4.99998 8.27977V17.3333C4.99998 17.7015 5.29845 18 5.66665 18H19.3333C19.7015 18 20 17.7015 20 17.3333V6.66667C20 6.29848 19.7015 6 19.3333 6H3.58937L4.99998 8.27977Z" fill="currentColor" stroke="currentColor"></path></svg> --}}
{{-- <svg class="w-7 h-7 -mb-1 text-gray-500 hover:text-gray-900 dark:hover:text-gray-200 dark:text-gray-300" xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" class="ai ai-ChatAdd"><path d="M12 8v3m0 0v3m0-3h3m-3 0H9"/><path d="M14 19c3.771 0 5.657 0 6.828-1.172C22 16.657 22 14.771 22 11c0-3.771 0-5.657-1.172-6.828C19.657 3 17.771 3 14 3h-4C6.229 3 4.343 3 3.172 4.172 2 5.343 2 7.229 2 11c0 3.771 0 5.657 1.172 6.828.653.654 1.528.943 2.828 1.07"/><path d="M14 19c-1.236 0-2.598.5-3.841 1.145-1.998 1.037-2.997 1.556-3.489 1.225-.492-.33-.399-1.355-.212-3.404L6.5 17.5"/></svg> --}}
</button>
</x-wirechat::actions.new-chat>
@endif
</div>
</section>
{{-- Search input --}}
@if ($allowChatsSearch)
<section class="mt-4">
<div class="px-2 rounded-lg dark:bg-[var(--wc-dark-secondary)] bg-[var(--wc-light-secondary)] grid grid-cols-12 items-center">
<label for="chats-search-field" class="col-span-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-5 w-5 h-5 dark:text-gray-300">
<path stroke-linecap="round" stroke-linejoin="round"
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg>
</label>
<input id="chats-search-field"
name="chats_search"
maxlength="100"
type="search"
wire:model.live.debounce='search'
placeholder="{{ __('wirechat::chats.inputs.search.placeholder') }}"
autocomplete="off"
@click.stop
class="col-span-11 border-0 bg-inherit dark:text-white outline-hidden w-full focus:outline-hidden focus:ring-0 hover:ring-0">
</div>
</section>
@endif
</header>

View File

@@ -0,0 +1,107 @@
@use('Namu\WireChat\Facades\WireChat')
<ul wire:loading.delay.long.remove wire:target="search" class="p-2 grid w-full spacey-y-2">
@foreach ($conversations as $key=> $conversation)
@php
//$receiver =$conversation->getReceiver();
$group = $conversation->isGroup() ? $conversation->group : null;
$receiver = $conversation->isGroup() ? null : ($conversation->isPrivate() ? $conversation->peer_participant?->participantable : $this->auth);
//$receiver = $conversation->isGroup() ? null : ($conversation->isPrivate() ? $conversation->peerParticipant()?->participantable : $this->auth);
$lastMessage = $conversation->lastMessage;
//mark isReadByAuth true if user has chat opened
$isReadByAuth = $conversation?->readBy($conversation->auth_participant??$this->auth) || $selectedConversationId == $conversation->id;
$belongsToAuth = $lastMessage?->belongsToAuth();
@endphp
<li x-data="{
conversationID: @js($conversation->id),
showUnreadStatus: @js(!$isReadByAuth),
handleChatOpened(event) {
// Hide unread dot
if (event.detail.conversation== this.conversationID) {
this.showUnreadStatus= false;
}
//update this so that the the selected conversation highlighter can be updated
$wire.selectedConversationId= event.detail.conversation;
},
handleChatClosed(event) {
// Clear the globally selected conversation.
$wire.selectedConversationId = null;
selectedConversationId = null;
},
handleOpenChat(event) {
// Clear the globally selected conversation.
if (this.showUnreadStatus== event.detail.conversation== this.conversationID) {
this.showUnreadStatus= false;
}
}
}"
id="conversation-{{ $conversation->id }}"
wire:key="conversation-em-{{ $conversation->id }}-{{ $conversation->updated_at->timestamp }}"
x-on:chat-opened.window="handleChatOpened($event)"
x-on:chat-closed.window="handleChatClosed($event)">
<a @if ($widget) tabindex="0"
role="button"
dusk="openChatWidgetButton"
@click="$dispatch('open-chat',{conversation:@js($conversation->id)})"
@keydown.enter="$dispatch('open-chat',{conversation:@js($conversation->id)})"
@else
wire:navigate href="{{ route(WireChat::viewRouteName(), $conversation->id) }}" @endif
@style(['border-color:var(--wc-brand-primary)' => $selectedConversationId == $conversation?->id])
class="py-3 flex gap-4 dark:hover:bg-[var(--wc-dark-secondary)] hover:bg-[var(--wc-light-secondary)] rounded-xs transition-colors duration-150 relative w-full cursor-pointer px-2"
:class="$wire.selectedConversationId == conversationID &&
'rounded-md dark:bg-[var(--wc-dark-secondary)] bg-[var(--wc-light-secondary)] border-[var(--wc-brand-primary)]'">
<div class="shrink-0">
<x-wirechat::avatar group="{{ $conversation->isGroup() }}"
:src="$group ? $group?->cover_url : $receiver?->cover_url ?? null" class="w-12 h-12" />
</div>
<aside class="grid grid-cols-12 w-full">
<div
class="col-span-10 pb-2 relative overflow-hidden truncate leading-5 w-full flex-nowrap p-1">
{{-- name --}}
<div class="flex gap-1 mb-1 w-full items-center">
<h6 class="truncate font-medium text-gray-900 dark:text-white">
{{ $group ? $group?->name : $receiver?->display_name }}
</h6>
@if ($conversation->isSelfConversation())
<span class="font-medium dark:text-white">({{__('wirechat::chats.labels.you') }})</span>
@endif
</div>
{{-- Message body --}}
@if ($lastMessage != null)
@include('wirechat::livewire.chats.partials.message-body')
@endif
</div>
{{-- Read status --}}
{{-- Only show if AUTH is NOT onwer of message --}}
@if ($lastMessage != null && !$lastMessage?->ownedBy($this->auth) && !$isReadByAuth)
<div x-show="showUnreadStatus" dusk="unreadMessagesDot" class=" col-span-2 flex flex-col text-center my-auto">
{{-- Dots icon --}}
<span dusk="unreadDotItem" class="sr-only">unread dot</span>
{{-- 1. Remove the @style directive and change the class --}}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="#b91c1c" class="bi bi-dot w-10 h-10 text-gray-700" viewBox="0 0 16 16">
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />
</svg>
</div>
@endif
</aside>
</a>
</li>
@endforeach
</ul>

View File

@@ -0,0 +1,11 @@
<section wire:loading.remove wire:target="search" class="w-full justify-center flex my-3 ">
<button wire:loading.remove wire:target="loadMore" wire:loading.attr="disabled"
dusk="loadMoreButton" @click="$wire.loadMore()"
class=" text-sm dark:text-white disabled:hover:cursor-not-allowed hover:text-gray-700 transition-colors dark:hover:text-gray-500 dark:gray-200">
@lang('wirechat::chats.labels.load_more')
</button>
<div wire:loading wire:target="loadMore">
<x-wirechat::loading-spin />
</div>
</section>

View File

@@ -0,0 +1,5 @@
<div x-cloak wire:loading.delay.class.remove="hidden"
wire:target="search"class="hidden transition-all duration-300 ">
<x-wirechat::loading-spin />
</div>

View File

@@ -0,0 +1,35 @@
<div class="flex gap-x-2 items-center">
{{-- Only show if AUTH is onwer of message --}}
@if ($lastMessage?->ownedBy($this->auth))
<span class="font-bold text-xs dark:text-white/90 dark:font-normal">
@lang('wirechat::chats.labels.you'):
</span>
@elseif(!$lastMessage?->ownedBy($this->auth) && $group !== null)
<span class="font-bold text-xs dark:text-white/80 dark:font-normal">
{{ $lastMessage->sendable?->display_name }}:
</span>
@endif
<p @class([
'truncate text-sm dark:text-white gap-2 items-center',
'font-semibold text-black' =>
!$isReadByAuth && !$lastMessage?->ownedBy($this->auth),
'font-normal text-gray-600' =>
$isReadByAuth && !$lastMessage?->ownedBy($this->auth),
'font-normal text-gray-600' =>
$isReadByAuth && $lastMessage?->ownedBy($this->auth),
])>
{{ $lastMessage->body != '' ? $lastMessage->body : ($lastMessage->isAttachment() ? '📎 '.__('wirechat::chats.labels.attachment') : '') }}
</p>
<span class="font-medium px-1 text-xs shrink-0 text-gray-800 dark:text-gray-50">
@if ($lastMessage->created_at->diffInMinutes(now()) < 1)
@lang('wirechat::chats.labels.now')
@else
{{ $lastMessage->created_at->shortAbsoluteDiffForHumans() }}
@endif
</span>
</div>

View File

@@ -0,0 +1,206 @@
<div>
<script>
window.WireChatModal = () => {
return {
show: false,
showActiveModalComponent: true,
activeModalComponent: false,
componentHistory: [],
listeners: [],
closeOnEscape: false,
closeOnEscapeIsForceful: false,
dispatchCloseEvent: false,
destroyOnClose: false,
closeOnClickAway:false,
closeModalOnEscape(trigger) {
///Only proceed if the trigger is for ChatModal
if (trigger.modalType != 'WireChatModal'){ return;}
//check if canCloseOnEsp
if (this.closeOnEscape === false) { return; }
if (!this.closingModal('closingModalOnEscape')) { return; }
//check if should also close all children modal when this current on is closed
const force = this.closeOnEscapeIsForceful === true;
this.closeModal(force);
},
closeModalOnClickAway(trigger) {
if (this.closeOnClickAway === false) {
return;
}
if (!this.closingModal('closingModalOnClickAway')) {
return;
}
this.closeModal(true);
},
closingModal(eventName) {
const componentName = this.$wire.get('components')[this.activeModalComponent].name;
var params = {
id: this.activeModalComponent,
closing: true,
};
Livewire.dispatchTo(componentName, eventName, params);
return params.closing;
},
closeModal(force = false, skipPreviousModals = 0, destroySkipped = false) {
if (this.show === false) {
return;
}
if (this.dispatchCloseEvent === true) {
const componentName = this.$wire.get('components')[this.activeModalComponent].name;
Livewire.dispatch('wireChatModalClosed', {
name: componentName
});
}
//Check if should completley destroy component on close
//Meaning state won't be retained if component is opened again
if (this.destroyOnClose === true) {
Livewire.dispatch('destroyWireChatModal', {
id: this.activeModalComponent
});
}
const id = this.componentHistory.pop();
if (id && !force) {
if (id) {
this.setActiveModalComponent(id, true);
} else {
this.setShowPropertyTo(false);
}
} else {
this.setShowPropertyTo(false);
}
},
setActiveModalComponent(id, skip = false) {
this.setShowPropertyTo(true);
if (this.activeModalComponent === id) {
return;
}
if (this.activeModalComponent !== false && skip === false) {
this.componentHistory.push(this.activeModalComponent);
}
let focusableTimeout = 50;
if (this.activeModalComponent === false) {
this.activeModalComponent = id
this.showActiveModalComponent = true;
} else {
this.showActiveModalComponent = false;
focusableTimeout = 400;
setTimeout(() => {
this.activeModalComponent = id;
this.showActiveModalComponent = true;
}, 300);
}
const attributes = this.$wire.get('components')[id]?.modalAttributes || {};
this.closeOnEscape = attributes.closeOnEscape ?? false;
this.closeOnEscapeIsForceful = attributes.closeOnEscapeIsForceful ?? false;
this.dispatchCloseEvent = attributes.dispatchCloseEvent ?? false;
this.destroyOnClose = attributes.destroyOnClose ?? false;
this.closeOnClickAway = attributes.closeOnClickAway ?? false;
this.$nextTick(() => {
let focusable = this.$refs[id]?.querySelector('[autofocus]');
if (focusable) {
setTimeout(() => {
focusable.focus();
}, focusableTimeout);
}
});
},
setShowPropertyTo(show) {
this.show = show;
if (show) {
document.body.classList.add('overflow-y-hidden');
} else {
document.body.classList.remove('overflow-y-hidden');
setTimeout(() => {
this.activeModalComponent = false;
this.$wire.resetState();
}, 300);
}
},
init() {
this.listeners.push(
Livewire.on('closeWireChatModal', (data) => {
this.closeModal(data?.force ?? false, data?.skipPreviousModals ?? 0, data
?.destroySkipped ?? false);
})
);
this.listeners.push(
Livewire.on('activeWireChatModalComponentChanged', ({
id
}) => {
this.setActiveModalComponent(id);
})
);
},
destroy() {
this.listeners.forEach((listener) => {
listener();
});
}
};
}
</script>
<div x-data="WireChatModal()" x-on:close.stop="setShowPropertyTo(false)"
x-on:keydown.escape.stop="closeModalOnEscape({modalType: 'WireChatModal', event: $event })"
tabindex="0"
x-show="show" class="fixed inset-0 z-50 overflow-y-auto" style="display: none;">
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-10 text-center sm:block sm:p-0">
<div x-show="show" x-on:click="closeModalOnClickAway()" x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0" class="fixed inset-0 transition-all transform">
<div class="absolute inset-0 bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] opacity-75"></div>
</div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen " aria-hidden="true">&#8203;</span>
<div x-show="show && showActiveModalComponent"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
class="inline-block align-middle rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 w-full sm:max-w-lg"
id="chat-dialog-container" x-trap.noscroll="show && showActiveModalComponent" aria-modal="true">
@forelse($components as $id => $component)
<div x-show.immediate="activeModalComponent == '{{ $id }}'" x-ref="{{ $id }}"
wire:key="{{ $id }}">
@livewire($component['name'], $component['arguments'], key($id))
</div>
@empty
@endforelse
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,95 @@
@use('Namu\WireChat\Facades\WireChat')
<div id="new-chat-modal ">
<div
class="relative w-full border mx-auto border-[var(--wc-light-secondary)] dark:border-[var(--wc-dark-secondary)] overflow-visible bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] dark:text-white p-12 sm:max-w-lg sm:rounded-lg">
<header class=" sticky top-0 bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] z-10 py-2">
<div class="flex justify-between items-center justify-between pb-2">
<h3 class="text-lg font-semibold">{{__('wirechat::new.chat.labels.heading') }}</h3>
<x-wirechat::actions.close-modal>
<button
dusk="close_modal_button"
class="p-2 text-gray-600 hover:bg-[var(--wc-light-secondary)] dark:hover:bg-[var(--wc-dark-secondary)] dark:hover:text-white rounded-full hover:text-gray-800 ">
<svg class="w-5 h-5 cursor-pointer" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke-width="1.2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</x-wirechat::actions.close-modal>
</div>
<section class="flex flex-wrap items-center px-0 border-b border-[var(--wc-light-border)] dark:border-[var(--wc-dark-border)]">
<input dusk="search_users_field" autofocus type="search" id="users-search-field"
wire:model.live.debounce='search' autocomplete="off" placeholder="{{__('wirechat::new.chat.inputs.search.placeholder')}}"
class=" w-full border-0 w-auto px-0 dark:bg-[var(--wc-dark-primary)] outline-hidden focus:outline-hidden bg-[var(--wc-light-primary)] rounded-lg focus:ring-0 hover:ring-0">
</section>
</header>
<div class="relative w-full">
{{-- New Group button --}}
@if (WireChat::showNewGroupModalButton() && auth()->user()->canCreateGroups())
{{-- Buton to trigger opening of new grop modal --}}
<x-wirechat::actions.new-group widget="{{$this->isWidget()}}">
<button @dusk="open_new_group_modal_button" class="flex items-center gap-3 my-4 rounded-lg p-2 w-full border transition-colors border-[var(--wc-light-border)] dark:border-[var(--wc-dark-border)] hover:border-[var(--wc-light-secondary)] dark:hover:border-[var(--wc-dark-secondary)]" >
<span style=" color: var(--wc-brand-primary); " class="p-1 bg-gray-100 rounded-full ">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class=" w-5 h-5">
<path fill-rule="evenodd" d="M8.25 6.75a3.75 3.75 0 1 1 7.5 0 3.75 3.75 0 0 1-7.5 0ZM15.75 9.75a3 3 0 1 1 6 0 3 3 0 0 1-6 0ZM2.25 9.75a3 3 0 1 1 6 0 3 3 0 0 1-6 0ZM6.31 15.117A6.745 6.745 0 0 1 12 12a6.745 6.745 0 0 1 6.709 7.498.75.75 0 0 1-.372.568A12.696 12.696 0 0 1 12 21.75c-2.305 0-4.47-.612-6.337-1.684a.75.75 0 0 1-.372-.568 6.787 6.787 0 0 1 1.019-4.38Z" clip-rule="evenodd" />
<path d="M5.082 14.254a8.287 8.287 0 0 0-1.308 5.135 9.687 9.687 0 0 1-1.764-.44l-.115-.04a.563.563 0 0 1-.373-.487l-.01-.121a3.75 3.75 0 0 1 3.57-4.047ZM20.226 19.389a8.287 8.287 0 0 0-1.308-5.135 3.75 3.75 0 0 1 3.57 4.047l-.01.121a.563.563 0 0 1-.373.486l-.115.04c-.567.2-1.156.349-1.764.441Z" />
</svg>
</span>
<p class="dark:text-white">@lang('wirechat::new.chat.actions.new_group.label')</p>
</button>
</x-wirechat::actions.new-group>
@endif
{{-- <h5 class="text font-semibold text-gray-800 dark:text-gray-100">Recent Chats</h5> --}}
<section class="my-4 grid">
@if (count($users)!=0)
<ul class="overflow-auto flex flex-col">
@foreach ($users as $key => $user)
<li wire:key="user-{{ $key }}"
wire:click="createConversation('{{ $user->id }}',{{ json_encode(get_class($user)) }})"
class="flex cursor-pointer group gap-2 items-center p-2">
<x-wirechat::avatar :src="$user->cover_url" class="w-10 h-10" />
<div class="flex flex-col">
<p class="group-hover:underline transition-all">
{{ $user->display_name }}</p>
@php
$location = $user->getLocationFirst();
@endphp
@if ($location && isset($location['name_short']))
<span class="text-sm text-gray-600 dark:text-gray-400">
{{ $location['name_short'] }}
</span>
@endif
</div>
</li>
@endforeach
</ul>
@else
@if (!empty($search))
<span class="m-auto">@lang('wirechat::new.chat.messages.empty_search_result')</span>
@endif
@endif
</section>
</div>
</div>
</div>

View File

@@ -0,0 +1,253 @@
<div x-data dusk="new_group_modal">
<div
class="relative w-full border items-center justify-center border-[var(--wc-light-border)] dark:border-[var(--wc-dark-border)] overflow-visible bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] dark:text-white sm:max-w-lg sm:rounded-lg">
{{-- Group Details --}}
<section x-show="$wire.showAddMembers==false"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 -translate-x-full" x-transition:enter-end="opacity-100 translate-x-0"
x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100 translate-x-0"
x-transition:leave-end="opacity-0 -translate-x-full"
>
<form wire:submit="validateDetails()" class="flex flex-col p-12">
<header>
<div class="flex gap-10 w-full">
@if ($photo)
<div class="relative w-28 h-28 overflow-clip rounded-full">
<x-wirechat::avatar :src="$photo->temporaryUrl()" class="w-28 h-28" />
<button
type="button"
class="bottom-0 inset-x-0 bg-white/40 text-red-800 flex items-center justify-center absolute "
wire:click="deletePhoto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="size-6 w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</button>
</div>
@else
<label class="cursor-pointer">
<x-wirechat::avatar wire:loading.class="animate-pulse" wire:target="photo" :group="true" class="w-28 h-28" />
<input wire:model="photo" dusk="add_photo_field" type="file" hidden accept=".jpg,.jpeg,.png,.webp">
</label>
@endif
<div class=" my-auto">
<label for="name">@lang('wirechat::new.group.inputs.name.label')</label>
<input id='name' type="text" wire:model='name' autofocus placeholder="{{__('wirechat::new.group.inputs.name.placeholder') }}"
class=" w-full border-0 px-0 bg-inherit dark:text-white outline-hidden w-full focus:outline-hidden focus:ring-0 hover:ring-0">
<span class="text-red-500 text-sm ">
@error('name')
{{ $message }}
@enderror
</span>
</div>
</div>
<span class="text-red-500 text-sm ">
@error('photo')
{{ $message }}
@enderror
</span>
</header>
<main class="my-5">
<div class=" my-auto flex flex-col gap-y-2">
<label class="my-2" for="description">@lang('wirechat::new.group.inputs.description.label')</label>
<textarea id='description' type="text" wire:model='description' placeholder="{{__('wirechat::new.group.inputs.description.placeholder')}}" rows="4"
class=" w-full resize-none rounded-lg border-[var(--wc-light-border)] dark:border-[var(--wc-dark-border)] bg-inherit dark:text-white outline-hidden w-full focus:outline-hidden focus:ring-0 hover:ring-0">
</textarea>
<span class="text-red-500 text-sm ">
@error('description')
{{ $message }}
@enderror
</span>
</div>
</main>
<footer class="flex gap-4 justify-end mt-auto">
<x-wirechat::actions.close-modal>
<x-jetstream.secondary-button type="button" dusk="cancel_create_new_group_button">
@lang('wirechat::new.group.actions.cancel.label')
</x-jetstream.secondary-button>
</x-wirechat::actions.close-modal>
<x-jetstream.button type="submit" x-bind:disabled="!$wire.name || !$wire.name.trim().length" dusk="next_button">
@lang('wirechat::new.group.actions.next.label')
</x-jetstream.button>
</footer>
</form>
</section>
{{-- Add members --}}
<section dusk="add_members_section" x-cloak x-show="$wire.showAddMembers==true"
x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 translate-x-full"
x-transition:enter-end="opacity-100 translate-x-0" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-x-0" x-transition:leave-end="opacity-0 translate-x-full"
class="p-12 relative h-full overflow-x-hidden ">
<header class=" sticky top-0 bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] z-10 py-2">
<div class="flex items-center pb-2">
<button @click="$wire.showAddMembers=false"
class="p-2 ml-0 text-gray-600 dark:hover:bg-[var(--wc-dark-secondary)] dark:hover:text-white rounded-full hover:text-gray-800 hover:bg-[var(--wc-light-secondary)]">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class=" w-5 w-5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>
</button>
<h3 class="text-sm mx-auto font-semibold "><span>@lang('wirechat::new.group.labels.add_members')</span> {{count($selectedMembers)}} / {{$maxGroupMembers}}</h3>
<x-jetstream.button
wire:click="create"
wire:loading.attr="disabled"
wire:target='create'
type="button"
class="text-xs py-1.5 px-3">
@lang('wirechat::new.group.actions.create.label')
</x-jetstream.button>
</div>
{{-- Member limit error --}}
<div
x-data="{ showError:false }"
x-on:show-member-limit-error.window="
showError=true;
setTimeout(()=>{ showError=false; },1500);
"
class="text-red-500 text-sm mx-auto ">
<span x-transition x-show="showError">
@lang('wirechat::new.group.messages.members_limit_error',['count'=>$maxGroupMembers])
</span>
</div>
{{-- Search input --}}
<section class="flex flex-wrap items-center px-0 border-b border-[var(--wc-light-secondary)] dark:border-[var(--wc-dark-secondary)]">
<input type="search" id="users-search-field" wire:model.live.debounce='search' autocomplete="off"
placeholder="{{__('wirechat::new.group.inputs.search.placeholder')}}"
class=" w-full border-0 w-auto dark:bg-[var(--wc-dark-primary)] outline-hidden focus:outline-hidden bg-[var(--wc-light-primary)] bg-none rounded-lg focus:ring-0 hover:ring-0">
</section>
<section class=" overflow-x-hidden my-2 ">
<ul
style="
-ms-overflow-style: none;
scrollbar-width: none;
"
class="flex w-full overflow-x-auto gap-3">
@if ($selectedMembers)
@foreach ($selectedMembers as $key => $member)
<li class="flex items-center text-nowrap min-w-fit px-2 py-1 text-sm font-medium text-gray-800 bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)] rounded-sm dark:text-gray-300"
wire:key="selected-member-{{ $member->id }}">
{{ $member->display_name }}
<button type="button"
wire:click="toggleMember('{{ $member->id }}',{{ json_encode(get_class($member)) }})"
class="flex items-center p-1 ms-2 text-sm text-gray-400 bg-transparent rounded-xs hover:bg-[var(--wc-light-secondary)] dark:hover:bg-[var(--wc-dark-secondary)] hover:text-gray-900 dark:hover:text-gray-300"
aria-label="Remove">
<svg class="w-2 h-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Remove badge</span>
</button>
</li>
@endforeach
@endif
</ul>
</section>
</header>
{{-- Search --}}
<div class="relative w-full">
{{-- <h5 class="text font-semibold text-gray-800 dark:text-gray-100">Recent Chats</h5> --}}
<section class="my-4 grid">
@if (count($users)!=0)
<ul class="overflow-auto flex flex-col">
@foreach ($users as $key => $user)
<li class="flex cursor-pointer group gap-2 items-center p-2">
<label
wire:click="toggleMember('{{ $user->id }}',{{ json_encode(get_class($user)) }})"
class="flex cursor-pointer gap-2 items-center w-full">
<x-wirechat::avatar src="{{ $user->cover_url }}" class="w-10 h-10" />
<div class="flex flex-col flex-1 min-w-0">
<p class="group-hover:underline transition-all truncate">
{{ $user->display_name }}</p>
@php
$location = $user->getLocationFirst();
@endphp
@if ($location && isset($location['name_short']))
<span class="text-sm text-gray-600 dark:text-gray-400">
{{ $location['name_short'] }}
</span>
@endif
</div>
<div class="ml-auto">
@if ($selectedMembers->contains(fn($member) => $member->id == $user->id && get_class($member) == get_class($user)))
<div class="w-6 h-6 bg-theme-brand rounded flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" class="w-4 h-4 text-theme-background">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</div>
@else
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="currentColor" class="bi bi-plus-square-dotted w-6 h-6 text-gray-400"
viewBox="0 0 16 16">
<path
d="M2.5 0q-.25 0-.487.048l.194.98A1.5 1.5 0 0 1 2.5 1h.458V0zm2.292 0h-.917v1h.917zm1.833 0h-.917v1h.917zm1.833 0h-.916v1h.916zm1.834 0h-.917v1h.917zm1.833 0h-.917v1h.917zM13.5 0h-.458v1h.458q.151 0 .293.029l.194-.981A2.5 2.5 0 0 0 13.5 0m2.079 1.11a2.5 2.5 0 0 0-.69-.689l-.556.831q.248.167.415.415l.83-.556zM1.11.421a2.5 2.5 0 0 0-.689.69l.831.556c.11-.164.251-.305.415-.415zM16 2.5q0-.25-.048-.487l-.98.194q.027.141.028.293v.458h1zM.048 2.013A2.5 2.5 0 0 0 0 2.5v.458h1V2.5q0-.151.029-.293zM0 3.875v.917h1v-.917zm16 .917v-.917h-1v.917zM0 5.708v.917h1v-.917zm16 .917v-.917h-1v.917zM0 7.542v.916h1v-.916zm15 .916h1v-.916h-1zM0 9.375v.917h1v-.917zm16 .917v-.917h-1v.917zm-16 .916v.917h1v-.917zm16 .917v-.917h-1v.917zm-16 .917v.458q0 .25.048.487l.98-.194A1.5 1.5 0 0 1 1 13.5v-.458zm16 .458v-.458h-1v.458q0 .151-.029.293l.981.194Q16 13.75 16 13.5M.421 14.89c.183.272.417.506.69.689l.556-.831a1.5 1.5 0 0 1-.415-.415zm14.469.689c.272-.183.506-.417.689-.69l-.831-.556c-.11.164-.251.305-.415.415l.556.83zm-12.877.373Q2.25 16 2.5 16h.458v-1H2.5q-.151 0-.293-.029zM13.5 16q.25 0 .487-.048l-.194-.98A1.5 1.5 0 0 1 13.5 15h-.458v1zm-9.625 0h.917v-1h-.917zm1.833 0h.917v-1h-.917zm1.834-1v1h.916v-1zm1.833 1h.917v-1h-.917zm1.833 0h.917v-1h-.917zM8.5 4.5a.5.5 0 0 0-1 0v3h-3a.5.5 0 0 0 0 1h3v3a.5.5 0 0 0 1 0v-3h3a.5.5 0 0 0 0-1h-3z" />
</svg>
@endif
</div>
</label>
</li>
@endforeach
</ul>
@else
@if (!empty($search))
<span class="m-auto">@lang('wirechat::new.group.messages.empty_search_result')</span>
@endif
@endif
</section>
</div>
</section>
</div>
</div>

View File

@@ -0,0 +1,12 @@
<div class="w-full flex min-h-full h-full rounded-lg" >
<div class=" hidden md:grid bg-inherit border-r border-[var(--wc-light-border)] dark:border-[var(--wc-dark-border)] dark:bg-inherit relative w-full h-full md:w-[360px] lg:w-[400px] xl:w-[500px] shrink-0 overflow-y-auto ">
<livewire:wirechat.chats
:showHomeRouteButton="false"
/>
</div>
<main class=" grid w-full grow h-full min-h-min relative overflow-y-auto" style="contain:content">
<livewire:wirechat.chat conversation="{{$this->conversation->id}}"/>
</main>
</div>

View File

@@ -0,0 +1,13 @@
<div class="w-full h-full min-h-full flex rounded-lg" >
<div class="relative w-full h-full border-r border-[var(--wc-light-border)] dark:border-[var(--wc-dark-border)] md:w-[360px] lg:w-[400px] xl:w-[500px] shrink-0 overflow-y-auto ">
<livewire:wirechat.chats/>
</div>
<main class="hidden md:grid h-full min-h-full w-full bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] h-full relative overflow-y-auto" style="contain:content">
<div class="m-auto text-center justify-center flex gap-3 flex-col items-center col-span-12">
<h4 class="font-medium p-2 px-3 rounded-full font-semibold bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)] dark:text-white dark:font-normal">@lang('wirechat::pages.chat.messages.welcome')</h4>
</div>
</main>
</div>

View File

@@ -0,0 +1,233 @@
<div class="h-full ">
@assets
<script>
window.ChatWidget = () => {
return {
show: false,
showActiveComponent: true,
activeWidgetComponent: false,
componentHistory: [],
listeners: [],
//current component attributes
closeOnEscape: false,
closeOnEscapeIsForceful: false,
dispatchCloseEvent: false,
destroyOnClose: false,
closeModalOnClickAway:false,
closeChatWidgetOnEscape(trigger) {
///Only proceed if the trigger is for ChatDrawer
if (trigger.modalType !== 'ChatWidget') {
return;
}
//check if canCloseOnEsp
if (this.closeOnEscape === false) {
return;
}
//Fire closingModalOnEscape:event to parent
if (!this.closingModal('closingModalOnEscape')) {
return;
}
//check if should also close all children modal when this current on is closed
const force = this.closeOnEscapeIsForceful === true;
this.closeWidget(force);
},
closingModal(eventName) {
const componentName = this.$wire.get('widgetComponents')[this.activeWidgetComponent].name;
var params = {
id: this.activeWidgetComponent,
closing: true,
};
Livewire.dispatchTo(componentName, eventName, params);
return params.closing;
},
closeWidget(force = false, skipPreviousModals = 0, destroySkipped = false) {
if (this.show === false) {
return;
}
//Check if should completley destroy component on close
//Meaning state won't be retained if component is opened again
if (this.destroyOnClose === true) {
Livewire.dispatch('destroyChatWidget', {
id: this.activeWidgetComponent
});
}
const id = this.componentHistory.pop();
if (id && !force) {
if (id) {
this.setActiveWidgetComponent(id, true);
} else {
this.setShowPropertyTo(false);
}
} else {
this.setShowPropertyTo(false);
}
},
setActiveWidgetComponent(id, skip = false) {
this.setShowPropertyTo(true);
// this.closeWidget(true);
if (this.activeWidgetComponent === id) {
return;
}
if (this.activeWidgetComponent !== false && skip === false) {
this.componentHistory.push(this.activeWidgetComponent);
}
let focusableTimeout = 50;
if (this.activeWidgetComponent === false) {
this.activeWidgetComponent = id
this.showActiveComponent = true;
} else {
this.showActiveComponent = false;
focusableTimeout = 400;
setTimeout(() => {
this.activeWidgetComponent = id;
this.showActiveComponent = true;
}, 300);
}
// Fetch modal attributes and set Alpine properties
const attributes = this.$wire.get('widgetComponents')[id]?.modalAttributes || {};
this.closeOnEscape = attributes.closeOnEscape ?? false;
this.closeOnEscapeIsForceful = attributes.closeOnEscapeIsForceful ?? false;
this.dispatchCloseEvent = attributes.dispatchCloseEvent ?? false;
this.destroyOnClose = attributes.destroyOnClose ?? false;
this.closeModalOnClickAway = attributes.closeModalOnClickAway ?? false;
this.$nextTick(() => {
let focusable = this.$refs[id]?.querySelector('[autofocus]');
if (focusable) {
setTimeout(() => {
focusable.focus();
}, focusableTimeout);
}
});
},
setShowPropertyTo(show) {
this.show = show;
if (show) {
document.body.classList.add('overflow-y-hidden');
} else {
document.body.classList.remove('overflow-y-hidden');
setTimeout(() => {
this.activeWidgetComponent = false;
this.$wire.resetState();
//Notify listeners that chat is
}, 300);
const conversation = this.$wire.selectedConversationId;
Livewire.dispatch('chat-closed', {
conversation:conversation
});
}
},
init() {
/*! Changed the event to closeChatWidget in order to not interfere with the main modal */
this.listeners.push(Livewire.on('closeChatWidget', (data) => { this.closeWidget(data?.force ?? false, data?.skipPreviousModals ?? 0, data ?.destroySkipped ?? false); }));
/*! Changed listener name to activeChatWidgetComponentChanged to not interfer with main modal*/
this.listeners.push(Livewire.on('activeChatWidgetComponentChanged', ({id}) => {
this.setActiveWidgetComponent(id);
}));
},
destroy() {
this.listeners.forEach((listener) => {
listener();
});
}
};
}
</script>
@endassets
<div
x-data="{
selectedConversationId:null,
get chatIsOpen(){
return $wire.selectedConversationId !==null;
}
}"
class ='w-full h-full bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] border border-[var(--wc-light-secondary)] dark:border-[var(--wc-dark-secondary)] flex overflow-hidden rounded-lg'>
<div :class="chatIsOpen && 'hidden md:grid'" class="relative w-full h-full sm:border-r border-[var(--wc-light-border)] dark:border-[var(--wc-dark-border)] md:w-[360px] lg:w-[400px] xl:w-[450px] shrink-0 overflow-y-auto ">
<livewire:wirechat.chats :widget="true" />
</div>
<main
x-data="ChatWidget()"
x-on:open-chat.window="$wire.selectedConversationId= $event.detail.conversation;"
x-on:close-chat.stop.window="setShowPropertyTo(false)"
x-on:keydown.escape.stop.window="closeChatWidgetOnEscape({ modalType: 'ChatWidget', event: $event });"
aria-modal="true"
tabindex="0"
class="w-full h-full min-h-full grid relative grow focus:outline-hidden focus:border-none"
:class="!chatIsOpen && 'hidden md:grid'"
style="contain:content;">
<div
x-cloak
x-show="show && showActiveComponent" x-transition:enter="ease-out duration-100"
x-transition:enter-start="opacity-0 -translate-x-full" x-transition:enter-end="opacity-100 translate-x-0"
x-transition:leave="ease-in duration-100 " x-transition:leave-start="opacity-100 translate-x-0"
x-transition:leave-end="opacity-0 -translate-x-full"
class="fixed inset-0" id="chatwidget-container"
aria-modal="true">
@forelse($widgetComponents as $id => $component)
<div x-show.immediate="activeWidgetComponent == '{{ $id }}'" x-ref="{{ $id }}"
wire:key="key-{{$id }}" class="h-full">
@livewire($component['name'], ['conversation'=> $component['conversation'] ,'widget'=>true], key($id))
</div>
@empty
@endforelse
</div>
<div x-show="!show && !chatIsOpen " class="m-auto justify-center flex gap-3 flex-col items-center ">
<h4 class="font-medium p-2 px-3 rounded-full font-semibold bg-[var(--wc-light-secondary)] dark:bg-[var(--wc-dark-secondary)] dark:text-white dark:font-normal">@lang('wirechat::widgets.wirechat.messages.welcome')</h4>
</div>
</main>
</div>
</div>