1
0
mirror of https://github.com/citizenfx/cfx-server-data.git synced 2025-03-12 23:07:20 +08:00

Betterchat v1

This commit is contained in:
Jonas Dellinger 2017-05-17 19:22:20 +02:00
parent aeab2edc5c
commit ffa9488e9a
16 changed files with 591 additions and 484 deletions

View File

@ -0,0 +1,3 @@
# betterchat
> Here will be some kind of documentation

View File

@ -1,15 +1,15 @@
description 'chat management stuff'
ui_page 'html/chat.html'
client_script 'chat_client.lua'
server_script 'chat_server.lua'
export 'printChatLine'
files {
'html/chat.html',
'html/chat.css',
'html/chat.js',
'html/jquery.faketextbox.js'
}
description 'better chat management stuff'
ui_page 'html/index.html'
client_script 'cl_chat.lua'
server_script 'sv_chat.lua'
files {
'html/index.html',
'html/index.css',
'html/config.js',
'html/App.js',
'html/Message.js',
'html/Suggestions.js'
}

View File

@ -1,56 +0,0 @@
local chatInputActive = false
local chatInputActivating = false
RegisterNetEvent('chatMessage')
AddEventHandler('chatMessage', function(name, color, message)
SendNUIMessage({
name = name,
color = color,
message = message
})
end)
RegisterNUICallback('chatResult', function(data, cb)
chatInputActive = false
SetNuiFocus(false)
if data.message then
local id = PlayerId()
--local r, g, b = GetPlayerRgbColour(id, _i, _i, _i)
local r, g, b = 0, 0x99, 255
TriggerServerEvent('chatMessageEntered', GetPlayerName(id), { r, g, b }, data.message)
end
cb('ok')
end)
Citizen.CreateThread(function()
SetTextChatEnabled(false)
while true do
Wait(0)
if not chatInputActive then
if IsControlPressed(0, 245) --[[ INPUT_MP_TEXT_CHAT_ALL ]] then
chatInputActive = true
chatInputActivating = true
SendNUIMessage({
meta = 'openChatBox'
})
end
end
if chatInputActivating then
if not IsControlPressed(0, 245) then
SetNuiFocus(true)
chatInputActivating = false
end
end
end
end)

View File

@ -1,50 +0,0 @@
RegisterServerEvent('chatCommandEntered')
RegisterServerEvent('chatMessageEntered')
AddEventHandler('chatMessageEntered', function(name, color, message)
if not name or not color or not message or #color ~= 3 then
return
end
TriggerEvent('chatMessage', source, name, message)
if not WasEventCanceled() then
TriggerClientEvent('chatMessage', -1, name, color, message)
end
print(name .. ': ' .. message)
end)
-- player join messages
AddEventHandler('playerActivated', function()
TriggerClientEvent('chatMessage', -1, '', { 0, 0, 0 }, '^2* ' .. GetPlayerName(source) .. ' joined.')
end)
AddEventHandler('playerDropped', function(reason)
TriggerClientEvent('chatMessage', -1, '', { 0, 0, 0 }, '^2* ' .. GetPlayerName(source) ..' left (' .. reason .. ')')
end)
-- say command handler
AddEventHandler('rconCommand', function(commandName, args)
if commandName == "say" then
local msg = table.concat(args, ' ')
TriggerClientEvent('chatMessage', -1, 'console', { 0, 0x99, 255 }, msg)
RconPrint('console: ' .. msg .. "\n")
CancelEvent()
end
end)
-- tell command handler
AddEventHandler('rconCommand', function(commandName, args)
if commandName == "tell" then
local target = table.remove(args, 1)
local msg = table.concat(args, ' ')
TriggerClientEvent('chatMessage', tonumber(target), 'console', { 0, 0x99, 255 }, msg)
RconPrint('console: ' .. msg .. "\n")
CancelEvent()
end
end)

View File

@ -0,0 +1,85 @@
local chatInputActive = false
local chatInputActivating = false
RegisterNetEvent('suggestionAdd')
RegisterNetEvent('chatMessage')
RegisterNetEvent('chatMessageEx')
AddEventHandler('chatMessage', function(author, color, text)
if author == "" then
author = false
end
SendNUIMessage({
type = 'ON_MESSAGE',
message = {
color = color,
multiline = true,
args = { author, text }
}
})
end)
AddEventHandler('chatMessageEx', function(message)
SendNUIMessage({
type = 'ON_MESSAGE',
message = message
})
end)
AddEventHandler('suggestionAdd', function(name, help, params)
Citizen.Trace(name)
SendNUIMessage({
type = 'ON_SUGGESTION_ADD',
suggestion = {
name = name,
help = help,
params = params or nil
}
})
end)
RegisterNUICallback('chatResult', function(data, cb)
chatInputActive = false
SetNuiFocus(false)
if not data.canceled then
local id = PlayerId()
TriggerServerEvent('chatMessageEntered', GetPlayerName(id), data.message)
end
cb('ok')
end)
RegisterNUICallback('loaded', function(data, cb)
TriggerServerEvent('chatInit');
cb('ok')
end)
Citizen.CreateThread(function()
SetTextChatEnabled(false)
while true do
Wait(0)
if not chatInputActive then
if IsControlPressed(0, 245) --[[ INPUT_MP_TEXT_CHAT_ALL ]] then
chatInputActive = true
chatInputActivating = true
SendNUIMessage({
type = 'ON_OPEN'
})
end
end
if chatInputActivating then
if not IsControlPressed(0, 245) then
SetNuiFocus(true)
chatInputActivating = false
end
end
end
end)

View File

@ -0,0 +1,166 @@
window.APP = {
template: '#app_template',
name: 'app',
data() {
return {
showInput: false,
showWindow: false,
suggestions: [],
message: '',
messages: [],
oldMessages: [],
oldMessagesIndex: -1,
};
},
destroyed() {
clearInterval(this.focusTimer);
window.removeEventListener('message', this.listener);
},
mounted() {
axios.post('http://betterchat/loaded', {});
this.listener = window.addEventListener('message', (event) => {
const item = event.data || event.detail; //'detail' is for debuging via browsers
if (this[item.type]) {
this[item.type](item);
}
});
},
watch: {
messages() {
if (this.showWindowTimer) {
clearTimeout(this.showWindowTimer);
}
this.showWindow = true;
this.showWindowTimer = setTimeout(() => {
if (!this.showInput) {
this.showWindow = false;
}
}, window.CONFIG.fadeTimeout);
const messagesObj = this.$refs.messages;
this.$nextTick(() => {
messagesObj.scrollTop = messagesObj.scrollHeight;
});
},
},
methods: {
ON_OPEN() {
this.showInput = true;
this.showWindow = true;
if (this.showWindowTimer) {
clearTimeout(this.showWindowTimer);
}
this.focusTimer = setInterval(() => {
if (this.$refs.input) {
this.$refs.input.focus();
} else {
clearInterval(this.focusTimer);
}
}, 100);
},
ON_MESSAGE(data) {
this.messages.push(data.message);
},
ON_SUGGESTION_ADD(data) {
const suggestion = data.suggestion;
if (!suggestion.params) {
suggestion.params = [];
}
this.suggestions.push(suggestion);
},
ON_SUGGESTION_REMOVE() {
},
keyUp() {
this.resize();
},
keyDown(e) {
if (e.which === 38 || e.which === 40) {
e.preventDefault();
this.moveOldMessageIndex(e.which === 38);
}
},
moveOldMessageIndex(up) {
if (up && this.oldMessages.length > this.oldMessagesIndex + 1) {
this.oldMessagesIndex += 1;
this.message = this.oldMessages[this.oldMessagesIndex];
} else if (!up && this.oldMessagesIndex - 1 >= 0) {
this.oldMessagesIndex -= 1;
this.message = this.oldMessages[this.oldMessagesIndex];
} else if (!up && this.oldMessagesIndex - 1 === -1) {
this.oldMessagesIndex = -1;
this.message = '';
}
},
resize() {
const input = this.$refs.input;
input.style.height = '5px';
input.style.height = `${input.scrollHeight + 2}px`;
},
addLine() {
this.message += '\n';
this.resize();
},
send(e) {
if (e.shiftKey || this.message === '') {
return;
}
axios.post('http://betterchat/chatResult', {
message: this.message,
});
this.oldMessages.unshift(this.message);
this.message = '';
this.showInput = false;
this.showWindowTimer = setTimeout(() => {
this.showWindow = false;
}, window.CONFIG.fadeTimeout);
},
hideInput(canceled) {
if (canceled) {
axios.post('http://betterchat/chatResult', {
canceled,
});
}
this.showInput = false;
clearInterval(this.focusTimer);
this.showWindowTimer = setTimeout(() => {
this.showWindow = false;
}, window.CONFIG.fadeTimeout);
},
},
components: {
Message: window.MESSAGE,
Suggestions: window.SUGGESTIONS,
},
};
window.emulate_open = () => {
window.dispatchEvent(new CustomEvent('message', {
detail: {
type: 'ON_OPEN',
},
}));
};
window.emulate_suggestion = (name, help, params = []) => {
window.dispatchEvent(new CustomEvent('message', {
detail: {
type: 'ON_SUGGESTION_ADD',
suggestion: {
name,
help,
params,
},
},
}));
};
window.emulate_message = (message) => {
window.dispatchEvent(new CustomEvent('message', {
detail: {
type: 'ON_MESSAGE',
message,
},
}));
};

View File

@ -0,0 +1,43 @@
window.MESSAGE = {
template: '#message_template',
data() {
return {};
},
computed: {
textEscaped() {
return this.template.replace(/{(\d+)}/g, (match, number) => {
return this.args[number] != undefined ? this.escapeHtml(this.args[number]) : match
});
},
},
created() {
},
methods: {
escapeHtml(unsafe) {
return unsafe
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
},
},
props: {
args: {
},
template: {
type: String,
default: window.CONFIG.defaultTemplate,
},
multiline: {
type: Boolean,
default: false,
},
color: {
type: String,
},
},
};

View File

@ -0,0 +1,46 @@
window.SUGGESTIONS = {
template: '#suggestions_template',
props: ['message', 'suggestions'],
data() {
return {};
},
computed: {
currentSuggestions() {
if (this.message === '') {
return [];
}
const currentSuggestions = this.suggestions.filter((s) => {
if (!s.name.startsWith(this.message)) {
const suggestionSplitted = s.name.split(' ');
const messageSplitted = this.message.split(' ');
for (let i = 0; i < messageSplitted.length; i += 1) {
if (i >= suggestionSplitted.length) {
return i < suggestionSplitted.length + s.params.length;
}
if (suggestionSplitted[i] !== messageSplitted[i]) {
return false;
}
}
return true;
}
return true;
}).slice(0, 5);
currentSuggestions.forEach((s) => {
// eslint-disable-next-line no-param-reassign
s.disabled = !s.name.startsWith(this.message);
s.params.forEach((p, index) => {
const wType = (index === s.params.length - 1) ? '.' : '\\S';
const regex = new RegExp(`${s.name} (?:\\w+ ){${index}}(?:${wType}*)$`, 'g');
// eslint-disable-next-line no-param-reassign
p.disabled = this.message.match(regex) == null;
});
});
return currentSuggestions;
},
},
methods: {},
};

View File

@ -1,102 +0,0 @@
body
{
background-color: transparent;
margin: 0px;
}
ul
{
margin: 0px;
padding: 0px;
list-style-type: none; /* hii */
}
#chat
{
position: absolute;
top: 30px;
left: 30px;
padding: 7px;
width: 30%;
font-size: 20px;
font-family: "Segoe UI", "Segoe UI Symbol", "Segoe UI Emoji", Arial, sans-serif;
color: #fff;
overflow: hidden;
text-shadow: 0px 0px 1px #333;
}
input.fake
{
position: absolute;
top: -10000px;
left: -10000px;
}
#chatInputHas
{
display: none;
}
#chatInputHas strong
{
display: inline-block;
vertical-align: bottom;
text-transform: uppercase;
height: 29px;
line-height: 26px;
}
#chatInput
{
background-color: transparent;
border: none;
outline: none !important;
padding: 3px;
display: inline-block;
vertical-align: middle;
margin: 0px;
color: #fff;
text-shadow: 0px 0px 1px #333;
font: inherit;
white-space: pre;
overflow: hidden;
}
#chatInput * > div:first-child
{
display: inline-block;
vertical-align: middle;
height: 23px;
line-height: 20px;
}
#chatInput .caret
{
display: inline-block;
min-width: 1px;
height: 22px;
margin-left: 1px;
vertical-align: bottom;
background-color: #fff;
box-shadow: 0px 0px 1px #333;
color: white;
}
#chatBuffer
{
height: 240px;
overflow: hidden;
}
.color-1{color: #ff4444;}
.color-2{color: #99cc00;}
.color-3{color: #ffbb33;}
.color-4{color: #0099cc;}
.color-5{color: #33b5e5;}
.color-6{color: #aa66cc;}
.color-8{color: #cc0000;}
.color-9{color: #cc0000;}

View File

@ -1,20 +0,0 @@
<html>
<head>
<script src="nui://game/ui/jquery.js" type="text/javascript"></script>
<script src="jquery.faketextbox.js" type="text/javascript"></script>
<script src="chat.js" type="text/javascript"></script>
<link href="chat.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="chat">
<div id="chatBuffer">
<ul>
</ul>
</div>
<div id="chatInputHas">
<strong>Chat</strong>
<div id="chatInput" />
</div>
</div>
</body>
</html>

View File

@ -1,138 +0,0 @@
function colorize(str)
{
const s = "<span>" + (str.replace(/\^([0-9])/g, (str, color) => `</span><span class="color-${color}">`)) + "</span>";
return s.replace(/<span[^>]*><\/span[^>]*>/g, '');
}
$(function()
{
var chatHideTimeout;
var inputShown = false;
function startHideChat()
{
if (chatHideTimeout)
{
clearTimeout(chatHideTimeout);
}
if (inputShown)
{
return;
}
chatHideTimeout = setTimeout(function()
{
if (inputShown)
{
return;
}
$('#chat').animate({ opacity: 0 }, 300);
}, 7000);
}
handleResult = function(elem, wasEnter)
{
inputShown = false;
$('#chatInputHas').hide();
startHideChat();
var obj = {};
if (wasEnter)
{
obj = { message: $(elem).val() };
}
$(elem).val('');
$.post('http://chat/chatResult', JSON.stringify(obj), function(data)
{
console.log(data);
});
};
$('#chatInput').fakeTextbox(); // //
$('#chatInput')[0].onPress(function(e)
{
if (e.which == 13)
{
handleResult(this, true);
}
});
$(document).keyup(function(e)
{
if (e.keyCode == 27)
{
handleResult($('#chatInput')[0].getTextBox(), false);
}
});
$(document).keydown(function(e)
{
if (e.keyCode == 9)
{
e.preventDefault();
return false;
}
else if (e.keyCode == 33)
{
let buf = $('#chatBuffer');
buf.scrollTop(buf.scrollTop() - 50);
}
else if (e.keyCode == 34)
{
let buf = $('#chatBuffer');
buf.scrollTop(buf.scrollTop() + 50);
}
});
window.addEventListener('message', function(event)
{
var item = event.data;
if (item.meta && item.meta == 'openChatBox')
{
inputShown = true;
$('#chat').stop().css('opacity', '1');
$('#chatInputHas').show();
$('#chatInput')[0].doFocus();
return;
}
// TODO: use some templating stuff for this
var colorR = parseInt(item.color[0]);
var colorG = parseInt(item.color[1]);
var colorB = parseInt(item.color[2]);
var name = item.name.replace('<', '&lt;');
var message = item.message.replace('<', '&lt;');
name = colorize(name);
message = colorize(message);
var buf = $('#chatBuffer');
var nameStr = '';
if (name != '')
{
nameStr = '<strong style="color: rgb(' + colorR + ', ' + colorG + ', ' + colorB + ')">' + name + ': </strong>';
}
buf.find('ul').append('<li>' + nameStr + message + '</li>');
buf.scrollTop(buf[0].scrollHeight - buf.height());
$('#chat').stop().css('opacity', '1');
startHideChat();
}, false);
});

View File

@ -0,0 +1,4 @@
window.CONFIG = {
defaultTemplate: '<b>{0}</b>: {1}',
fadeTimeout: 7000,
};

View File

@ -0,0 +1,105 @@
* {
font-family: 'Lato', sans-serif;
margin: 0;
padding: 0;
}
.no-grow {
flex-grow: 0;
}
#app {
font-family: 'Lato', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: white;
}
.chat-window {
position: absolute;
top: 15px;
left: 15px;
width: 38%;
height: 240px;
background-color: rgba(52, 73, 94, 0.7);
-webkit-animation-duration: 2s;
}
.chat-messages {
position: relative;
height: 95%;
font-size: 1.2rem;
margin: 5px;
overflow-x: hidden;
overflow-y: hidden;
}
.chat-input {
font-size: 1.2rem;
position: absolute;
top: 257px;
left: 15px;
width: 38%;
box-sizing: border-box;
}
.prefix {
position: absolute;
margin-top: 2px;
left: 4px;
}
textarea {
font-size: 1.1rem;
display: block;
box-sizing: border-box;
padding: 5px;
padding-left: 27px;
color: white;
background-color: rgba(44, 62, 80, 1.0);
width: 100%;
border-width: 0;
height: 34px;
overflow: hidden;
}
.msg {
margin-bottom: 3px;
}
.multiline {
margin-left: 1.5rem;
text-indent: -1.5rem;
white-space: pre-line;
}
.suggestions {
list-style-type: none;
padding: 5px;
padding-left: 27px;
font-size: 1.1rem;
box-sizing: border-box;
color: white;
background-color: rgba(44, 62, 80, 1.0);
width: 100%;
}
.help {
color: #b0bbbd;
}
.disabled {
color: #b0bbbd;
}
.suggestion {
margin-bottom: 5px;
}

View File

@ -0,0 +1,96 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<link rel="stylesheet" href="index.css"></link>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css"></link>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css"></link>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.1/axios.min.js"></script>
<script type="text/javascript" src="config.js"></script>
</head>
<body>
<div id="app"></div>
<!-- App Template -->
<script type="text/x-template" id="app_template">
<div id="app">
<div class="chat-window" :class="{ 'fadeOut animated': !showWindow }">
<div class="chat-messages" ref="messages">
<message v-for="msg in messages"
:multiline="msg.multiline"
:args="msg.args"
:template="msg.template"
:key="msg">
</message>
</div>
</div>
<div class="chat-input" v-show="showInput">
<span class="prefix"></span>
<textarea v-model="message"
ref="input"
type="text"
value="/help"
autofocus
@keyup.esc="hideInput"
@keyup="keyUp"
@keydown="keyDown"
@keypress.enter.none.prevent="send"
@keypress.enter.shift.prevent="addLine">
</textarea>
<suggestions :message="message" :suggestions="suggestions">
</suggestions>
</div>
</div>
</script>
<!-- Message Template -->
<script type="text/x-template" id="message_template">
<div class="msg" :class="{ multiline }">
<span v-html="textEscaped"></span>
</div>
</script>
<!-- Suggestions Template -->
<script type="text/x-template" id="suggestions_template">
<ul class="suggestions" v-show="currentSuggestions.length > 0">
<li class="suggestion" v-for="s in currentSuggestions">
<p>
<span :class="{ 'disabled': s.disabled }">
{{s.name}}
</span>
<span class="param"
v-for="(p, index) in s.params"
:class="{ 'disabled': p.disabled }">
[{{p.name}}]
</span>
</p>
<small class="help">
<template v-if="!s.disabled">
{{s.help}}
</template>
<template v-for="p in s.params" v-if="!p.disabled">
{{p.help}}
</template>
</small>
</li>
</ul>
</script>
<!-- Scripts -->
<script type="text/javascript" src="./Suggestions.js"></script>
<script type="text/javascript" src="./Message.js"></script>
<script type="text/javascript" src="./App.js"></script>
<!-- Main Entry -->
<script type="text/javascript">
const instance = new Vue({
el: '#app',
render: h => h(window.APP),
});
window.instance = instance;
</script>
</body>
</html>

View File

@ -1,103 +0,0 @@
(function ($) {
$.fn.fakeTextbox = function () {
return this.each(function () {
var $me = $(this),
cursorTimer,
$tb = $('<input type="text" class="fake" />');
if ($me.data('ftbftw')) {
console.log('already initialized');
return;
}
$me.data('ftbftw', 1);
$tb.insertAfter($me);
function appendCaret(toHere, position, selStart, selEnd) {
if (position === selStart) {
toHere += "</div><div class='caret'>";
}
if (position === selEnd) {
toHere += "</div><div>";
}
return toHere;
}
function syncTextbox() {
var tbVal = $tb.val().replace('<', '&lt;');
var tbLen = tbVal.length;
var selStart = $tb.get(0).selectionStart;
var selEnd = $tb.get(0).selectionEnd;
var newOut = '<div>';
for (var i = 0; i < tbLen; i++) {
newOut = appendCaret(newOut, i, selStart, selEnd);
newOut += tbVal[i];
}
$me.html(colorize(appendCaret(newOut, i, selStart, selEnd) + '</div>'));
if (selStart != selEnd) {
$('.caret', $me).addClass('selection');
}
}
$me.click(function () {
$tb.focus();
});
$tb.bind("change keypress keyup", function()
{
setTimeout(syncTextbox, 1); //
})
.blur(function () {
clearInterval(cursorTimer);
cursorTimer = null;
var $cursor = $('.caret', $me);
$cursor.css({
visibility: 'visible'
});
$me.removeClass('focused');
}).focus(function () {
if (!cursorTimer) {
$me.addClass('focused');
cursorTimer = window.setInterval(function () {
var $cursor = $('.caret', $me);
if ($cursor.hasClass('selection') || $cursor.css('visibility') === 'hidden') {
$cursor.css({
visibility: 'visible'
});
} else {
$cursor.css({
visibility: 'hidden'
});
}
}, 500);
}
});
this.doFocus = function()
{
$tb.focus();
};
this.onPress = function(f)
{
$tb.bind('keypress', f);
};
this.getTextBox = function()
{
return $tb;
};
syncTextbox();
if ($me.hasClass('initFocus')) {
$tb.focus();
}
});
};
}(jQuery));

View File

@ -0,0 +1,28 @@
RegisterServerEvent('chatCommandEntered')
RegisterServerEvent('chatMessageEntered')
RegisterServerEvent('initialSuggestions')
AddEventHandler('chatMessageEntered', function(author, message)
if not message or not author then
return
end
TriggerEvent('chatMessage', source, author, message)
if not WasEventCanceled() then
print("No cancel")
TriggerClientEvent('chatMessage', -1, author, { 0, 0, 0 }, message)
end
print(author .. ': ' .. message)
end)
-- player join messages
AddEventHandler('playerActivated', function()
TriggerClientEvent('chatMessage', -1, '', { 0, 0, 0 }, '^2* ' .. GetPlayerName(source) .. ' joined.')
end)
AddEventHandler('playerDropped', function(reason)
TriggerClientEvent('chatMessage', -1, '', { 0, 0, 0 }, '^2* ' .. GetPlayerName(source) ..' left (' .. reason .. ')')
end)