refactor: chatbox to alpinejs

- This removes Vue completely from unit3d.
This commit is contained in:
HDVinnie
2025-06-18 15:08:53 -04:00
parent d5fd361edd
commit d55488b74c
18 changed files with 1319 additions and 1828 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -16,7 +16,7 @@
"ext-zip": "*",
"assada/laravel-achievements": "^2.8",
"bjeavons/zxcvbn-php": "^1.4.2",
"doctrine/dbal": "^3.9.4",
"doctrine/dbal": "^3.9.5",
"gabrielelana/byte-units": "^0.5.0",
"graham-campbell/markdown": "^v16.0.0",
"guzzlehttp/guzzle": "^7.9.3",
@@ -24,14 +24,14 @@
"hdvinnie/laravel-security-headers": "^3.0.1",
"intervention/image": "^2.7.2",
"joypixels/assets": "^v7.0.1",
"laravel/fortify": "^v1.25.4",
"laravel/framework": "^v12.15.0",
"laravel/fortify": "^1.26.0",
"laravel/framework": "^12.18.0",
"laravel/octane": "^2.9.3",
"laravel/scout": "^10.15.0",
"laravel/tinker": "^2.10.1",
"livewire/livewire": "^3.6.3",
"marcreichel/igdb-laravel": "^5.3.0",
"meilisearch/meilisearch-php": "^1.14.0",
"marcreichel/igdb-laravel": "^5.3.1",
"meilisearch/meilisearch-php": "^1.15.0",
"paragonie/constant_time_encoding": "^2.7.0",
"spatie/laravel-backup": "^9.3.3",
"spatie/laravel-cookie-consent": "^3.3.3",
@@ -44,14 +44,14 @@
"require-dev": {
"barryvdh/laravel-debugbar": "^3.15.4",
"brianium/paratest": "^7.8.3",
"calebdw/larastan": "^3.4.2",
"calebdw/larastan": "^3.4.3",
"calebdw/larastan-livewire": "^2.2.0",
"fakerphp/faker": "^1.24.1",
"jasonmccreary/laravel-test-assertions": "^2.8.0",
"laravel/pint": "v1.20.0",
"laravel/sail": "^1.43.0",
"laravel/sail": "^1.43.1",
"mockery/mockery": "^1.6.12",
"nunomaduro/collision": "^8.8.0",
"nunomaduro/collision": "^8.8.1",
"pestphp/pest": "^3.8.2",
"pestphp/pest-plugin-laravel": "^3.2",
"pestphp/pest-plugin-livewire": "^3.0",
@@ -59,7 +59,7 @@
"phpunit/phpunit": "^11.5.15",
"ryoluo/sail-ssl": "^1.4.0",
"spatie/laravel-ignition": "^2.9.1",
"tomasvotruba/bladestan": "^0.11.2"
"tomasvotruba/bladestan": "^0.11.3"
},
"config": {
"preferred-install": "dist",

479
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,13 +22,9 @@
"socket.io": "2.4.0",
"socket.io-client": "2.3.1",
"sweetalert2": "^11.15.10",
"virtual-select-plugin": "^1.0.46",
"vue": "^2.6.14",
"vue-loader": "^15.9.8",
"vue-template-compiler": "^2.6.14"
"virtual-select-plugin": "^1.0.46"
},
"devDependencies": {
"@vitejs/plugin-vue2": "^2.3.3",
"laravel-vite-plugin": "^1.1.1",
"vite-plugin-static-copy": "^2.2.0",
"prettier-plugin-blade": "2.0.0",

View File

@@ -39,6 +39,7 @@ import.meta.glob(['/public/img/pipes/**', '/resources/sass/vendor/webfonts/font-
import { Livewire, Alpine } from '../../vendor/livewire/livewire/dist/livewire.esm.js';
// Custom AlpineJS Components
import './components/alpine/chatbox';
import './components/alpine/checkboxGrid';
import './components/alpine/dialog';
import './components/alpine/dislikeButton';

View File

@@ -1,32 +0,0 @@
<template>
<div id="tran">{{ result }}</div>
</template>
<script>
export default {
name: 'trans',
props: ['path'],
data() {
return {
result: '',
};
},
methods: {
getTranslation() {
axios
.get(`/api/lang/${this.path}`)
.then((response) => {
this.result = response.data.results;
})
.catch((error) => {
console.error(`Something went wrong fetching ${this.path}`);
});
},
},
created() {
this.getTranslation();
},
};
</script>

View File

@@ -1,60 +0,0 @@
<template>
<div class="text-center">
<button @click="checkUpdate()" class="btn btn-primary btn-md">
<i v-if="loading" class="fa fa-circle-notch fa-spin"></i>
{{ loading ? 'Loading...' : 'Check For Update' }}
</button>
</div>
</template>
<script>
import Swal from 'sweetalert2';
export default {
data() {
return {
loading: false,
};
},
methods: {
checkUpdate() {
this.loading = true;
axios
.get('/dashboard/UNIT3D')
.then((response) => {
if (response.data.updated === false) {
this.loading = false;
Swal.fire({
position: 'center',
icon: 'warning',
title: 'There Is A Update Available!',
showCancelButton: true,
showConfirmButton: true,
confirmButtonText: '<i class="fa fa-github"></i> Download from Github',
html: `New version <a href="github.com/HDInnovations/UNIT3D-Community-Edition/releases">${response.data.latestversion} </a> is available`,
}).then((result) => {
if (result.value) {
window.location.assign(
'//github.com/HDInnovations/UNIT3D-Community-Edition/releases',
);
}
});
} else {
this.loading = false;
Swal.fire({
position: 'center',
icon: 'success',
title: 'You Are Running The Latest Version Of UNIT3D Community Edition!',
showCancelButton: false,
timer: 4500,
});
}
})
.catch((error) => {
Swal.fire('Oops...', error.response.data, 'error');
});
},
},
};
</script>

View File

@@ -0,0 +1,721 @@
// AlpineJS Chatbox Component for UNIT3D
// Handles all chat logic, state, and Echo events
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
// Initialize dayjs plugins
dayjs.extend(relativeTime);
// Utility functions
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
// Message handler module
const messageHandler = {
format(message) {
return message;
},
create(message, context, save = true, user_id = 1, receiver_id = null, bot_id = null) {
if (/\[size=[0-9]{3,}\]/.test(message)) return;
if (context.state.chat.tab === 'userlist') return;
if (!message || message.trim() === '') return;
return axios.post('/api/chat/messages', {
user_id,
receiver_id,
bot_id,
chatroom_id: context.state.chat.room,
message: message,
save,
targeted: context.state.chat.target,
}).then((response) => {
if (context.state.chat.activeTab.startsWith('bot') ||
context.state.chat.activeTab.startsWith('target')) {
context.messages.push(response.data.data);
}
if (context.messages.length > (context.config.message_limit || 100)) {
context.messages.splice(0, context.messages.length - (context.config.message_limit || 100));
}
if (context.$refs && context.$refs.message) {
context.$refs.message.value = '';
}
context.scrollToBottom();
});
},
delete(id, context) {
if (!id) return;
return axios.post(`/api/chat/message/${id}/delete`)
.then(() => {
const index = context.messages.findIndex(msg => msg.id === id);
if (index !== -1) {
context.messages.splice(index, 1);
}
})
.catch((error) => {
console.error('Error deleting message:', error);
});
}
};
// Channel handler module
const channelHandler = {
setupRoom(id, context) {
if (context.channel) {
window.Echo.leave(`chatroom.${context.state.chat.room}`);
}
context.channel = window.Echo.join(`chatroom.${id}`);
this.setupListeners(context);
},
setupListeners(context) {
if (!context.channel) return;
context.channel
.here((users) => {
context.state.ui.connecting = false;
context.users = users;
})
.joining((user) => {
if (!context.users.some(u => u.id === user.id)) {
context.users.push(user);
}
})
.leaving((user) => {
context.users = context.users.filter(u => u.id !== user.id);
})
.listen('.new.message', (e) => {
if (!context.state.chat.activeTab.startsWith('room')) return;
const message = context.processMessageCanMod(e.message);
context.messages.push(message);
context.scrollToBottom();
})
.listen('.new.ping', (e) => {
context.handlePing('room', e.ping.id);
})
.listen('.delete.message', (e) => {
if (context.state.chat.target > 0 || context.state.chat.bot > 0) return;
let index = context.messages.findIndex((msg) => msg.id === e.message.id);
if (index !== -1) context.messages.splice(index, 1);
})
.listenForWhisper('typing', (e) => {
if (context.state.chat.target > 0 || context.state.chat.bot > 0) return;
context.activePeer = e;
clearTimeout(context.typingTimeout);
context.typingTimeout = setTimeout(() => {
context.activePeer = false;
}, 15000);
});
context.channel.error((error) => {
console.error('Socket error:', error);
context.state.ui.error = 'Connection lost. Trying to reconnect...';
setTimeout(() => {
this.setupRoom(context.state.chat.room, context);
}, 5000);
});
}
};
document.addEventListener('alpine:init', () => {
Alpine.data('chatbox', (user) => ({
state: {
ui: {
connecting: true,
loading: true,
fullscreen: false,
error: null
},
chat: {
tab: '',
room: 0,
target: 0,
bot: 0,
activeTab: '',
activeRoom: '',
activeTarget: '',
activeBot: '',
botName: '',
botId: 0,
botCommand: '',
listening: 1,
showWhispers: true
},
message: {
helpName: '',
helpCommand: '',
helpId: 0,
receiver_id: null,
bot_id: null
}
},
auth: user,
statuses: [],
status: 0,
echoes: [],
bots: [],
chatrooms: [],
messages: [],
users: [],
pings: [],
audibles: [],
boot: 0,
activePeer: false,
frozen: false,
scroll: true,
channel: null,
chatter: null,
config: {},
typingTimeout: null,
blurHandler: null,
focusHandler: null,
init() {
this.state.chat.activeRoom = this.auth.chatroom.name;
this.blurHandler = () => {
document.getElementById('chatbody').setAttribute('audio', true);
};
this.focusHandler = () => {
document.getElementById('chatbody').setAttribute('audio', false);
};
Promise.all([
this.fetchStatuses(),
this.fetchEchoes(),
this.fetchBots(),
this.fetchAudibles(),
this.fetchRooms()
]).then(() => {
this.state.ui.loading = false;
this.listenForChatter();
this.attachAudible();
}).catch(error => {
console.error('Error initializing chat:', error);
this.state.ui.error = 'Error loading chat. Please try again.';
this.state.ui.loading = false;
});
this.$cleanup = () => {
if (this.channel) {
window.Echo.leave(`chatroom.${this.state.chat.room}`);
}
if (this.chatter) {
this.chatter.stopListening('Chatter');
}
window.removeEventListener('blur', this.blurHandler);
window.removeEventListener('focus', this.focusHandler);
clearTimeout(this.typingTimeout);
};
},
// Fetchers
async fetchAudibles() {
try {
const response = await axios.get('/api/chat/audibles');
this.audibles = response.data.data;
return this.fetchConfiguration();
} catch (error) {
console.error('Error fetching audibles:', error);
throw error;
}
},
async fetchEchoes() {
try {
const response = await axios.get('/api/chat/echoes');
this.echoes = this.sortEchoes(response.data.data);
this.boot = 1;
} catch (error) {
console.error('Error fetching echoes:', error);
throw error;
}
},
async fetchBots() {
try {
const response = await axios.get('/api/chat/bots');
this.bots = response.data.data;
if (this.bots.length > 0) {
this.state.message.helpId = this.bots[0].id;
this.state.message.helpName = this.bots[0].name;
this.state.message.helpCommand = this.bots[0].command;
}
} catch (error) {
console.error('Error fetching bots:', error);
throw error;
}
},
async fetchRooms() {
try {
const response = await axios.get('/api/chat/rooms');
this.chatrooms = response.data.data;
if (this.chatrooms.length > 0) {
this.state.chat.room = this.auth.chatroom.id;
this.state.chat.tab = this.auth.chatroom.name;
this.state.chat.activeTab = 'room' + this.state.chat.room;
// Immediately load messages for the user's current chatroom
await this.changeRoom(this.auth.chatroom.id);
}
} catch (error) {
console.error('Error fetching rooms:', error);
throw error;
}
},
async fetchConfiguration() {
try {
const response = await axios.get(`/api/chat/config`);
this.config = response.data;
} catch (error) {
console.error('Error fetching configuration:', error);
throw error;
}
},
async fetchBotMessages(id) {
try {
this.state.ui.connecting = true;
const response = await axios.get(`/api/chat/bot/${id}`);
// Process messages to add canMod property for each message and sanitize content
this.messages = response.data.data
.map(message => this.processMessageCanMod(message))
.reverse();
this.state.ui.connecting = false;
this.scrollToBottom();
} catch (error) {
console.error('Error fetching bot messages:', error);
this.state.ui.connecting = false;
throw error;
}
},
async fetchPrivateMessages() {
try {
this.state.ui.connecting = true;
const response = await axios.get(`/api/chat/private/messages/${this.state.chat.target}`);
// Process messages to add canMod property for each message and sanitize content
this.messages = response.data.data
.map(message => this.processMessageCanMod(message))
.reverse();
this.state.ui.connecting = false;
this.scrollToBottom();
} catch (error) {
console.error('Error fetching private messages:', error);
this.state.ui.connecting = false;
throw error;
}
},
async fetchMessages() {
try {
this.state.ui.connecting = true;
const response = await axios.get(`/api/chat/messages/${this.state.chat.room}`);
// Process messages to add canMod property for each message and sanitize content
this.messages = response.data.data
.map(message => this.processMessageCanMod(message))
.reverse();
this.state.ui.connecting = false;
this.scrollToBottom();
} catch (error) {
console.error('Error fetching messages:', error);
this.state.ui.connecting = false;
throw error;
}
},
// Process messages for display
processMessageCanMod(message) {
if (!message) return message;
// Check if the user can moderate this message
message.canMod = this.canMod(message);
// Sanitize message content if it exists
if (message.message) {
message.originalMessage = message.message;
message.message = messageHandler.format(message.message);
}
return message;
},
// Permission checking
canMod(message) {
if (!message || !message.user || !this.auth || !this.auth.group) return false;
return (
// Owner can mod all messages
this.auth.group.is_owner ||
// User can mod their own messages
message.user.id === this.auth.id ||
// Admins can mod messages except for Owner messages
(this.auth.group.is_admin && message.user?.group && !message.user.group.is_owner) ||
// Mods cannot mod other mods' messages
(this.auth.group.is_modo && message.user?.group && !message.user.group.is_modo)
);
},
async fetchStatuses() {
try {
const response = await axios.get('/api/chat/statuses');
this.statuses = response.data;
} catch (error) {
console.error('Error fetching statuses:', error);
throw error;
}
},
// Tab/Room/Target/Bot switching
changeTab(typeVal, newVal) {
if (typeVal == 'room') {
this.state.chat.bot = 0;
this.state.chat.target = 0;
this.state.message.bot_id = 0;
this.state.message.receiver_id = 0;
this.state.chat.tab = newVal;
this.state.chat.activeTab = 'room' + newVal;
this.state.chat.activeRoom = newVal;
this.deletePing('room', newVal);
let currentRoom = this.echoes.find(o => o.room && o.room.id == newVal);
if (currentRoom) {
this.changeRoom(currentRoom.room.id);
this.state.message.receiver_id = null;
this.state.message.bot_id = null;
}
let currentAudio = this.audibles.find(o => o.room && o.room.id == newVal);
this.state.chat.listening = currentAudio && currentAudio.status == 1 ? 1 : 0;
} else if (typeVal == 'target') {
this.state.chat.bot = 0;
this.state.chat.tab = newVal;
this.state.chat.activeTab = 'target' + newVal;
this.state.chat.activeTarget = newVal;
this.deletePing('target', newVal);
let currentTarget = this.echoes.find(o => o.target && o.target.id == newVal);
if (currentTarget) {
this.changeTarget(currentTarget.target.id);
this.state.message.receiver_id = currentTarget.target.id;
this.state.message.bot_id = null;
}
let currentAudio = this.audibles.find(o => o.target && o.target.id == newVal);
this.state.chat.listening = currentAudio && currentAudio.status == 1 ? 1 : 0;
} else if (typeVal == 'bot') {
this.state.chat.target = 0;
this.state.chat.tab = newVal;
this.state.chat.activeTab = 'bot' + newVal;
this.state.chat.activeBot = newVal;
this.deletePing('bot', newVal);
let currentBot = this.echoes.find(o => o.bot && o.bot.id == newVal);
if (currentBot) {
this.state.chat.botName = currentBot.bot.name;
this.state.chat.botCommand = currentBot.bot.command;
this.state.chat.botId = currentBot.bot.id;
this.changeBot(currentBot.bot.id);
this.state.message.receiver_id = 1;
this.state.message.bot_id = currentBot.bot.id;
}
let currentAudio = this.audibles.find(o => o.bot && o.bot.id == newVal);
this.state.chat.listening = currentAudio && currentAudio.status == 1 ? 1 : 0;
} else if (typeVal == 'list') {
this.state.chat.tab = newVal;
}
},
changeRoom(id) {
this.state.chat.bot = 0;
this.state.chat.target = 0;
this.state.chat.room = id;
this.state.message.bot_id = null;
this.state.message.receiver_id = null;
if (this.auth.chatroom.id === id) {
this.state.chat.tab = this.auth.chatroom.name;
this.state.chat.activeRoom = this.auth.chatroom.name;
this.fetchMessages();
} else {
axios.post(`/api/chat/user/chatroom`, { room_id: id })
.then((response) => {
this.auth = response.data;
this.state.chat.tab = this.auth.chatroom.name;
this.state.chat.activeRoom = this.auth.chatroom.name;
this.fetchMessages();
})
.catch(error => {
console.error('Error changing room:', error);
});
}
// Set up room channel with improved connection handling
channelHandler.setupRoom(id, this);
},
leaveRoom(id) {
if (id !== 1) {
// Update the user's chatroom in the database
axios.post(`/api/chat/echoes/delete/chatroom`, { room_id: id })
.then((response) => {
// Reassign the auth variable to the response data
this.auth = response.data;
document.getElementById('currentChatroom').value = '1';
this.fetchRooms().then(() => {
// Check if there are other chat tabs available
if (this.state.chat.tab) {
// Switch to the first chat tab
const firstTab = this.state.chat.tab;
this.changeTab('room', firstTab);
} else if (this.chatrooms.length > 0) {
// Default to the first chatroom from the dropdown
const firstChatroom = this.chatrooms[0];
this.changeRoom(firstChatroom.id);
} else {
console.warn('No chat tabs or chatrooms available.');
}
});
})
.catch((error) => {
console.error('Error leaving room:', error);
});
}
},
changeTarget(id) {
if (this.state.chat.target !== id && id != 0) {
this.state.chat.target = id;
this.fetchPrivateMessages();
}
},
changeBot(id) {
if (this.state.chat.bot !== id && id != 0) {
this.state.chat.bot = id;
this.state.message.bot_id = id;
this.state.message.receiver_id = 1;
this.fetchBotMessages(this.state.chat.bot);
}
},
// Delegate message operations to messageHandler
createMessage(message, save = true, user_id = 1, receiver_id = null, bot_id = null) {
return messageHandler.create(
message,
this,
save,
user_id,
receiver_id || this.state.message.receiver_id,
bot_id || this.state.message.bot_id
);
},
deleteMessage(id) {
return messageHandler.delete(id, this);
},
isTyping(e) {
const self = this;
if (!this._debouncedIsTyping) {
this._debouncedIsTyping = debounce(function(e) {
if (self.state.chat.tab != 'userlist' &&
self.state.chat.target < 1 &&
self.channel &&
self.state.chat.tab != '') {
self.channel.whisper('typing', { username: e.username });
}
}, 300);
}
this._debouncedIsTyping(e);
},
// Sound management
playSound() {
if (window.sounds && window.sounds['alert.mp3']) {
window.sounds['alert.mp3'].pause();
window.sounds['alert.mp3'].currentTime = 0;
window.sounds['alert.mp3'].play();
}
},
// Event listeners
listenForChatter() {
this.chatter = window.Echo.private(`chatter.${this.auth.id}`);
this.chatter.listen('Chatter', (e) => {
if (e.type == 'echo') {
this.echoes = this.sortEchoes(e.echoes);
} else if (e.type == 'audible') {
this.audibles = e.audibles;
} else if (e.type == 'new.message') {
if (!this.state.chat.activeTab.startsWith('bot') &&
!this.state.chat.activeTab.startsWith('target')) return;
if (e.message.bot && e.message.bot.id != this.state.chat.bot) return;
if (e.message.user && e.message.user.id != this.state.chat.target) return;
// Process and sanitize new message
const message = this.processMessageCanMod(e.message);
this.messages.push(message);
} else if (e.type == 'new.bot') {
// Process and sanitize new bot message
const message = this.processMessageCanMod(e.message);
this.messages.push(message);
} else if (e.type == 'new.ping') {
if (e.ping.type == 'bot') {
this.handlePing('bot', e.ping.id);
} else {
this.handlePing('target', e.ping.id);
}
} else if (e.type == 'delete.message') {
if (this.state.chat.target < 1 && this.state.chat.bot < 1) return;
let index = this.messages.findIndex((msg) => msg.id === e.message.id);
if (index !== -1) this.messages.splice(index, 1);
} else if (e.type == 'typing') {
if (this.state.chat.target < 1) return;
this.activePeer = e.username;
clearTimeout(this.typingTimeout);
this.typingTimeout = setTimeout(() => {
this.activePeer = false;
}, 15000);
}
});
this.chatter.error((error) => {
console.error('Chatter connection error:', error);
setTimeout(() => {
this.listenForChatter();
}, 5000);
});
},
listenForEvents() {
channelHandler.setupListeners(this);
},
// Utility
sortEchoes(obj) {
if (!obj || !Array.isArray(obj)) return [];
return obj.sort((a, b) => {
let nv1 = a.room?.name || a.target?.username || a.bot?.name || '';
let nv2 = b.room?.name || b.target?.username || b.bot?.name || '';
return nv1.localeCompare(nv2);
});
},
deletePing(type, id) {
let idx = this.pings.findIndex(p => p.type === type && p.id === id);
if (idx !== -1) this.pings.splice(idx, 1);
},
handlePing(type, id) {
if (!this.pings.some(p => p.type === type && p.id === id)) {
this.pings.push({ type, id, count: 0 });
}
this.playSound();
},
checkPings(type, id) {
return this.pings.some(p => p.type === type && p.id === id);
},
attachAudible() {
// Use the stored handlers for consistency and cleanup
window.addEventListener('blur', this.blurHandler);
window.addEventListener('focus', this.focusHandler);
},
// UI actions
changeFullscreen() {
this.state.ui.fullscreen = !this.state.ui.fullscreen;
},
changeWhispers() {
this.state.chat.showWhispers = !this.state.chat.showWhispers;
},
changeStatus(status_id) {
this.status = status_id;
if (this.auth.chat_status.id !== status_id) {
axios.post(`/api/chat/user/status`, { status_id })
.then((response) => {
this.auth = response.data;
})
.catch(error => {
console.error('Error changing status:', error);
});
}
},
startBot() {
if (this.state.chat.bot == 9999) return;
this.state.chat.tab = '@' + this.state.message.helpName;
this.state.chat.bot = this.state.message.helpId;
this.state.message.bot_id = this.state.message.helpId;
this.state.message.receiver_id = 1;
this.state.chat.botId = this.state.message.helpId;
this.state.chat.botName = this.state.message.helpName;
this.state.chat.botCommand = this.state.message.helpCommand;
this.fetchBotMessages(this.state.chat.bot);
},
forceMessage(name) {
const messageInput = document.getElementById('chatbox__messages-create');
if (messageInput) {
messageInput.value = '/msg ' + name + ' ';
messageInput.focus();
}
},
forceGift(name) {
const messageInput = document.getElementById('chatbox__messages-create');
if (messageInput) {
messageInput.value = '/gift ' + name + ' ';
messageInput.focus();
}
},
scrollToBottom() {
this.$nextTick(() => {
if (this.$refs && this.$refs.messagesWrapper) {
requestAnimationFrame(() => {
this.$refs.messagesWrapper.scrollTop = this.$refs.messagesWrapper.scrollHeight;
});
}
});
},
formatTime(timestamp) {
if (!timestamp) return '';
return dayjs(timestamp).fromNow();
},
}));
});

View File

@@ -1,70 +0,0 @@
<template>
<form class="form chatroom__new-message">
<p class="form__group">
<textarea
id="chatbox__messages-create"
class="form__textarea"
name="message"
placeholder=" "
send="true"
></textarea>
<label class="form__label form__label--floating" for="chatbox__messages-create">
Write your message...
</label>
</p>
</form>
</template>
<script>
export default {
data() {
return {
user: null,
editor: null,
input: null,
};
},
computed: {
receiver_id() {
return this.$parent.receiver_id;
},
bot_id() {
return this.$parent.bot_id;
},
},
methods: {
keyup(e) {
this.$emit('typing', this.user);
},
keydown(e) {
if (e.keyCode === 13 && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
},
sendMessage() {
let msg = (this.input.value = this.input.value.trim());
if (msg !== null && msg !== '') {
this.$emit('message-sent', {
message: msg,
save: true,
user_id: this.user.id,
receiver_id: this.receiver_id,
bot_id: this.bot_id,
});
this.input.value = '';
}
},
},
created() {
this.user = this.$parent.auth;
},
mounted() {
this.editor = document.getElementById('chatbox__messages-create').value;
this.input = document.getElementById('chatbox__messages-create');
this.input.addEventListener('keyup', this.keyup);
this.input.addEventListener('keydown', this.keydown);
},
};
</script>

View File

@@ -1,192 +0,0 @@
<template>
<div class="chatroom__messages--wrapper">
<ul class="chatroom__messages">
<li v-for="message in messages">
<article class="chatbox-message">
<header class="chatbox-message__header">
<address
class="chatbox-message__address user-tag"
:style="`${message.user?.is_lifetime ? 'background-image: url(/img/sparkels.gif);' : 'background-image:' + message.user?.group?.effect};`"
>
<a
class="user-tag__link"
:class="message.user?.group?.icon"
:href="`/users/${message.user?.username}`"
:style="`color: ${message.user?.group?.color}`"
:title="message.user?.group?.name"
>
<span v-if="message.user && message.user.id > 1" :style="`padding-right: 5px;`">{{
message.user?.username ?? 'Unknown'
}}</span>
<span
v-if="
message.bot && message.bot.id >= 1 && (!message.user || message.user.id < 2)
"
>{{ message.bot?.name ?? 'Unknown' }}</span
>
<i v-if="message.user?.icon !== null">
<img
:style="`max-height: 16px; vertical-align: text-bottom;`"
title="Custom User Icon"
:src="`/authenticated-images/user-icons/${message.user.username}`"
/>
</i>
<i
v-if="message.user?.is_lifetime == 1"
class="fal fa-star"
id="lifeline"
title="Lifetime Donor"
></i>
<i
v-if="message.user?.is_donor == 1 && message.user?.is_lifetime == 0"
class="fal fa-star text-gold"
title="Donor"
></i>
</a>
</address>
<div
v-if="message.bot && message.bot.id >= 1 && (!message.user || message.user.id < 2)"
class="bbcode-rendered"
:style="`font-style: italic; white-space: nowrap;`"
v-html="message.message"
></div>
<time
v-if="message.bot && message.bot.id >= 1 && (!message.user || message.user.id < 2)"
:style="`margin-left: 10px; white-space: nowrap;`"
class="chatbox-message__time"
:datetime="message.created_at"
:title="message.created_at"
>
{{ message.created_at | diffForHumans }}
</time>
<time
v-else
class="chatbox-message__time"
:datetime="message.created_at"
:title="message.created_at"
>
{{ message.created_at | diffForHumans }}
</time>
</header>
<aside class="chatbox-message__aside">
<figure class="chatbox-message__figure">
<i
class="fa fa-bell"
title="System Notification"
v-if="message.bot && message.bot.id >= 1 && (!message.user || message.user.id < 2)"
></i>
<a
v-if="message.user?.id != 1"
:href="`/users/${message.user?.username}`"
class="chatbox-message__avatar-link"
>
<img
v-if="message.user?.id != 1"
class="chatbox-message__avatar"
:src="
message.user?.image
? `/authenticated-images/user-avatars/${message.user.username}`
: '/img/profile.png'
"
:style="`border: 2px solid ${message.user?.chat_status?.color};`"
:title="message.user?.chat_status?.name"
/>
</a>
</figure>
</aside>
<menu class="chatbox-message__menu">
<li class="chatbox-message__menu-item">
<button
class="chatbox-message__delete-button"
v-if="message.user?.id != 1 && canMod(message)"
@click="deleteMessage(message.id)"
title="Delete message"
>
<i class="fa fa-trash"></i>
</button>
</li>
</menu>
<section
v-if="message.user && message.user.id > 1"
class="chatbox-message__content bbcode-rendered"
v-html="message.message"
></section>
</article>
</li>
<li v-if="messages.length === 0">There is no chat history here. Send a message!</li>
</ul>
</div>
</template>
<script>
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
export default {
props: {
messages: { required: true },
},
data() {
return {
editor: null,
};
},
methods: {
checkBot(e, message) {
if (e.target.hasAttribute('trigger') && e.target.getAttribute('trigger') == 'bot') {
e.preventDefault();
let target = e.target.hash;
const tmp = target.split('/');
document.getElementById('chat-message').value = '/' + tmp[1] + ' ' + tmp[2] + ' ';
document.getElementById('chat-message').value = '/' + tmp[1] + ' ' + tmp[2] + ' ';
}
},
canMod(message) {
/*
A user can Mod his own messages
A user in an is_modo group can Mod messages
A is_modo CAN NOT Mod another is_modo message
*/
return (
/* Owner can mod all */
this.$parent.auth.group.is_owner ||
/* User can mod his own message */
message.user.id === this.$parent.auth.id ||
/* is_admin can mod messages except for Owner messages */
(this.$parent.auth.group.is_admin && !message.user.group.is_owner) ||
/* Mods CAN NOT mod other mods messages */
(this.$parent.auth.group.is_modo && !message.user.group.is_modo)
);
},
editMessage(message) {},
deleteMessage(id) {
axios.post(`/api/chat/message/${id}/delete`);
},
userStyles(user) {
return `cursor: pointer; color: ${user.group.color}; background-image: ${user.group.effect};`;
},
groupColor(user) {
return user && user.group && user.group.hasOwnProperty('color')
? `color: ${user.group.color};`
: `cursor: pointer;`;
},
},
created() {
dayjs.extend(relativeTime);
this.interval = setInterval(() => this.$forceUpdate(), 30000);
},
filters: {
diffForHumans: (date) => {
if (!date) {
return null;
}
return dayjs(date).fromNow();
},
},
beforeDestroy() {
clearInterval(this.interval);
},
};
</script>

View File

@@ -1,50 +0,0 @@
<template>
<section class="chatroom__users">
<h2 class="chatroom-users__heading">Users</h2>
<ul class="chatroom-users__list">
<li class="chatroom-users__list-item" v-for="user in users">
<span
class="chatroom-users__user user-tag"
:style="user.group?.effect && `backgroundImage: ${user.group.effect}`"
>
<a
class="chatroom-users__user-link user-tag__link"
:class="user.group?.icon"
:href="`/users/${user.username}`"
:style="`color: ${user.group?.color}`"
:title="user.group?.name"
>
{{ user.username }}
</a>
</span>
<menu class="chatroom-users__buttons" v-if="$parent.auth.id !== user.id">
<li>
<button
class="chatroom-users__button"
title="Gift user bon (/gift <username> <amount> <message>)"
@click.prevent="$parent.forceGift(user.username)"
>
<i class="fas fa-gift"></i>
</button>
</li>
<li>
<button
class="chatroom-users__button"
title="Send chat PM (/msg <username> <message>)"
@click.prevent="$parent.forceMessage(user.username)"
>
<i class="fas fa-envelope"></i>
</button>
</li>
</menu>
</li>
</ul>
</section>
</template>
<script>
export default {
props: {
users: { required: true },
},
};
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +0,0 @@
<template>
<div class="form__group">
<select
id="currentChatroom"
class="form__select"
:class="chatrooms.some((chatroom) => chatroom.id == current) || 'form__select--default'"
v-model="selected"
@change="changedRoom"
>
<option
v-for="chatroom in chatrooms"
:value="chatroom.id"
:selected="selected == chatroom.id"
>
{{ chatroom.name }}
</option>
</select>
<label class="form__label form__label--floating" for="currentChatroom"> Room </label>
</div>
</template>
<script>
export default {
props: {
current: { type: Number, default: 1 },
chatrooms: { required: true },
},
data() {
return {
selected: 1,
};
},
methods: {
changedRoom(event) {
this.$emit('changedRoom', this.selected);
},
},
created() {
this.selected = this.current;
},
};
</script>

View File

@@ -1,42 +0,0 @@
<template>
<div class="form__group">
<select id="currentChatstatus" class="form__select" v-model="selected" @change="changedStatus">
<option
v-for="chatstatus in chatstatuses"
:value="chatstatus.id"
:selected="chatstatus.id == selected"
>
<i
:class="
chatstatus.icon ? chatstatus.icon + ' pointee mr-5' : 'fa fa-dot-circle-o pointee mr-5'
"
:style="`color: ${chatstatus.color}`"
></i>
{{ chatstatus.name }}
</option>
</select>
<label for="currentChatroom" class="form__label form__label--floating"> Status </label>
</div>
</template>
<script>
export default {
props: {
current: { type: Number, default: 1 },
chatstatuses: { required: true },
},
data() {
return {
selected: 1,
};
},
methods: {
changedStatus(event) {
this.$emit('changedStatus', this.selected);
},
},
created() {
this.selected = this.current;
},
};
</script>

View File

@@ -1,12 +1,4 @@
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
import Echo from 'laravel-echo';
import Vue from 'vue';
import chatbox from '../components/chat/Chatbox.vue';
import client from 'socket.io-client';
window.io = client;
@@ -19,8 +11,3 @@ window.Echo = new Echo({
transports: ['websocket'],
enabledTransports: ['wss'],
});
new Vue({
el: '#vue',
components: { chatbox: chatbox },
});

View File

@@ -78,7 +78,34 @@
padding: 0;
margin: 0;
list-style-type: none;
display: flex;
display: none;
}
/* Modified hover behavior to work with Alpine.js permission check */
.chatbox-message:hover .chatbox-message__menu {
display: block;
}
/* Ensure the delete button itself is visible on hover */
.chatbox-message__delete-button {
padding: 4px 12px;
margin: 0;
background-color: transparent;
text-align: center;
border: none;
color: transparent;
font-size: 14px;
cursor: pointer;
}
.chatbox-message:hover .chatbox-message__delete-button {
color: var(--chatbox-button-fg);
width: auto;
&:hover {
color: var(--chatbox-button-hover-fg);
width: auto;
}
}
.chatbox-message__header {
@@ -245,3 +272,63 @@
color: var(--chatbox-button-hover-fg);
}
}
/* Chat loading spinner */
.chatbox__spinner {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 300px;
width: 100%;
}
.spinner__dots {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 15px;
}
.spinner__dots .dot {
width: 12px;
height: 12px;
margin: 0 6px;
border-radius: 50%;
background-color: #65a6c2;
display: inline-block;
animation: pulse 1.5s infinite ease-in-out;
}
.spinner__dots .dot:nth-child(2) {
animation-delay: 0.2s;
}
.spinner__dots .dot:nth-child(3) {
animation-delay: 0.4s;
}
.spinner__dots .dot:nth-child(4) {
animation-delay: 0.6s;
}
.spinner__dots .dot:nth-child(5) {
animation-delay: 0.8s;
}
@keyframes pulse {
0%, 100% {
transform: scale(0.4);
opacity: 0.4;
}
50% {
transform: scale(1);
opacity: 1;
}
}
.spinner__text {
font-size: 16px;
color: #65a6c2;
font-weight: 500;
}

View File

@@ -1,3 +1,264 @@
<chatbox
:user="{{ App\Models\User::with(['chatStatus', 'chatroom', 'group'])->find(auth()->id()) }}"
></chatbox>
@php
$user = App\Models\User::with(['chatStatus', 'chatroom', 'group'])->find(auth()->id());
@endphp
<section
id="chatbody"
class="panelV2 chatbox"
x-data="chatbox(@js($user))"
:class="state.ui.fullscreen && 'chatbox--fullscreen'"
audio="false"
>
<div class="chatbox__spinner" x-show="state.ui.loading">
<div class="spinner__dots">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
<div class="spinner__text">Chatbox Loading</div>
</div>
<div x-show="!state.ui.loading">
<header class="panel__header" id="chatbox_header">
<h2 class="panel__heading">
<i class="fas fa-comment-dots"></i>
Chatbox
</h2>
<div class="panel__actions">
<div class="panel__action">
<button class="form__button form__button--text" @click.prevent="startBot()">
<i class="fa fa-robot"></i>
<span x-text="state.message.helpName"></span>
</button>
</div>
<div class="panel__action" x-show="state.chat.target < 1 && state.chat.bot < 1">
<button class="form__button form__button--text" @click.prevent="changeTab('list', 'userlist')">
<i class="fa fa-users"></i>
Users: <span x-text="users.length"></span>
</button>
</div>
<div class="panel__action">
<template x-if="state.chat.room && state.chat.room > 0 && state.chat.bot < 1 && state.chat.target < 1 && state.chat.tab != 'userlist'">
<button class="form__button form__standard-icon-button form__standard-icon-button--skinny"
@click.prevent="changeAudible('room', state.chat.room, state.chat.listening ? 0 : 1)"
:style="'color: ' + (state.chat.listening ? 'rgb(0,102,0)' : 'rgb(204,0,0)')">
<i :class="state.chat.listening ? 'fa fa-bell' : 'fa fa-bell-slash'"></i>
</button>
</template>
<template x-if="state.chat.bot && state.chat.bot >= 1 && state.chat.target < 1 && state.chat.tab != 'userlist'">
<button class="form__button form__standard-icon-button form__standard-icon-button--skinny"
@click.prevent="changeAudible('bot', state.chat.bot, state.chat.listening ? 0 : 1)"
:style="'color: ' + (state.chat.listening ? 'rgb(0,102,0)' : 'rgb(204,0,0)')">
<i :class="state.chat.listening ? 'fa fa-bell' : 'fa fa-bell-slash'"></i>
</button>
</template>
<template x-if="state.chat.target && state.chat.target >= 1 && state.chat.bot < 1 && state.chat.tab != 'userlist'">
<button class="form__button form__standard-icon-button form__standard-icon-button--skinny"
@click.prevent="changeAudible('target', state.chat.target, state.chat.listening ? 0 : 1)"
:style="'color: ' + (state.chat.listening ? 'rgb(0,102,0)' : 'rgb(204,0,0)')">
<i :class="state.chat.listening ? 'fa fa-bell' : 'fa fa-bell-slash'"></i>
</button>
</template>
</div>
<div class="panel__action">
<button class="form__button form__standard-icon-button form__standard-icon-button--skinny"
title="Toggle typing notifications"
@click.prevent="changeWhispers()"
:style="'color: ' + (state.chat.showWhispers ? 'rgb(0,102,0)' : 'rgb(204,0,0)')">
<i :class="state.chat.showWhispers ? 'fas fa-keyboard' : 'fa fa-keyboard'"></i>
</button>
</div>
<div class="panel__action">
<div class="form__group">
<select id="currentChatroom" class="form__select" x-model.number="state.chat.room" @change="changeRoom(state.chat.room)">
<template x-for="chatroom in chatrooms" :key="chatroom.id">
<option :value="chatroom.id" x-text="chatroom.name"></option>
</template>
</select>
<label class="form__label form__label--floating" for="currentChatroom"> Room </label>
</div>
</div>
<div class="panel__action">
<div class="form__group">
<select id="currentChatstatus" class="form__select" x-model.number="status" @change="changeStatus(status)">
<template x-for="chatstatus in statuses" :key="chatstatus.id">
<option :value="chatstatus.id" x-text="chatstatus.name"></option>
</template>
</select>
<label class="form__label form__label--floating" for="currentChatstatus"> Status </label>
</div>
</div>
<div class="panel__action">
<button id="panel-fullscreen" class="form__button form__standard-icon-button"
title="Toggle Fullscreen" @click.prevent="changeFullscreen()">
<i :class="state.ui.fullscreen ? 'fas fa-compress' : 'fas fa-expand'"></i>
</button>
</div>
</div>
</header>
<menu id="chatbox_tabs" class="panel__tabs" role="tablist" x-show="boot == 1">
<template x-for="(echo, idx) in echoes" :key="echo.room && echo.room.id ? echo.room.id : 'room-' + idx">
<li x-show="echo.room && echo.room.name && echo.room.name.length > 0"
class="panel__tab chatbox__tab"
:class="state.chat.tab && echo.room && state.chat.tab === echo.room.name && 'panel__tab--active'"
role="tab"
@click.prevent="changeTab('room', echo.room.id)">
<i class="fa fa-comment" :class="checkPings('room', echo.room && echo.room.id ? echo.room.id : 0) ? 'fa-beat text-success' : 'text-danger'"></i>
<span x-text="echo.room && echo.room.name ? echo.room.name : ''"></span>
<button x-show="state.chat.tab && echo.room && state.chat.tab === echo.room.name" class="chatbox__tab-delete-button" @click.prevent="leaveRoom(state.chat.room)">
<i class="fa fa-times chatbox__tab-delete-icon"></i>
</button>
</li>
</template>
<template x-for="(echo, idx) in echoes" :key="echo.target && echo.target.id ? echo.target.id : 'target-' + idx">
<li x-show="echo.target && echo.target.id >= 3 && echo.target.username && echo.target.username.length > 0"
class="panel__tab chatbox__tab"
:class="state.chat.target >= 3 && echo.target && state.chat.target === echo.target.id && 'panel__tab--active'"
role="tab"
@click.prevent="changeTab('target', echo.target.id)">
<i class="fa fa-comment" :class="checkPings('target', echo.target && echo.target.id ? echo.target.id : 0) ? 'fa-beat text-success' : 'text-danger'"></i>
@<span x-text="echo.target && echo.target.username ? echo.target.username : ''"></span>
<button x-show="state.chat.target >= 3 && echo.target && state.chat.target === echo.target.id" class="chatbox__tab-delete-button" @click.prevent="leaveTarget(state.chat.target)">
<i class="fa fa-times chatbox__tab-delete-icon"></i>
</button>
</li>
</template>
<template x-for="(echo, idx) in echoes" :key="echo.bot && echo.bot.id ? echo.bot.id : 'bot-' + idx">
<li x-show="echo.bot && echo.bot.id >= 1 && echo.bot.name && echo.bot.name.length > 0"
class="panel__tab chatbox__tab"
:class="state.chat.bot > 0 && echo.bot && state.chat.bot === echo.bot.id && 'panel__tab--active'"
role="tab"
@click.prevent="changeTab('bot', echo.bot.id)">
<i class="fa fa-comment" :class="checkPings('bot', echo.bot && echo.bot.id ? echo.bot.id : 0) ? 'fa-beat text-success' : 'text-danger'"></i>
@<span x-text="echo.bot && echo.bot.name ? echo.bot.name : ''"></span>
<button x-show="state.chat.bot > 0 && echo.bot && state.chat.bot === echo.bot.id" class="chatbox__tab-delete-button" @click.prevent="leaveBot(state.chat.bot)">
<i class="fa fa-times chatbox__tab-delete-icon"></i>
</button>
</li>
</template>
</menu>
<div class="chatbox__chatroom" x-show="!state.ui.connecting">
<template x-if="state.chat.tab !== '' && state.chat.tab !== 'userlist'">
<>
<div class="chatroom__messages--wrapper" x-ref="messagesWrapper">
<ul class="chatroom__messages">
<template x-for="message in messages" :key="message.id">
<li>
<article class="chatbox-message">
<header class="chatbox-message__header">
<address class="chatbox-message__address user-tag"
:style="(message.user?.is_lifetime ? 'background-image: url(/img/sparkels.gif);' : (message.user?.group?.effect ? 'background-image:' + message.user.group.effect + ';' : ''))">
<a class="user-tag__link"
:class="message.user?.group?.icon"
:href="message.user?.username ? '/users/' + message.user.username : ''"
:style="message.user?.group?.color ? 'color:' + message.user.group.color : ''"
:title="message.user?.group?.name">
<span x-show="message.user && message.user.id > 1" style="padding-right: 5px;" x-text="message.user?.username || 'Unknown'"></span>
<span x-show="message.bot && message.bot.id >= 1 && (!message.user || message.user.id < 2)" x-text="message.bot?.name || 'Unknown'"></span>
<i x-show="message.user?.icon !== null && message.user?.icon !== undefined">
<img :style="'max-height: 16px; vertical-align: text-bottom;'"
title="Custom User Icon"
:src="'/authenticated-images/user-icons/' + message.user.username" />
</i>
<i x-show="message.user?.is_lifetime == 1" class="fal fa-star" id="lifeline" title="Lifetime Donor"></i>
<i x-show="message.user?.is_donor == 1 && message.user?.is_lifetime == 0" class="fal fa-star text-gold" title="Donor"></i>
</a>
</address>
<div x-show="message.bot && message.bot.id >= 1 && (!message.user || message.user.id < 2)"
class="bbcode-rendered bot-message"
style="font-style: italic; white-space: nowrap; display: inline;"
x-html="message.message"></div>
<time x-show="message.bot && message.bot.id >= 1 && (!message.user || message.user.id < 2)"
style="margin-left: 10px; white-space: nowrap; display: inline;"
class="chatbox-message__time"
:datetime="message.created_at"
:title="message.created_at"
x-text="formatTime(message.created_at)"></time>
<time x-show="!(message.bot && message.bot.id >= 1 && (!message.user || message.user.id < 2))"
class="chatbox-message__time"
:datetime="message.created_at"
:title="message.created_at"
x-text="formatTime(message.created_at)"></time>
</header>
<aside class="chatbox-message__aside">
<figure class="chatbox-message__figure">
<i class="fa fa-bell" title="System Notification"
x-show="message.bot && message.bot.id >= 1 && (!message.user || message.user.id < 2)"></i>
<a x-show="message.user && message.user.id != 1"
:href="'/users/' + message.user.username"
class="chatbox-message__avatar-link">
<img x-show="message.user && message.user.id != 1"
class="chatbox-message__avatar"
:src="message.user?.image ? '/authenticated-images/user-avatars/' + message.user.username : '/img/profile.png'"
:style="'border: 2px solid ' + (message.user?.chat_status?.color || '#ccc')"
:title="message.user?.chat_status?.name" />
</a>
</figure>
</aside>
<section class="chatbox-message__content bbcode-rendered"
x-show="!(message.bot && message.bot.id >= 1 && (!message.user || message.user.id < 2))"
x-html="message.message"></section>
<!-- Move menu back to original position after timestamp -->
<menu class="chatbox-message__menu" x-show="message.canMod === true || message.canMod === 1">
<li class="chatbox-message__menu-item">
<button class="chatbox-message__delete-button" title="Delete Message"
@click.prevent="deleteMessage(message.id)"
style="cursor: pointer; padding: 0; margin-left: 8px;">
<i class="fa fa-trash"></i>
</button>
</li>
</menu>
</article>
</li>
</template>
<li x-show="messages.length === 0">There is no chat history here. Send a message!</li>
</ul>
</div>
</>
</template>
<template x-if="state.chat.tab === 'userlist'">
<section class="chatroom__users">
<h2 class="chatroom-users__heading">Users</h2>
<ul class="chatroom-users__list">
<template x-for="user in users" :key="user.id">
<li class="chatroom-users__list-item">
<span class="chatroom-users__user user-tag">
<a class="chatroom-users__user-link user-tag__link" :href="'/users/' + user.username">
<span x-text="user.username"></span>
</a>
</span>
<menu class="chatroom-users__buttons" x-show="auth.id !== user.id">
<li>
<button class="chatroom-users__button" title="Gift user bon" @click.prevent="forceGift(user.username)">
<i class="fas fa-gift"></i>
</button>
</li>
<li>
<button class="chatroom-users__button" title="Send chat PM" @click.prevent="forceMessage(user.username)">
<i class="fas fa-envelope"></i>
</button>
</li>
</menu>
</li>
</template>
</ul>
</section>
</template>
<section class="chatroom__whispers" x-show="state.chat.showWhispers">
<span x-show="state.chat.target < 1 && state.chat.bot < 1 && activePeer && activePeer.username != ''">
<span x-text="activePeer ? activePeer.username + ' is typing ...' : '*' "></span>
</span>
</section>
<form class="form chatroom__new-message" @submit.prevent="createMessage($refs.message.value, true, auth.id, state.message.receiver_id, state.message.bot_id)">
<p class="form__group">
<textarea id="chatbox__messages-create" class="form__textarea" name="message" placeholder=" " x-ref="message"
@keydown.enter.prevent="createMessage($refs.message.value, true, auth.id, state.message.receiver_id, state.message.bot_id); $refs.message.value = ''"
@keyup="isTyping(auth)"></textarea>
<label class="form__label form__label--floating" for="chatbox__messages-create">Write your message...</label>
</p>
</form>
</div>
</div>
</section>

View File

@@ -1,6 +1,5 @@
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue2';
import { viteStaticCopy } from 'vite-plugin-static-copy';
export default defineConfig({
@@ -43,19 +42,6 @@ export default defineConfig({
dest: 'unit3d'
}]
}),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false,
},
},
}),
],
resolve: {
alias: {
vue: 'vue/dist/vue.esm.js',
}
}
});