Snapshot of main at 9f39a6e1e (2026-11-02)

This commit is contained in:
Ronald Huynen committed 2026-11-02 10:41:47 +01:00
commit 6ccf1792d7
2580 files changed
+1040202

No files matched your search

@@ -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>
@@ -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>
@@ -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>
@@ -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">&#8203;</span>
<div x-show="show && showActiveModalComponent"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
class="inline-block align-middle rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 w-full sm:max-w-lg"
id="chat-dialog-container" x-trap.noscroll="show && showActiveModalComponent" aria-modal="true">
@forelse($components as $id => $component)
<div x-show.immediate="activeModalComponent == '{{ $id }}'" x-ref="{{ $id }}"
wire:key="{{ $id }}">
@livewire($component['name'], $component['arguments'], key($id))
</div>
@empty
@endforelse
</div>
</div>
</div>
</div>
@@ -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>