Snapshot of main at 9f39a6e1e (2026-11-02)
This commit is contained in:
commit
6ccf1792d7
2580 files changed
+1040202
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user