mirror of
https://github.com/HDInnovations/UNIT3D.git
synced 2026-01-31 01:35:31 +01:00
refactor: chatbox to alpinejs
- This removes Vue completely from unit3d.
This commit is contained in:
@@ -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
479
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
721
resources/js/components/alpine/chatbox.js
Normal file
721
resources/js/components/alpine/chatbox.js
Normal 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();
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user