Snapshot of main at 9f39a6e1e (2026-11-02)
This commit is contained in:
commit
6ccf1792d7
2580 files changed
+1040202
No files matched your search
BIN
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
|
||||
<div onclick="Livewire.dispatch('closeWireChatModal')">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
@props([
|
||||
'widget' => false
|
||||
])
|
||||
|
||||
|
||||
<x-wirechat::actions.open-modal
|
||||
component="wirechat.new.chat"
|
||||
:widget="$widget"
|
||||
>
|
||||
{{$slot}}
|
||||
</x-wirechat::actions.open-modal>
|
||||
@@ -0,0 +1,11 @@
|
||||
@props([
|
||||
'widget' => false
|
||||
])
|
||||
|
||||
|
||||
<x-wirechat::actions.open-modal
|
||||
component="wirechat.new.group"
|
||||
:widget="$widget"
|
||||
>
|
||||
{{$slot}}
|
||||
</x-wirechat::actions.open-modal>
|
||||
+16
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,134 @@
|
||||
|
||||
@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';
|
||||
const bodyText = message.body || (message.attachment ? '📎 {{ __("Attachment") }}' : '{{ __("New message") }}');
|
||||
let body = bodyText;
|
||||
let icon = message.sendable?.cover_url;
|
||||
|
||||
if (message.conversation.type == 'group') {
|
||||
title = message.conversation?.group?.name;
|
||||
body = message.sendable?.display_name + ': ' + bodyText;
|
||||
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.notifications.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
|
||||
@@ -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>
|
||||
@@ -0,0 +1,78 @@
|
||||
|
||||
@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(){
|
||||
{{-- The window resize listener outlives popovers removed by a Livewire re-render --}}
|
||||
if(!this.$refs.popoverButton) return;
|
||||
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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,126 @@
|
||||
<!-- WIRECHAT LAYOUT LOADED -->
|
||||
<!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, interactive-widget=resizes-content">
|
||||
<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 h-screen" style="height: 100dvh;">
|
||||
|
||||
<x-jetstream.banner />
|
||||
<x-jetstream.toaster />
|
||||
|
||||
<div class="flex flex-col flex-1 min-h-0 bg-gray-100">
|
||||
@livewire('navigation-menu')
|
||||
<x-notifications position="bottom-end" />
|
||||
|
||||
|
||||
<header class="shrink-0 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">
|
||||
@auth
|
||||
@if(userHasMultipleProfiles() && session('activeProfileName'))
|
||||
@php
|
||||
$translatedRole = session('activeProfileRoleLabel') ? __(session('activeProfileRoleLabel')) : null;
|
||||
$isUserProfile = $translatedRole === __('User');
|
||||
@endphp
|
||||
{{ __('Chat messenger') }}: {{ session('activeProfileName') }}@if($translatedRole && !$isUserProfile) <span class="hidden lg:inline text-sm text-gray-400 font-normal">({{ $translatedRole }})</span>@endif
|
||||
@else
|
||||
{{ __('Chat messenger') }}
|
||||
@endif
|
||||
@else
|
||||
{{ __('Chat messenger') }}
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex flex-col flex-1 min-h-0 bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)]">
|
||||
<!-- Page Content -->
|
||||
<main class="flex flex-col flex-1 min-h-0 max-w-7xl mx-auto overflow-hidden bg-white shadow-xl w-full">
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@if (session('profile-switched-notification'))
|
||||
@livewire('notify-switch-profile')
|
||||
@endif
|
||||
|
||||
@auth
|
||||
@livewire('account-info-modal')
|
||||
@endauth
|
||||
@livewireScripts
|
||||
@wirechatAssets
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,434 @@
|
||||
{{-- 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>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const css = `
|
||||
.nav-emoji {
|
||||
filter: grayscale(1) opacity(0.5) brightness(0.8);
|
||||
border-radius: 50%;
|
||||
font-size: var(--category-emoji-size, 1rem) !important;
|
||||
transform: scale(0.82);
|
||||
}
|
||||
.nav-button[aria-selected="true"] .nav-emoji {
|
||||
filter: none;
|
||||
box-shadow: 0 0 0 2px var(--input-border-color);
|
||||
}
|
||||
.indicator-wrapper { display: none !important; }
|
||||
`;
|
||||
|
||||
function injectInto(picker) {
|
||||
if (!picker || !picker.shadowRoot) return;
|
||||
const existing = picker.shadowRoot.querySelector('style[data-wc-nav-circles]');
|
||||
if (existing) existing.remove();
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.setAttribute('data-wc-nav-circles', '');
|
||||
styleEl.textContent = css;
|
||||
picker.shadowRoot.appendChild(styleEl);
|
||||
}
|
||||
|
||||
function watchPicker(picker) {
|
||||
// If nav is already rendered, inject immediately
|
||||
if (picker.shadowRoot && picker.shadowRoot.querySelector('.nav-emoji')) {
|
||||
injectInto(picker);
|
||||
return;
|
||||
}
|
||||
// Otherwise observe the shadowRoot for the nav to appear
|
||||
const observer = new MutationObserver(() => {
|
||||
if (picker.shadowRoot && picker.shadowRoot.querySelector('.nav-emoji')) {
|
||||
observer.disconnect();
|
||||
injectInto(picker);
|
||||
}
|
||||
});
|
||||
if (picker.shadowRoot) {
|
||||
observer.observe(picker.shadowRoot, { childList: true, subtree: true });
|
||||
}
|
||||
// Also re-check once the custom element is defined and upgraded
|
||||
customElements.whenDefined('emoji-picker').then(() => {
|
||||
if (picker.shadowRoot && picker.shadowRoot.querySelector('.nav-emoji')) {
|
||||
observer.disconnect();
|
||||
injectInto(picker);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setup() {
|
||||
// Watch for emoji-picker elements added to the document
|
||||
const domObserver = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node.nodeType !== 1) continue;
|
||||
if (node.tagName === 'EMOJI-PICKER') watchPicker(node);
|
||||
node.querySelectorAll && node.querySelectorAll('emoji-picker').forEach(watchPicker);
|
||||
}
|
||||
}
|
||||
});
|
||||
domObserver.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// Handle any pickers already in the DOM
|
||||
document.querySelectorAll('emoji-picker').forEach(watchPicker);
|
||||
}
|
||||
|
||||
if (document.body) {
|
||||
setup();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', setup);
|
||||
}
|
||||
|
||||
window.injectEmojiPickerNavIcons = injectInto;
|
||||
})();
|
||||
</script>
|
||||
|
||||
@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;
|
||||
}
|
||||
}"
|
||||
|
||||
@load-emoji-picker.window="loadEmojiPicker()"
|
||||
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 (textarea uses x-model='body', not wire:model)
|
||||
let messageInputs = document.querySelectorAll(
|
||||
'textarea[x-model="body"], ' +
|
||||
'textarea[x-model=\'body\'], ' +
|
||||
'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();
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
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.ctrlKey || e.metaKey)) {
|
||||
// Ctrl/Cmd+Enter sends the message; a plain Enter is a new line.
|
||||
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>
|
||||
|
||||
{{-- iOS Safari keyboard fix: adjust chat height when virtual keyboard appears --}}
|
||||
<script>
|
||||
(function() {
|
||||
if (!window.visualViewport) return;
|
||||
|
||||
var chatRoot = document.querySelector('[wire\\:id]');
|
||||
// Walk up to find the chat container that fills the screen
|
||||
var chatContainer = document.querySelector('.flex.flex-col.grow.h-full');
|
||||
|
||||
function onViewportResize() {
|
||||
var vv = window.visualViewport;
|
||||
var offsetFromBottom = window.innerHeight - (vv.offsetTop + vv.height);
|
||||
if (offsetFromBottom > 50) {
|
||||
// Keyboard is open: push footer up by the keyboard height
|
||||
document.documentElement.style.setProperty('--wc-keyboard-offset', offsetFromBottom + 'px');
|
||||
// Scroll chat body to bottom so last message stays visible
|
||||
var conversation = document.getElementById('conversation');
|
||||
if (conversation) conversation.scrollTop = conversation.scrollHeight;
|
||||
} else {
|
||||
document.documentElement.style.setProperty('--wc-keyboard-offset', '0px');
|
||||
}
|
||||
}
|
||||
|
||||
window.visualViewport.addEventListener('resize', onViewportResize);
|
||||
window.visualViewport.addEventListener('scroll', onViewportResize);
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,176 @@
|
||||
<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'
|
||||
x-bind:disabled="$wire.selectedMembers.length === 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>
|
||||
|
||||
{{-- Add all linked members button (shown for Organization, Bank, Admin profiles) --}}
|
||||
@php $activeProfileType = getActiveProfileType(); @endphp
|
||||
@if(in_array($activeProfileType, ['Organization', 'Bank', 'Admin']))
|
||||
<div class="my-2">
|
||||
<button type="button"
|
||||
wire:click="addLinkedMembers"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="addLinkedMembers"
|
||||
class="flex items-center gap-3 text-xs text-theme-primary hover:text-theme-brand hover:underline focus:outline-none disabled:opacity-50">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z" />
|
||||
</svg>
|
||||
<span wire:loading.remove wire:target="addLinkedMembers">
|
||||
{{ __('Add all linked users of this :type', ['type' => __(strtolower($activeProfileType))]) }}
|
||||
</span>
|
||||
<span wire:loading wire:target="addLinkedMembers">
|
||||
{{ __('Adding...') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<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 class="flex flex-col gap-1">
|
||||
|
||||
@if ($selectedMembers)
|
||||
|
||||
@foreach ($selectedMembers as $key => $member)
|
||||
<li class="flex items-center justify-between 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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,207 @@
|
||||
@php
|
||||
$authIsAdminInGroup= $participant?->isAdmin();
|
||||
$authIsOwner= $participant?->isOwner();
|
||||
$isGroup= $conversation?->isGroup();
|
||||
$activeProfileType = getActiveProfileType();
|
||||
$activeProfile = getActiveProfile();
|
||||
$showSyncButton = ($authIsAdminInGroup || $authIsOwner)
|
||||
&& in_array($activeProfileType, ['Organization', 'Bank', 'Admin'])
|
||||
&& $activeProfile !== null;
|
||||
@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 ">
|
||||
|
||||
{{-- Sync confirmation modal --}}
|
||||
@if($showSyncButton)
|
||||
<template x-teleport="body">
|
||||
<div x-data x-show="$wire.confirmingSync" x-cloak
|
||||
class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50">
|
||||
<div class="bg-[var(--wc-light-primary)] dark:bg-[var(--wc-dark-primary)] rounded-lg shadow-lg max-w-sm w-full mx-4 p-6 space-y-4">
|
||||
<h3 class="font-semibold text-base">{{ __('Sync members') }}</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __('This will remove all members that are currently not linked to :name.', ['name' => $activeProfile->name]) }}
|
||||
</p>
|
||||
<div class="flex justify-end gap-3 pt-2">
|
||||
<x-jetstream.secondary-button type="button" wire:click="$set('confirmingSync', false)">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.danger-button type="button"
|
||||
wire:click="syncLinkedMembers"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="syncLinkedMembers">
|
||||
<span wire:loading.remove wire:target="syncLinkedMembers">{{ __('Sync') }}</span>
|
||||
<span wire:loading wire:target="syncLinkedMembers">{{ __('Syncing...') }}</span>
|
||||
</x-jetstream.danger-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@endif
|
||||
|
||||
<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>
|
||||
|
||||
{{-- Sync linked members button --}}
|
||||
@if($showSyncButton)
|
||||
<div class="px-2 pb-2">
|
||||
<button type="button"
|
||||
wire:click="confirmSync"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="confirmSync"
|
||||
class="flex items-center gap-3 text-xs text-theme-primary hover:text-theme-brand hover:underline focus:outline-none disabled:opacity-50">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||||
</svg>
|
||||
<span>{{ __('Sync with all users linked to this :type', ['type' => __(strtolower($activeProfileType))]) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Search --}}
|
||||
<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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,374 @@
|
||||
|
||||
<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, 'belongsToAuth' => $belongsToAuth ])
|
||||
@endif
|
||||
|
||||
{{-- Attachemnt is Video/ --}}
|
||||
@if (str()->startsWith($attachment->mime_type, 'video/'))
|
||||
<x-wirechat::video height="max-h-[400px]" :cover="false" source="{{ $attachment ? route('wirechat.attachment.serve', $attachment) : '' }}" />
|
||||
@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
|
||||
|
||||
{{-- File safety warning for received non-image attachments --}}
|
||||
@if (!$belongsToAuth && !str()->startsWith($attachment->mime_type, 'image/'))
|
||||
<div class="mt-1 flex items-start gap-1.5 px-2 py-1.5 text-xs text-theme-danger">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3.5 h-3.5 flex-shrink-0 mt-0.5">
|
||||
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<span>{{ __('messages.wirechat.file_warning') }}</span>
|
||||
</div>
|
||||
@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 && !($attachment && str_starts_with($attachment->mime_type ?? '', 'image/svg')))
|
||||
@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>
|
||||
@@ -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 ? route('wirechat.attachment.serve', $attachment) : '#' }}">
|
||||
<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>
|
||||
@@ -0,0 +1,649 @@
|
||||
@use('Namu\WireChat\Helpers\Helper')
|
||||
|
||||
<footer class="shrink-0 h-auto relative sticky bottom-0 mt-auto" style="padding-bottom: var(--wc-keyboard-offset, 0px); transition: padding-bottom 0.1s ease;">
|
||||
|
||||
{{-- 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 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 || uploading" x-cloak
|
||||
x-data="{ uploading: false }"
|
||||
x-on:wc-upload-start.window="uploading = true"
|
||||
x-on:wc-upload-end.window="uploading = false"
|
||||
class="relative flex flex-col w-full gap-3" wire:loading.class="animate-pulse" wire:target="sendMessage">
|
||||
|
||||
{{-- Upload spinner (shown only before the first file preview appears) --}}
|
||||
<div x-show="uploading && $wire.media.length === 0 && ($wire.files.length ?? 1) === 0"
|
||||
class="flex items-center justify-center py-4">
|
||||
<svg class="animate-spin w-6 h-6 text-gray-500 dark:text-gray-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@if (count($media) > 0)
|
||||
<div x-data="attachments('media')" class="relative">
|
||||
{{-- Upload spinner overlay --}}
|
||||
<div x-show="isUploading" class="absolute inset-0 flex items-center justify-center bg-white/60 dark:bg-black/40 z-20 rounded-lg">
|
||||
<svg class="animate-spin w-6 h-6 text-gray-500 dark:text-gray-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
</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 --}}
|
||||
@php $filesArray = is_array($files) ? $files : ($files ? [$files] : []); @endphp
|
||||
@if (count($filesArray) > 0)
|
||||
<section x-data="attachments('files')"
|
||||
class="relative 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;">
|
||||
|
||||
{{-- Upload spinner overlay --}}
|
||||
<div x-show="isUploading" class="absolute inset-0 flex items-center justify-center bg-white/60 dark:bg-black/40 z-20 rounded-lg">
|
||||
<svg class="animate-spin w-6 h-6 text-gray-500 dark:text-gray-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{{-- Loop through files for preview --}}
|
||||
@foreach ($filesArray 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 --}}
|
||||
@if(count($filesArray) < config('wirechat.attachments.max_uploads', 10))
|
||||
<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($filesArray) }})" 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>
|
||||
@endif
|
||||
|
||||
</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
|
||||
|
||||
|
||||
|
||||
{{-- 'uploading': attachments upload one by one; do not send until all have finished,
|
||||
or the rest stay behind (and on a slow server the send raced the last upload) --}}
|
||||
<form x-data="{
|
||||
'body': $wire.entangle('body'),
|
||||
'uploading': false,
|
||||
}" x-on:wc-upload-start.window="uploading = true" x-on:wc-upload-end.window="uploading = false" 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="!uploading && ((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 flex max-w-fit items-center">
|
||||
<button wire:loading.attr="disabled" type="button" dusk="emoji-trigger-button"
|
||||
@click="openEmojiPicker = ! openEmojiPicker; $dispatch('load-emoji-picker')" 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';"
|
||||
{{-- Enter adds a new line (the textarea's own behaviour). Ctrl+Enter, or
|
||||
Cmd+Enter on Apple keyboards, sends: it submits the form, whose
|
||||
@submit handler checks there is something to send. Not while an
|
||||
input method is still composing a character. --}}
|
||||
@keydown.enter="($event.ctrlKey || $event.metaKey) && !$event.isComposing && ($event.preventDefault(), $el.form.requestSubmit())"
|
||||
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';
|
||||
});
|
||||
// Move cursor to end if the input is pre-populated
|
||||
$nextTick(() => {
|
||||
if ($el.value.length > 0) {
|
||||
$el.focus();
|
||||
$el.setSelectionRange($el.value.length, $el.value.length);
|
||||
$el.style.height = 'auto';
|
||||
$el.style.height = $el.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="!uploading && ((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;
|
||||
|
||||
// Signal immediately so the preview container becomes visible
|
||||
window.dispatchEvent(new CustomEvent('wc-upload-start'));
|
||||
this.isUploading = true;
|
||||
|
||||
// Validate selected files and upload if valid
|
||||
this.validateFiles(files, count)
|
||||
.then((validFiles) => {
|
||||
if (validFiles.length > 0) {
|
||||
this.uploadFiles(validFiles);
|
||||
} else {
|
||||
window.dispatchEvent(new CustomEvent('wc-upload-end'));
|
||||
this.isUploading = false;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
window.dispatchEvent(new CustomEvent('wc-upload-end'));
|
||||
this.isUploading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Upload files using Livewire's upload
|
||||
uploadFiles(files) {
|
||||
this.isUploading = true;
|
||||
this.progress = 0;
|
||||
|
||||
// Initialize per-file progress tracking
|
||||
const totalFiles = files.length;
|
||||
const fileProgress = Array.from(files).map(() => 0);
|
||||
let settled = 0;
|
||||
files.forEach((file, index) => {
|
||||
$wire.upload(
|
||||
`${this.wireModel}`, // Livewire model
|
||||
file, // Single file
|
||||
() => {
|
||||
fileProgress[index] = 100; // Mark this file as complete
|
||||
this.progress = Math.round((fileProgress.reduce((a, b) => a + b, 0)) / totalFiles);
|
||||
if (++settled >= totalFiles) {
|
||||
this.isUploading = false;
|
||||
window.dispatchEvent(new CustomEvent('wc-upload-end'));
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
fileProgress[index] = -1; // Mark as failed
|
||||
$dispatch('wirechat-toast', { type: 'error', message: `Validation error: ${error}` });
|
||||
if (++settled >= totalFiles) {
|
||||
this.isUploading = false;
|
||||
window.dispatchEvent(new CustomEvent('wc-upload-end'));
|
||||
}
|
||||
},
|
||||
(event) => {
|
||||
fileProgress[index] = event.detail.progress; // Update per-file progress
|
||||
this.progress = Math.round((fileProgress.reduce((a, b) => a + b, 0)) / totalFiles); // 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 mimeSubtype = file.type.split('/')[1].toLowerCase();
|
||||
const extension = file.name.split('.').pop().toLowerCase();
|
||||
const typeAllowed = this.allowedFileTypes.includes(mimeSubtype) || this.allowedFileTypes.includes(extension);
|
||||
return file.size > this.maxSize || !typeAllowed;
|
||||
});
|
||||
|
||||
// Filter valid files
|
||||
const validFiles = Array.from(files).filter((file) => {
|
||||
const mimeSubtype = file.type.split('/')[1].toLowerCase();
|
||||
const extension = file.name.split('.').pop().toLowerCase();
|
||||
const typeAllowed = this.allowedFileTypes.includes(mimeSubtype) || this.allowedFileTypes.includes(extension);
|
||||
return file.size <= this.maxSize && typeAllowed;
|
||||
});
|
||||
|
||||
// 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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,84 @@
|
||||
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
|
||||
@php
|
||||
$isQr = str_starts_with($attachment?->mime_type ?? '', 'image/svg');
|
||||
$appUrl = rtrim(config('app.url'), '/');
|
||||
$msgBody = trim($message?->body ?? '');
|
||||
$transactionLinkPattern = '#^' . preg_quote($appUrl, '#') . '/[a-z]{2}/[a-z\-]+/(\d+)$#';
|
||||
$isTransactionBody = $isQr && preg_match($transactionLinkPattern, $msgBody, $qrMatches);
|
||||
$transactionLinkId = $isTransactionBody ? $qrMatches[1] : null;
|
||||
$attachmentUrl = $attachment ? route('wirechat.attachment.serve', $attachment) : null;
|
||||
@endphp
|
||||
<div x-data="{ open: false }" class="inline-block">
|
||||
@if ($isQr)
|
||||
<div @class([
|
||||
'bg-white p-3 overflow-hidden rounded-xl',
|
||||
'rounded-br-md rounded-tr-2xl' => ($isSameAsNext && $isNotSameAsPrevious && $belongsToAuth),
|
||||
'rounded-r-md' => ($isSameAsPrevious && $belongsToAuth),
|
||||
'rounded-br-xl rounded-r-xl' => ($isNotSameAsPrevious && $isNotSameAsNext && $belongsToAuth),
|
||||
'rounded-br-2xl' => ($isNotSameAsNext && $belongsToAuth),
|
||||
'rounded-bl-md rounded-tl-2xl' => ($isSameAsNext && $isNotSameAsPrevious && !$belongsToAuth),
|
||||
'rounded-l-md' => ($isSameAsPrevious && !$belongsToAuth),
|
||||
'rounded-bl-xl rounded-l-xl' => ($isNotSameAsPrevious && $isNotSameAsNext && !$belongsToAuth),
|
||||
'rounded-bl-2xl' => ($isNotSameAsNext && !$belongsToAuth),
|
||||
])>
|
||||
<div class="flex flex-col items-center">
|
||||
<img loading="lazy" src="{{ $attachmentUrl }}" alt="{{ __('wirechat::chat.labels.attachment') }}"
|
||||
class="h-[100px] w-[100px] object-contain rounded-none cursor-pointer" @click="open = true">
|
||||
@if ($isTransactionBody)
|
||||
<a href="{{ $msgBody }}" class="text-center text-base text-gray-700 underline px-4 pt-3 pb-1 hover:text-theme-brand transition-colors">
|
||||
{{ __('View transaction') }} #{{ $transactionLinkId }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<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 cursor-pointer',
|
||||
'rounded-br-md rounded-tr-2xl' => ($isSameAsNext && $isNotSameAsPrevious && $belongsToAuth),
|
||||
'rounded-r-md' => ($isSameAsPrevious && $belongsToAuth),
|
||||
'rounded-br-xl rounded-r-xl' => ($isNotSameAsPrevious && $isNotSameAsNext && $belongsToAuth),
|
||||
'rounded-br-2xl' => ($isNotSameAsNext && $belongsToAuth),
|
||||
'rounded-bl-md rounded-tl-2xl' => ($isSameAsNext && $isNotSameAsPrevious && !$belongsToAuth),
|
||||
'rounded-l-md' => ($isSameAsPrevious && !$belongsToAuth),
|
||||
'rounded-bl-xl rounded-l-xl' => ($isNotSameAsPrevious && $isNotSameAsNext && !$belongsToAuth),
|
||||
'rounded-bl-2xl' => ($isNotSameAsNext && !$belongsToAuth),
|
||||
])
|
||||
loading="lazy" src="{{ $attachmentUrl }}" alt="{{ __('wirechat::chat.labels.attachment') }}"
|
||||
@click="open = true">
|
||||
@endif
|
||||
|
||||
{{-- Fullscreen lightbox --}}
|
||||
<template x-teleport="body">
|
||||
<div x-show="open"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
@click="open = false"
|
||||
@keydown.escape.window="open = false"
|
||||
class="fixed inset-0 z-[9999] flex items-center justify-center cursor-zoom-out {{ $isQr ? 'bg-white' : 'bg-black' }}"
|
||||
style="display: none;">
|
||||
@if ($isQr)
|
||||
<img src="{{ $attachmentUrl }}" alt="{{ __('wirechat::chat.labels.attachment') }}"
|
||||
class="max-h-screen max-w-screen w-full h-full object-contain select-none p-8">
|
||||
@else
|
||||
<img src="{{ $attachmentUrl }}" alt="{{ __('wirechat::chat.labels.attachment') }}"
|
||||
class="max-h-screen max-w-screen object-contain select-none">
|
||||
@endif
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -0,0 +1,106 @@
|
||||
@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 (matches all localized slugs, e.g. /en/statement/123 or /nl/transactie-bewijs/123)
|
||||
$transactionShowPattern = '#^' . preg_quote($appUrl, '#') . '/[a-z]{2}/[a-z\-]+/(\d+)$#';
|
||||
$isTransactionLink = preg_match($transactionShowPattern, $body, $transactionMatches);
|
||||
$transactionLinkId = $isTransactionLink ? $transactionMatches[1] : null;
|
||||
@endphp
|
||||
@if ($isTransactionLink && !$attachment)
|
||||
<a href="{{ $body }}" class="underline text-sm md:text-base dark:text-white block py-1">
|
||||
{{ __('View transaction') }} #{{ $transactionLinkId }}
|
||||
</a>
|
||||
@elseif ($isTransactionLink && $attachment)
|
||||
{{-- Link already rendered inside the image/QR partial --}}
|
||||
@else
|
||||
<pre class="whitespace-pre-line break-all 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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,61 @@
|
||||
|
||||
@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="flex flex-col 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
|
||||
|
||||
@if ($showNewChatModalButton)
|
||||
<div class="mt-2">
|
||||
<x-wirechat::actions.new-chat widget="{{$this->isWidget()}}">
|
||||
@if ($title === '')
|
||||
<x-jetstream.button id="open-new-chat-modal-button" type="button">
|
||||
{{ __('wirechat::new.chat.labels.heading') }}
|
||||
</x-jetstream.button>
|
||||
@else
|
||||
<x-jetstream.secondary-button id="open-new-chat-modal-button" type="button">
|
||||
{{ __('wirechat::new.chat.labels.heading') }}
|
||||
</x-jetstream.secondary-button>
|
||||
@endif
|
||||
</x-wirechat::actions.new-chat>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</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>
|
||||
@@ -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>
|
||||
+11
@@ -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>
|
||||
+5
@@ -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>
|
||||
+35
@@ -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 ((int) ($lastMessage->created_at->diffInMinutes(now(), true)) < 1)
|
||||
@lang('wirechat::chats.labels.now')
|
||||
@else
|
||||
{{ $lastMessage->created_at->shortAbsoluteDiffForHumans() }}
|
||||
@endif
|
||||
</span>
|
||||
|
||||
|
||||
</div>
|
||||
@@ -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">​</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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,270 @@
|
||||
|
||||
<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>
|
||||
{{-- Add all linked members button (shown for Organization, Bank, Admin profiles) --}}
|
||||
@php $activeProfileType = getActiveProfileType(); @endphp
|
||||
@if(in_array($activeProfileType, ['Organization', 'Bank', 'Admin']))
|
||||
<div class="my-2">
|
||||
<button type="button"
|
||||
wire:click="addLinkedMembers"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="addLinkedMembers"
|
||||
class="flex items-center gap-3 text-xs text-theme-primary hover:text-theme-brand hover:underline focus:outline-none disabled:opacity-50">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z" />
|
||||
</svg>
|
||||
<span wire:loading.remove wire:target="addLinkedMembers">
|
||||
{{ __('Add all linked users of this :type', ['type' => __(strtolower($activeProfileType))]) }}
|
||||
</span>
|
||||
<span wire:loading wire:target="addLinkedMembers">
|
||||
{{ __('Adding...') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 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 class="flex flex-col gap-1">
|
||||
|
||||
@if ($selectedMembers)
|
||||
|
||||
@foreach ($selectedMembers as $key => $member)
|
||||
<li class="flex items-center justify-between 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user