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,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>
|
||||
Reference in new issue
Block a user