diff --git a/resources/[gameplay]/chat-theme-gtao/style.css b/resources/[gameplay]/chat-theme-gtao/style.css index c2c0fa3..5e28f97 100644 --- a/resources/[gameplay]/chat-theme-gtao/style.css +++ b/resources/[gameplay]/chat-theme-gtao/style.css @@ -85,7 +85,9 @@ } .chat-input > div { - background-color: rgba(0, 0, 0, .6); + background-color: rgba(0, 0, 0, .6) !important; + border: calc(0.28vh / 2) solid rgba(180, 180, 180, .6); + outline: calc(0.28vh / 2) solid rgba(0, 0, 0, .8); /* to replace margin-background */ padding: calc(0.28vh / 2); } @@ -93,6 +95,19 @@ margin: 0; margin-left: 0.7%; margin-top: -0.1%; + line-height: 2.8vh; +} + +.chat-input .prefix.any { + opacity: 0.8; +} + +.chat-input .prefix.any:before { + content: '['; +} + +.chat-input .prefix.any:after { + content: ']'; } .chat-input > div + div { @@ -110,9 +125,7 @@ textarea { background: transparent; - border: calc(0.28vh / 2) solid rgba(180, 180, 180, .6); - padding: calc(0.28vh / 2); - padding-left: calc(3.5% + (0.28vh / 2)); + padding: 0.5vh; } @media screen and (min-aspect-ratio: 21/9) { diff --git a/resources/[gameplay]/chat/.gitignore b/resources/[gameplay]/chat/.gitignore new file mode 100644 index 0000000..682c87a --- /dev/null +++ b/resources/[gameplay]/chat/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +yarn-error.log +dist/ \ No newline at end of file diff --git a/resources/[gameplay]/chat/cl_chat.lua b/resources/[gameplay]/chat/cl_chat.lua index f42d441..09e3eac 100644 --- a/resources/[gameplay]/chat/cl_chat.lua +++ b/resources/[gameplay]/chat/cl_chat.lua @@ -2,7 +2,6 @@ local isRDR = not TerraingridActivate and true or false local chatInputActive = false local chatInputActivating = false -local chatHidden = true local chatLoaded = false RegisterNetEvent('chatMessage') @@ -10,6 +9,8 @@ RegisterNetEvent('chat:addTemplate') RegisterNetEvent('chat:addMessage') RegisterNetEvent('chat:addSuggestion') RegisterNetEvent('chat:addSuggestions') +RegisterNetEvent('chat:addMode') +RegisterNetEvent('chat:removeMode') RegisterNetEvent('chat:removeSuggestion') RegisterNetEvent('chat:clear') @@ -47,14 +48,25 @@ AddEventHandler('__cfx_internal:serverPrint', function(msg) }) end) -AddEventHandler('chat:addMessage', function(message) +-- addMessage +local addMessage = function(message) + if type(message) == 'string' then + message = { + args = { message } + } + end + SendNUIMessage({ type = 'ON_MESSAGE', message = message }) -end) +end -AddEventHandler('chat:addSuggestion', function(name, help, params) +exports('addMessage', addMessage) +AddEventHandler('chat:addMessage', addMessage) + +-- addSuggestion +local addSuggestion = function(name, help, params) SendNUIMessage({ type = 'ON_SUGGESTION_ADD', suggestion = { @@ -63,7 +75,10 @@ AddEventHandler('chat:addSuggestion', function(name, help, params) params = params or nil } }) -end) +end + +exports('addSuggestion', addSuggestion) +AddEventHandler('chat:addSuggestion', addSuggestion) AddEventHandler('chat:addSuggestions', function(suggestions) for _, suggestion in ipairs(suggestions) do @@ -81,6 +96,20 @@ AddEventHandler('chat:removeSuggestion', function(name) }) end) +AddEventHandler('chat:addMode', function(mode) + SendNUIMessage({ + type = 'ON_MODE_ADD', + mode = mode + }) +end) + +AddEventHandler('chat:removeMode', function(name) + SendNUIMessage({ + type = 'ON_MODE_REMOVE', + name = name + }) +end) + AddEventHandler('chat:addTemplate', function(id, html) SendNUIMessage({ type = 'ON_TEMPLATE_ADD', @@ -110,7 +139,7 @@ RegisterNUICallback('chatResult', function(data, cb) if data.message:sub(1, 1) == '/' then ExecuteCommand(data.message:sub(2)) else - TriggerServerEvent('_chat:messageEntered', GetPlayerName(id), { r, g, b }, data.message) + TriggerServerEvent('_chat:messageEntered', GetPlayerName(id), { r, g, b }, data.message, data.mode) end end @@ -124,7 +153,7 @@ local function refreshCommands() local suggestions = {} for _, command in ipairs(registeredCommands) do - if IsAceAllowed(('command.%s'):format(command.name)) then + if IsAceAllowed(('command.%s'):format(command.name)) and command.name ~= 'toggleChat' then table.insert(suggestions, { name = '/' .. command.name, help = '' @@ -178,7 +207,7 @@ AddEventHandler('onClientResourceStop', function(resName) end) RegisterNUICallback('loaded', function(data, cb) - TriggerServerEvent('chat:init'); + TriggerServerEvent('chat:init') refreshCommands() refreshThemes() @@ -188,10 +217,41 @@ RegisterNUICallback('loaded', function(data, cb) cb('ok') end) +local CHAT_HIDE_STATES = { + SHOW_WHEN_ACTIVE = 0, + ALWAYS_SHOW = 1, + ALWAYS_HIDE = 2 +} + +local kvpEntry = GetResourceKvpString('hideState') +local chatHideState = kvpEntry and tonumber(kvpEntry) or CHAT_HIDE_STATES.SHOW_WHEN_ACTIVE +local isFirstHide = true + +if not isRDR then + RegisterKeyMapping('toggleChat', 'Toggle chat', 'keyboard', 'l') + + RegisterCommand('toggleChat', function() + if chatHideState == CHAT_HIDE_STATES.SHOW_WHEN_ACTIVE then + chatHideState = CHAT_HIDE_STATES.ALWAYS_SHOW + elseif chatHideState == CHAT_HIDE_STATES.ALWAYS_SHOW then + chatHideState = CHAT_HIDE_STATES.ALWAYS_HIDE + elseif chatHideState == CHAT_HIDE_STATES.ALWAYS_HIDE then + chatHideState = CHAT_HIDE_STATES.SHOW_WHEN_ACTIVE + end + + isFirstHide = false + + SetResourceKvp('hideState', tostring(chatHideState)) + end, false) +end + Citizen.CreateThread(function() SetTextChatEnabled(false) SetNuiFocus(false) + local lastChatHideState = -1 + local origChatHideState = -1 + while true do Wait(0) @@ -215,19 +275,26 @@ Citizen.CreateThread(function() end if chatLoaded then - local shouldBeHidden = false + local forceHide = IsScreenFadedOut() or IsPauseMenuActive() - if IsScreenFadedOut() or IsPauseMenuActive() then - shouldBeHidden = true + if forceHide then + origChatHideState = chatHideState + chatHideState = CHAT_HIDE_STATES.ALWAYS_HIDE + elseif origChatHideState ~= -1 then + chatHideState = origChatHideState + origChatHideState = -1 end - if (shouldBeHidden and not chatHidden) or (not shouldBeHidden and chatHidden) then - chatHidden = shouldBeHidden + if chatHideState ~= lastChatHideState then + lastChatHideState = chatHideState SendNUIMessage({ type = 'ON_SCREEN_STATE_CHANGE', - shouldHide = shouldBeHidden + hideState = chatHideState, + fromUserInteraction = not forceHide and not isFirstHide }) + + isFirstHide = false end end end diff --git a/resources/[gameplay]/chat/fxmanifest.lua b/resources/[gameplay]/chat/fxmanifest.lua index 14c61bd..246f135 100644 --- a/resources/[gameplay]/chat/fxmanifest.lua +++ b/resources/[gameplay]/chat/fxmanifest.lua @@ -1,30 +1,17 @@ description 'chat management stuff' -ui_page 'html/index.html' +ui_page 'dist/ui.html' client_script 'cl_chat.lua' server_script 'sv_chat.lua' files { - 'html/index.html', - 'html/index.css', - 'html/config.default.js', - 'html/config.js', - 'html/App.js', - 'html/Message.js', - 'html/Suggestions.js', - 'html/vendor/vue.2.3.3.min.js', - 'html/vendor/flexboxgrid.6.3.1.min.css', - 'html/vendor/animate.3.5.2.min.css', - 'html/vendor/latofonts.css', - 'html/vendor/fonts/LatoRegular.woff2', - 'html/vendor/fonts/LatoRegular2.woff2', - 'html/vendor/fonts/LatoLight2.woff2', - 'html/vendor/fonts/LatoLight.woff2', - 'html/vendor/fonts/LatoBold.woff2', - 'html/vendor/fonts/LatoBold2.woff2', - } + 'dist/ui.html', + 'dist/index.css', + 'html/vendor/*.css', + 'html/vendor/fonts/*.woff2', +} fx_version 'adamant' games { 'rdr3', 'gta5' } -rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.' \ No newline at end of file +rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.' diff --git a/resources/[gameplay]/chat/html/App.js b/resources/[gameplay]/chat/html/App.js deleted file mode 100644 index 6e54d2f..0000000 --- a/resources/[gameplay]/chat/html/App.js +++ /dev/null @@ -1,255 +0,0 @@ -window.APP = { - template: '#app_template', - name: 'app', - data() { - return { - style: CONFIG.style, - showInput: false, - showWindow: false, - shouldHide: true, - backingSuggestions: [], - removedSuggestions: [], - templates: CONFIG.templates, - message: '', - messages: [], - oldMessages: [], - oldMessagesIndex: -1, - tplBackups: [], - msgTplBackups: [] - }; - }, - destroyed() { - clearInterval(this.focusTimer); - window.removeEventListener('message', this.listener); - }, - mounted() { - post('http://chat/loaded', JSON.stringify({})); - 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.resetShowWindowTimer(); - - const messagesObj = this.$refs.messages; - this.$nextTick(() => { - messagesObj.scrollTop = messagesObj.scrollHeight; - }); - }, - }, - computed: { - suggestions() { - return this.backingSuggestions.filter((el) => this.removedSuggestions.indexOf(el.name) <= -1); - }, - }, - methods: { - ON_SCREEN_STATE_CHANGE({ shouldHide }) { - this.shouldHide = shouldHide; - }, - 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({ message }) { - this.messages.push(message); - }, - ON_CLEAR() { - this.messages = []; - this.oldMessages = []; - this.oldMessagesIndex = -1; - }, - ON_SUGGESTION_ADD({ suggestion }) { - const duplicateSuggestion = this.backingSuggestions.find(a => a.name == suggestion.name); - if (duplicateSuggestion) { - if(suggestion.help || suggestion.params) { - duplicateSuggestion.help = suggestion.help || ""; - duplicateSuggestion.params = suggestion.params || []; - } - return; - } - if (!suggestion.params) { - suggestion.params = []; //TODO Move somewhere else - } - this.backingSuggestions.push(suggestion); - }, - ON_SUGGESTION_REMOVE({ name }) { - if(this.removedSuggestions.indexOf(name) <= -1) { - this.removedSuggestions.push(name); - } - }, - ON_TEMPLATE_ADD({ template }) { - if (this.templates[template.id]) { - this.warn(`Tried to add duplicate template '${template.id}'`) - } else { - this.templates[template.id] = template.html; - } - }, - ON_UPDATE_THEMES({ themes }) { - this.removeThemes(); - - this.setThemes(themes); - }, - removeThemes() { - for (let i = 0; i < document.styleSheets.length; i++) { - const styleSheet = document.styleSheets[i]; - const node = styleSheet.ownerNode; - - if (node.getAttribute('data-theme')) { - node.parentNode.removeChild(node); - } - } - - this.tplBackups.reverse(); - - for (const [ elem, oldData ] of this.tplBackups) { - elem.innerText = oldData; - } - - this.tplBackups = []; - - this.msgTplBackups.reverse(); - - for (const [ id, oldData ] of this.msgTplBackups) { - this.templates[id] = oldData; - } - - this.msgTplBackups = []; - }, - setThemes(themes) { - for (const [ id, data ] of Object.entries(themes)) { - if (data.style) { - const style = document.createElement('style'); - style.type = 'text/css'; - style.setAttribute('data-theme', id); - style.appendChild(document.createTextNode(data.style)); - - document.head.appendChild(style); - } - - if (data.styleSheet) { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.type = 'text/css'; - link.href = data.baseUrl + data.styleSheet; - link.setAttribute('data-theme', id); - - document.head.appendChild(link); - } - - if (data.templates) { - for (const [ tplId, tpl ] of Object.entries(data.templates)) { - const elem = document.getElementById(tplId); - - if (elem) { - this.tplBackups.push([ elem, elem.innerText ]); - elem.innerText = tpl; - } - } - } - - if (data.script) { - const script = document.createElement('script'); - script.type = 'text/javascript'; - script.src = data.baseUrl + data.script; - - document.head.appendChild(script); - } - - if (data.msgTemplates) { - for (const [ tplId, tpl ] of Object.entries(data.msgTemplates)) { - this.msgTplBackups.push([ tplId, this.templates[tplId] ]); - this.templates[tplId] = tpl; - } - } - } - }, - warn(msg) { - this.messages.push({ - args: [msg], - template: '^3CHAT-WARN: ^0{0}', - }); - }, - clearShowWindowTimer() { - clearTimeout(this.showWindowTimer); - }, - resetShowWindowTimer() { - this.clearShowWindowTimer(); - this.showWindowTimer = setTimeout(() => { - if (!this.showInput) { - this.showWindow = false; - } - }, CONFIG.fadeTimeout); - }, - keyUp() { - this.resize(); - }, - keyDown(e) { - if (e.which === 38 || e.which === 40) { - e.preventDefault(); - this.moveOldMessageIndex(e.which === 38); - } else if (e.which == 33) { - var buf = document.getElementsByClassName('chat-messages')[0]; - buf.scrollTop = buf.scrollTop - 100; - } else if (e.which == 34) { - var buf = document.getElementsByClassName('chat-messages')[0]; - buf.scrollTop = buf.scrollTop + 100; - } - }, - 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`; - }, - send(e) { - if(this.message !== '') { - post('http://chat/chatResult', JSON.stringify({ - message: this.message, - })); - this.oldMessages.unshift(this.message); - this.oldMessagesIndex = -1; - this.hideInput(); - } else { - this.hideInput(true); - } - }, - hideInput(canceled = false) { - if (canceled) { - post('http://chat/chatResult', JSON.stringify({ canceled })); - } - this.message = ''; - this.showInput = false; - clearInterval(this.focusTimer); - this.resetShowWindowTimer(); - }, - }, -}; diff --git a/resources/[gameplay]/chat/html/App.ts b/resources/[gameplay]/chat/html/App.ts new file mode 100644 index 0000000..627e617 --- /dev/null +++ b/resources/[gameplay]/chat/html/App.ts @@ -0,0 +1,434 @@ +import { post } from './utils'; +import CONFIG from './config'; +import Vue from 'vue'; + +import Suggestions from './Suggestions.vue'; +import MessageV from './Message.vue'; +import { Suggestion } from './Suggestions'; + +export interface Message { + args: string[]; + template: string; + params?: { [key: string]: string }; + multiline?: boolean; + color?: [ number, number, number ]; + templateId?: number; + + id?: string; +} + +export interface ThemeData { + style: string; + styleSheet: string; + baseUrl: string; + script: string; + templates: { [id: string]: string }; // not supported rn + msgTemplates: { [id: string]: string }; + +} + +export interface Mode { + name: string; + displayName: string; + color: string; +} + +enum ChatHideStates { + ShowWhenActive = 0, + AlwaysShow = 1, + AlwaysHide = 2, +} + +const defaultMode: Mode = { + name: 'all', + displayName: 'All', + color: '#fff' +}; + +export default Vue.extend({ + template: "#app_template", + name: "app", + components: { + Suggestions, + MessageV + }, + data() { + return { + style: CONFIG.style, + showInput: false, + showWindow: false, + showHideState: false, + hideState: ChatHideStates.ShowWhenActive, + backingSuggestions: [] as Suggestion[], + removedSuggestions: [] as string[], + templates: { ...CONFIG.templates } as { [ key: string ]: string }, + message: "", + messages: [] as Message[], + oldMessages: [] as string[], + oldMessagesIndex: -1, + tplBackups: [] as unknown as [ HTMLElement, string ][], + msgTplBackups: [] as unknown as [ string, string ][], + focusTimer: 0, + showWindowTimer: 0, + showHideStateTimer: 0, + listener: (event: MessageEvent) => {}, + modes: [defaultMode] as Mode[], + modeIdx: 0, + }; + }, + destroyed() { + clearInterval(this.focusTimer); + window.removeEventListener("message", this.listener); + }, + mounted() { + post("http://chat/loaded", JSON.stringify({})); + + this.listener = (event: MessageEvent) => { + const item: any = event.data || (event).detail; //'detail' is for debugging via browsers + + if (!item || !item.type) { + return; + } + + const typeRef = item.type as + 'ON_OPEN' | 'ON_SCREEN_STATE_CHANGE' | 'ON_MESSAGE' | 'ON_CLEAR' | 'ON_SUGGESTION_ADD' | + 'ON_SUGGESTION_REMOVE' | 'ON_TEMPLATE_ADD' | 'ON_UPDATE_THEMES' | 'ON_MODE_ADD' | 'ON_MODE_REMOVE'; + + if (this[typeRef]) { + this[typeRef](item); + } + }; + + window.addEventListener("message", this.listener); + }, + watch: { + messages() { + if (this.hideState !== ChatHideStates.AlwaysHide) { + if (this.showWindowTimer) { + clearTimeout(this.showWindowTimer); + } + this.showWindow = true; + this.resetShowWindowTimer(); + } + + const messagesObj = this.$refs.messages as HTMLDivElement; + this.$nextTick(() => { + messagesObj.scrollTop = messagesObj.scrollHeight; + }); + } + }, + computed: { + suggestions(): Suggestion[] { + return this.backingSuggestions.filter( + el => this.removedSuggestions.indexOf(el.name) <= -1 + ); + }, + + hideAnimated(): boolean { + return this.hideState !== ChatHideStates.AlwaysHide; + }, + + modeIdxGet(): number { + return (this.modeIdx >= this.modes.length) ? (this.modes.length - 1) : this.modeIdx; + }, + + modePrefix(): string { + if (this.modes.length === 1) { + return `➤`; + } + + return this.modes[this.modeIdxGet].displayName; + }, + + modeColor(): string { + return this.modes[this.modeIdxGet].color; + }, + + hideStateString(): string { + // TODO: localization + switch (this.hideState) { + case ChatHideStates.AlwaysShow: + return 'Visible'; + case ChatHideStates.AlwaysHide: + return 'Hidden'; + case ChatHideStates.ShowWhenActive: + return 'When active'; + } + } + }, + methods: { + ON_SCREEN_STATE_CHANGE({ hideState, fromUserInteraction }: { hideState: ChatHideStates, fromUserInteraction: boolean }) { + this.hideState = hideState; + + if (this.hideState === ChatHideStates.AlwaysHide) { + if (!this.showInput) { + this.showWindow = false; + } + } else if (this.hideState === ChatHideStates.AlwaysShow) { + this.showWindow = true; + if (this.showWindowTimer) { + clearTimeout(this.showWindowTimer); + } + } else { + this.resetShowWindowTimer(); + } + + if (fromUserInteraction) { + this.showHideState = true; + + if (this.showHideStateTimer) { + clearTimeout(this.showHideStateTimer); + } + + this.showHideStateTimer = window.setTimeout(() => { + this.showHideState = false; + }, 1500); + } + }, + ON_OPEN() { + this.showInput = true; + this.showWindow = true; + if (this.showWindowTimer) { + clearTimeout(this.showWindowTimer); + } + this.focusTimer = window.setInterval(() => { + if (this.$refs.input) { + (this.$refs.input as HTMLInputElement).focus(); + } else { + clearInterval(this.focusTimer); + } + }, 100); + }, + ON_MESSAGE({ message }: { message: Message }) { + message.id = `${new Date().getTime()}${Math.random()}`; + this.messages.push(message); + }, + ON_CLEAR() { + this.messages = []; + this.oldMessages = []; + this.oldMessagesIndex = -1; + }, + ON_SUGGESTION_ADD({ suggestion }: { suggestion: Suggestion }) { + const duplicateSuggestion = this.backingSuggestions.find( + a => a.name == suggestion.name + ); + if (duplicateSuggestion) { + if (suggestion.help || suggestion.params) { + duplicateSuggestion.help = suggestion.help || ""; + duplicateSuggestion.params = suggestion.params || []; + } + return; + } + if (!suggestion.params) { + suggestion.params = []; //TODO Move somewhere else + } + this.backingSuggestions.push(suggestion); + }, + ON_SUGGESTION_REMOVE({ name }: { name: string }) { + if (this.removedSuggestions.indexOf(name) <= -1) { + this.removedSuggestions.push(name); + } + }, + ON_MODE_ADD({ mode }: { mode: Mode }) { + this.modes = [ + ...this.modes.filter(a => a.name !== mode.name), + mode + ]; + }, + ON_MODE_REMOVE({ name }: { name: string }) { + this.modes = this.modes.filter(a => a.name !== name); + + if (this.modes.length === 0) { + this.modes = [defaultMode]; + } + }, + ON_TEMPLATE_ADD({ template }: { template: { id: string, html: string }}) { + if (this.templates[template.id]) { + this.warn(`Tried to add duplicate template '${template.id}'`); + } else { + this.templates[template.id] = template.html; + } + }, + ON_UPDATE_THEMES({ themes }: { themes: { [key: string]: ThemeData } }) { + this.removeThemes(); + + this.setThemes(themes); + }, + removeThemes() { + for (let i = 0; i < document.styleSheets.length; i++) { + const styleSheet = document.styleSheets[i]; + const node = styleSheet.ownerNode as Element; + + if (node.getAttribute("data-theme")) { + node.parentNode?.removeChild(node); + } + } + + this.tplBackups.reverse(); + + for (const [elem, oldData] of this.tplBackups) { + elem.innerText = oldData; + } + + this.tplBackups = []; + + this.msgTplBackups.reverse(); + + for (const [id, oldData] of this.msgTplBackups) { + this.templates[id] = oldData; + } + + this.msgTplBackups = []; + }, + setThemes(themes: { [key: string]: ThemeData }) { + for (const [id, data] of Object.entries(themes)) { + if (data.style) { + const style = document.createElement("style"); + style.type = "text/css"; + style.setAttribute("data-theme", id); + style.appendChild(document.createTextNode(data.style)); + + document.head.appendChild(style); + } + + if (data.styleSheet) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.type = "text/css"; + link.href = data.baseUrl + data.styleSheet; + link.setAttribute("data-theme", id); + + document.head.appendChild(link); + } + + if (data.templates) { + for (const [tplId, tpl] of Object.entries(data.templates)) { + const elem = document.getElementById(tplId); + + if (elem) { + this.tplBackups.push([elem, elem.innerText]); + elem.innerText = tpl; + } + } + } + + if (data.script) { + const script = document.createElement("script"); + script.type = "text/javascript"; + script.src = data.baseUrl + data.script; + + document.head.appendChild(script); + } + + if (data.msgTemplates) { + for (const [tplId, tpl] of Object.entries(data.msgTemplates)) { + this.msgTplBackups.push([tplId, this.templates[tplId]]); + this.templates[tplId] = tpl; + } + } + } + }, + warn(msg: string) { + this.messages.push({ + args: [msg], + template: "^3CHAT-WARN: ^0{0}" + }); + }, + clearShowWindowTimer() { + clearTimeout(this.showWindowTimer); + }, + resetShowWindowTimer() { + this.clearShowWindowTimer(); + this.showWindowTimer = window.setTimeout(() => { + if (this.hideState !== ChatHideStates.AlwaysShow && !this.showInput) { + this.showWindow = false; + } + }, CONFIG.fadeTimeout); + }, + keyUp() { + this.resize(); + }, + keyDown(e: KeyboardEvent) { + if (e.which === 38 || e.which === 40) { + e.preventDefault(); + this.moveOldMessageIndex(e.which === 38); + } else if (e.which == 33) { + var buf = document.getElementsByClassName("chat-messages")[0]; + buf.scrollTop = buf.scrollTop - 100; + } else if (e.which == 34) { + var buf = document.getElementsByClassName("chat-messages")[0]; + buf.scrollTop = buf.scrollTop + 100; + } else if (e.which === 9) { // tab + if (e.shiftKey || e.altKey) { + --this.modeIdx; + + if (this.modeIdx < 0) { + this.modeIdx = this.modes.length - 1; + } + } else { + this.modeIdx = (this.modeIdx + 1) % this.modes.length; + } + } + + this.resize(); + }, + moveOldMessageIndex(up: boolean) { + 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 as HTMLInputElement; + + // scrollHeight includes padding, but content-box excludes padding + // remove padding before setting height on the element + const style = getComputedStyle(input); + const paddingRemove = parseFloat(style.paddingBottom) + parseFloat(style.paddingTop); + + input.style.height = "5px"; + input.style.height = `${input.scrollHeight - paddingRemove}px`; + }, + send() { + if (this.message !== "") { + post( + "http://chat/chatResult", + JSON.stringify({ + message: this.message, + mode: this.modes[this.modeIdxGet].name + }) + ); + this.oldMessages.unshift(this.message); + this.oldMessagesIndex = -1; + this.hideInput(); + } else { + this.hideInput(true); + } + }, + hideInput(canceled = false) { + setTimeout(() => { + const input = this.$refs.input as HTMLInputElement; + delete input.style.height; + }, 50); + + if (canceled) { + post("http://chat/chatResult", JSON.stringify({ canceled })); + } + this.message = ""; + this.showInput = false; + clearInterval(this.focusTimer); + + if (this.hideState !== ChatHideStates.AlwaysHide) { + this.resetShowWindowTimer(); + } else { + this.showWindow = false; + } + } + } +}); \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/App.vue b/resources/[gameplay]/chat/html/App.vue new file mode 100644 index 0000000..b6eade7 --- /dev/null +++ b/resources/[gameplay]/chat/html/App.vue @@ -0,0 +1,44 @@ + + + + + + + + + + + {{modePrefix}} + + + + + + + Chat: {{hideStateString}} + + + + + + \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/Message.js b/resources/[gameplay]/chat/html/Message.ts similarity index 58% rename from resources/[gameplay]/chat/html/Message.js rename to resources/[gameplay]/chat/html/Message.ts index 067d842..9326fd7 100644 --- a/resources/[gameplay]/chat/html/Message.js +++ b/resources/[gameplay]/chat/html/Message.ts @@ -1,42 +1,48 @@ -Vue.component('message', { - template: '#message_template', +import CONFIG from './config'; +import Vue, { PropType } from 'vue'; + +export default Vue.component('message', { data() { return {}; }, computed: { - textEscaped() { + textEscaped(): string { let s = this.template ? this.template : this.templates[this.templateId]; - if (this.template) { - //We disable templateId since we are using a direct raw template - this.templateId = -1; - } - //This hack is required to preserve backwards compatability - if (this.templateId == CONFIG.defaultTemplateId + if (!this.template && this.templateId == CONFIG.defaultTemplateId && this.args.length == 1) { s = this.templates[CONFIG.defaultAltTemplateId] //Swap out default template :/ } + s = s.replace(`@default`, this.templates[this.templateId]); + s = s.replace(/{(\d+)}/g, (match, number) => { - const argEscaped = this.args[number] != undefined ? this.escape(this.args[number]) : match + const argEscaped = this.args[number] != undefined ? this.escape(this.args[number]) : match; if (number == 0 && this.color) { //color is deprecated, use templates or ^1 etc. return this.colorizeOld(argEscaped); } return argEscaped; }); + + // format variant args + s = s.replace(/\{\{([a-zA-Z0-9_\-]+?)\}\}/g, (match, id) => { + const argEscaped = this.params[id] != undefined ? this.escape(this.params[id]) : match; + return argEscaped; + }); + return this.colorize(s); }, }, methods: { - colorizeOld(str) { + colorizeOld(str: string): string { return `${str}` }, - colorize(str) { - let s = "" + (str.replace(/\^([0-9])/g, (str, color) => ``)) + ""; + colorize(str: string): string { + let s = "" + colorTrans(str) + ""; - const styleDict = { + const styleDict: {[ key: string ]: string} = { '*': 'font-weight: bold;', '_': 'text-decoration: underline;', '~': 'text-decoration: line-through;', @@ -49,8 +55,15 @@ Vue.component('message', { s = s.replace(styleRegex, (str, style, inner) => `${inner}`) } return s.replace(/]*><\/span[^>]*>/g, ''); + + function colorTrans(str: string) { + return str + .replace(/\^([0-9])/g, (str, color) => ``) + .replace(/\^#([0-9A-F]{3,6})/gi, (str, color) => ``) + .replace(/~([a-z])~/g, (str, color) => ``); + } }, - escape(unsafe) { + escape(unsafe: string): string { return String(unsafe) .replace(/&/g, '&') .replace(/, }, args: { - type: Array, + type: Array as PropType, + }, + params: { + type: Object as PropType<{ [ key: string]: string }>, }, template: { type: String, @@ -79,8 +95,8 @@ Vue.component('message', { default: false, }, color: { //deprecated - type: Array, - default: false, + type: Array as PropType, + default: null, }, }, }); diff --git a/resources/[gameplay]/chat/html/Message.vue b/resources/[gameplay]/chat/html/Message.vue new file mode 100644 index 0000000..7f87cea --- /dev/null +++ b/resources/[gameplay]/chat/html/Message.vue @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/Suggestions.js b/resources/[gameplay]/chat/html/Suggestions.ts similarity index 75% rename from resources/[gameplay]/chat/html/Suggestions.js rename to resources/[gameplay]/chat/html/Suggestions.ts index 07c4688..08cd9ae 100644 --- a/resources/[gameplay]/chat/html/Suggestions.js +++ b/resources/[gameplay]/chat/html/Suggestions.ts @@ -1,11 +1,29 @@ -Vue.component('suggestions', { - template: '#suggestions_template', - props: ['message', 'suggestions'], +import CONFIG from './config'; +import Vue, { PropType } from 'vue'; + +export interface Suggestion { + name: string; + help: string; + params: string[]; + + disabled: boolean; +} + +export default Vue.component('suggestions', { + props: { + message: { + type: String + }, + + suggestions: { + type: Array as PropType + } + }, data() { return {}; }, computed: { - currentSuggestions() { + currentSuggestions(): Suggestion[] { if (this.message === '') { return []; } @@ -34,6 +52,7 @@ Vue.component('suggestions', { const regex = new RegExp(`${s.name} (?:\\w+ ){${index}}(?:${wType}*)$`, 'g'); // eslint-disable-next-line no-param-reassign + // @ts-ignore p.disabled = this.message.match(regex) == null; }); }); diff --git a/resources/[gameplay]/chat/html/Suggestions.vue b/resources/[gameplay]/chat/html/Suggestions.vue new file mode 100644 index 0000000..d8d5c38 --- /dev/null +++ b/resources/[gameplay]/chat/html/Suggestions.vue @@ -0,0 +1,29 @@ + + + + + + + {{s.name}} + + + [{{p.name}}] + + + + + {{s.help}} + + + {{p.help}} + + + + + + + + \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/config.default.js b/resources/[gameplay]/chat/html/config.ts similarity index 81% rename from resources/[gameplay]/chat/html/config.default.js rename to resources/[gameplay]/chat/html/config.ts index 82192ad..bd76f77 100644 --- a/resources/[gameplay]/chat/html/config.default.js +++ b/resources/[gameplay]/chat/html/config.ts @@ -1,6 +1,4 @@ -// DO NOT EDIT THIS FILE -// Copy it to `config.js` and edit it -window.CONFIG = { +export default { defaultTemplateId: 'default', //This is the default template for 2 args1 defaultAltTemplateId: 'defaultAlt', //This one for 1 arg templates: { //You can add static templates here @@ -13,7 +11,7 @@ window.CONFIG = { suggestionLimit: 5, style: { background: 'rgba(52, 73, 94, 0.7)', - width: '38%', + width: '38vw', height: '22%', } }; diff --git a/resources/[gameplay]/chat/html/index.css b/resources/[gameplay]/chat/html/index.css index 30f1699..df5c1c2 100644 --- a/resources/[gameplay]/chat/html/index.css +++ b/resources/[gameplay]/chat/html/index.css @@ -8,6 +8,14 @@ .color-8{color: #cc0000;} .color-9{color: #cc0068;} +.gameColor-w{color: #ffffff;} +.gameColor-r{color: #ff4444;} +.gameColor-g{color: #99cc00;} +.gameColor-y{color: #ffbb33;} +.gameColor-b{color: #33b5e5;} + +/* todo: more game colors */ + * { font-family: 'Lato', sans-serif; margin: 0; @@ -63,26 +71,47 @@ em { box-sizing: border-box; } +.chat-input > div.input { + position: relative; + display: flex; + align-items: stretch; + width: 100%; + background-color: rgba(44, 62, 80, 1.0); +} + +.chat-hide-state { + text-transform: uppercase; + margin-left: 0.05vw; + font-size: 1.65vh; +} + .prefix { font-size: 1.8vh; - position: absolute; - margin-top: 0.5%; - left: 0.208%; + /*position: absolute; + top: 0%;*/ + height: 100%; + vertical-align: middle; + line-height: calc(1vh + 1vh + 1.85vh); + padding-left: 0.5vh; + text-transform: uppercase; + font-weight: bold; + display: inline-block; } textarea { font-size: 1.65vh; + line-height: 1.85vh; display: block; - box-sizing: border-box; - padding: 1%; - padding-left: 3.5%; + box-sizing: content-box; + padding: 1vh; + padding-left: 0.5vh; color: white; - background-color: rgba(44, 62, 80, 1.0); - width: 100%; border-width: 0; height: 3.15%; overflow: hidden; text-overflow: ellipsis; + flex: 1; + background-color: transparent; } textarea:focus, input:focus { @@ -123,5 +152,9 @@ textarea:focus, input:focus { } .hidden { - display: none; + opacity: 0; +} + +.hidden.animated { + transition: opacity 1s; } \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/index.d.ts b/resources/[gameplay]/chat/html/index.d.ts new file mode 100644 index 0000000..314e3aa --- /dev/null +++ b/resources/[gameplay]/chat/html/index.d.ts @@ -0,0 +1,4 @@ +declare module '*.vue' { + import Vue from 'vue' + export default Vue +} \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/index.html b/resources/[gameplay]/chat/html/index.html index 6ce65dd..c9b40ac 100644 --- a/resources/[gameplay]/chat/html/index.html +++ b/resources/[gameplay]/chat/html/index.html @@ -3,115 +3,12 @@ - - - + + + - - - - - - - - - - - - - - - - - - - - - -
+ + {{s.name}} + + + [{{p.name}}] + +