Files

435 lines
15 KiB
PHP

{{-- 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>