diff --git a/code/modules/asset_cache/assets/tgui.dm b/code/modules/asset_cache/assets/tgui.dm index 03e2acd449..9c79925602 100644 --- a/code/modules/asset_cache/assets/tgui.dm +++ b/code/modules/asset_cache/assets/tgui.dm @@ -5,13 +5,6 @@ "tgui.bundle.css" = file("tgui/public/tgui.bundle.css"), ) -/datum/asset/simple/tgui_edge - keep_local_name = TRUE - assets = list( - "tgui.bundle.edge.js" = file("tgui/public/tgui.bundle.edge.js"), - "tgui.bundle.edge.css" = file("tgui/public/tgui.bundle.edge.css"), - ) - /datum/asset/simple/tgui_panel keep_local_name = TRUE assets = list( diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm index 446ab4deba..c6076ed56c 100644 --- a/code/modules/client/client procs.dm +++ b/code/modules/client/client procs.dm @@ -226,9 +226,7 @@ //CONNECT// /////////// /client/New(TopicData) - // TODO: Remove version check with 516 - if(byond_version >= 516) // Enable 516 compat browser storage mechanisms - winset(src, null, "browser-options=[DEFAULT_CLIENT_BROWSER_OPTIONS]") + winset(src, null, "browser-options=[DEFAULT_CLIENT_BROWSER_OPTIONS]") TopicData = null //Prevent calls to client.Topic from connect @@ -360,11 +358,9 @@ fully_created = TRUE attempt_auto_fit_viewport() - // TODO: Remove version check with 516 - if(byond_version >= 516) - // Now that we're fully initialized, use our prefs - if(prefs?.read_preference(/datum/preference/toggle/browser_dev_tools)) - winset(src, null, "browser-options=[DEFAULT_CLIENT_BROWSER_OPTIONS],devtools") + // Now that we're fully initialized, use our prefs + if(prefs?.read_preference(/datum/preference/toggle/browser_dev_tools)) + winset(src, null, "browser-options=[DEFAULT_CLIENT_BROWSER_OPTIONS],devtools") ////////////// //DISCONNECT// @@ -672,18 +668,6 @@ src << browse("[message]","window=dropmessage;size=480x360;can_close=1") qdel(src) -/// Keydown event in a tgui window this client has open. Has keycode passed to it. -/client/verb/TguiKeyDown(keycode as text) - set name = "TguiKeyDown" - set hidden = TRUE - return // stub - -/// Keyup event in a tgui window this client has open. Has keycode passed to it. -/client/verb/TguiKeyUp(keycode as text) // Doesn't seem to currently fire? - set name = "TguiKeyUp" - set hidden = TRUE - return // stub - /client/verb/toggle_fullscreen() set name = "Toggle Fullscreen" set category = "OOC.Client Settings" diff --git a/code/modules/client/preference_setup/general/03_body.dm b/code/modules/client/preference_setup/general/03_body.dm index af656eef26..15a01122a8 100644 --- a/code/modules/client/preference_setup/general/03_body.dm +++ b/code/modules/client/preference_setup/general/03_body.dm @@ -341,7 +341,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O var/key = pref.rlimb_data[name] if(!istext(key)) log_debug("Bad rlimb_data for [key_name(pref.client)], [name] was set to [key]") - to_chat(user, span_warning("Error loading robot limb data for `[name]`, clearing pref.")) + to_chat(usr, span_warning("Error loading robot limb data for `[name]`, clearing pref.")) pref.rlimb_data -= name else R = LAZYACCESS(all_robolimbs, key) @@ -552,7 +552,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O pref.set_biological_gender(mob_species.genders[1]) pref.custom_species = null //grab one of the valid hair styles for the newly chosen species - var/list/valid_hairstyles = pref.get_valid_hairstyles(user) + var/list/valid_hairstyles = pref.get_valid_hairstyles() if(valid_hairstyles.len) if(!(pref.h_style in valid_hairstyles)) @@ -604,7 +604,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O return TOPIC_REFRESH_UPDATE_PREVIEW else if(href_list["hair_style"]) - var/list/valid_hairstyles = pref.get_valid_hairstyles(user) + var/list/valid_hairstyles = pref.get_valid_hairstyles() var/new_h_style = tgui_input_list(user, "Choose your character's hair style:", "Character Preference", valid_hairstyles, pref.h_style) if(new_h_style && CanUseTopic(user)) @@ -629,7 +629,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O else if(href_list["hair_style_left"]) var/H = href_list["hair_style_left"] - var/list/valid_hairstyles = pref.get_valid_hairstyles(user) + var/list/valid_hairstyles = pref.get_valid_hairstyles() var/start = valid_hairstyles.Find(H) if(start != 1) //If we're not the beginning of the list, become the previous element. @@ -640,7 +640,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O else if(href_list["hair_style_right"]) var/H = href_list["hair_style_right"] - var/list/valid_hairstyles = pref.get_valid_hairstyles(user) + var/list/valid_hairstyles = pref.get_valid_hairstyles() var/start = valid_hairstyles.Find(H) if(start != valid_hairstyles.len) //If we're not the end of the list, become the next element. @@ -1054,7 +1054,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O return TOPIC_REFRESH_UPDATE_PREVIEW else if(href_list["ear_color2"]) - var/new_earc2 = tgui_color_picker(user, "Choose your character's ear colour:", "Character Preference", + var/new_earc2 = tgui_color_picker(user, "Choose your character's secondary ear colour:", "Character Preference", pref.read_preference(/datum/preference/color/human/ears_color2)) if(new_earc2) pref.update_preference_by_type(/datum/preference/color/human/ears_color2, new_earc2) diff --git a/code/modules/client/preferences/types/game/ui.dm b/code/modules/client/preferences/types/game/ui.dm index 3f110e0680..cf50190be8 100644 --- a/code/modules/client/preferences/types/game/ui.dm +++ b/code/modules/client/preferences/types/game/ui.dm @@ -81,7 +81,7 @@ savefile_identifier = PREFERENCE_PLAYER minimum = 1 - maximum = 5 + maximum = 20 step = 1 /datum/preference/numeric/tgui_say_height/create_default_value() @@ -90,6 +90,22 @@ /datum/preference/numeric/tgui_say_height/apply_to_client(client/client, value) client.tgui_say?.load() + +/datum/preference/numeric/tgui_say_width + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "tgui_say_width" + savefile_identifier = PREFERENCE_PLAYER + + minimum = 360 + maximum = 800 + step = 20 + +/datum/preference/numeric/tgui_say_width/create_default_value() + return 360 + +/datum/preference/numeric/tgui_say_width/apply_to_client(client/client, value) + client.tgui_say?.load() + /datum/preference/text/preset_colors category = PREFERENCE_CATEGORY_MANUALLY_RENDERED savefile_identifier = PREFERENCE_PLAYER diff --git a/code/modules/keybindings/bindings_movekeys.dm b/code/modules/keybindings/bindings_movekeys.dm index 896618084a..bd15f10096 100644 --- a/code/modules/keybindings/bindings_movekeys.dm +++ b/code/modules/keybindings/bindings_movekeys.dm @@ -25,7 +25,7 @@ var/global/list/MOVE_KEY_MAPPINGS = list( // Validate input. Must be one (and only one) of the key codes) if(isnull(movekey) || (movekey & ~0xFFF) || (movekey & (movekey - 1))) - log_debug("Client [ckey] sent an illegal movement key down: [movekeyName] ([movekey])") + // og_debug("Client [ckey] sent an illegal movement key down: [movekeyName] ([movekey])") // We forward tgui keys nowadays return // Record that we are now holding the key! @@ -57,7 +57,7 @@ var/global/list/MOVE_KEY_MAPPINGS = list( // Validate input. Must be one (and only one) of the key codes) if(isnull(movekey) || (movekey & ~0xFFF) || (movekey & (movekey - 1))) - log_debug("Client [ckey] sent an illegal movement key up: [movekeyName] ([movekey])") + // log_debug("Client [ckey] sent an illegal movement key up: [movekeyName] ([movekey])") // We forward tgui keys nowadays return // Clear bit indicating we were holding the key diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm index c95ab2ff64..f337b41cdc 100644 --- a/code/modules/tgui/tgui.dm +++ b/code/modules/tgui/tgui.dm @@ -106,11 +106,8 @@ strict_mode = TRUE, fancy = user.read_preference(/datum/preference/toggle/tgui_fancy), assets = list( - // FIXME: Delete this when 516 is required! - user.client.byond_version >= 516 \ - ? get_asset_datum(/datum/asset/simple/tgui_edge) \ - : get_asset_datum(/datum/asset/simple/tgui), - )) + get_asset_datum(/datum/asset/simple/tgui), + )) else window.send_message("ping") send_assets() diff --git a/code/modules/tgui_input/color.dm b/code/modules/tgui_input/color.dm index 83b22e206c..23c1122a92 100644 --- a/code/modules/tgui_input/color.dm +++ b/code/modules/tgui_input/color.dm @@ -18,7 +18,7 @@ else return // Client does NOT have tgui_input on: Returns regular input - if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_mode) || user.client.byond_version < 516) // Todo, remove once virgo is on 516, but currently the TGUI interface is already programmed for edge + if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_mode)) return input(user, message, title, default) as color|null var/datum/tgui_color_picker/picker = new(user, message, title, default, timeout, autofocus) picker.tgui_interact(user) diff --git a/code/modules/tgui_input/say_modal/modal.dm b/code/modules/tgui_input/say_modal/modal.dm index 40dba424c0..c43554ac12 100644 --- a/code/modules/tgui_input/say_modal/modal.dm +++ b/code/modules/tgui_input/say_modal/modal.dm @@ -65,11 +65,13 @@ window_open = FALSE var/minimumHeight = client?.prefs?.read_preference(/datum/preference/numeric/tgui_say_height) || 1 - winset(client, "tgui_say", "pos=410,400;size=360,[(minimumHeight * 20) + 10];is-visible=0;") + var/minimumWidth = client?.prefs?.read_preference(/datum/preference/numeric/tgui_say_width) || 1 + winset(client, "tgui_say", "pos=410,400;size=360,30;is-visible=0;") window.send_message("props", list( lightMode = client?.prefs?.read_preference(/datum/preference/toggle/tgui_say_light), minimumHeight = minimumHeight, + minimumWidth = minimumWidth, maxLength = max_length, )) diff --git a/code/modules/tgui_panel/external.dm b/code/modules/tgui_panel/external.dm index 8912eb8c12..6574dd6ec9 100644 --- a/code/modules/tgui_panel/external.dm +++ b/code/modules/tgui_panel/external.dm @@ -32,12 +32,9 @@ tgui_panel.initialize(force = TRUE) // Force show the panel to see if there are any errors winset(src, "legacy_output_selector", "left=output_browser") - // TODO: Remove version check with 516 - if(byond_version >= 516) - if(prefs?.read_preference(/datum/preference/toggle/browser_dev_tools)) - winset(src, null, "browser-options=[DEFAULT_CLIENT_BROWSER_OPTIONS],devtools") - else - winset(src, null, "browser-options=[DEFAULT_CLIENT_BROWSER_OPTIONS]") + + if(prefs?.read_preference(/datum/preference/toggle/browser_dev_tools)) + winset(src, null, "browser-options=[DEFAULT_CLIENT_BROWSER_OPTIONS],devtools") /client/verb/refresh_tgui() set name = "Refresh TGUI" diff --git a/code/modules/tooltip/tooltip.dm b/code/modules/tooltip/tooltip.dm index 27c0e79d6f..cd267521e5 100644 --- a/code/modules/tooltip/tooltip.dm +++ b/code/modules/tooltip/tooltip.dm @@ -111,7 +111,8 @@ Notes: last_target = null /datum/tooltip/proc/do_hide() - winshow(owner, control, FALSE) + if(owner) + winshow(owner, control, FALSE) /datum/tooltip/Destroy(force) last_target = null diff --git a/tgui/package.json b/tgui/package.json index 7f0bf06de2..960b6df590 100644 --- a/tgui/package.json +++ b/tgui/package.json @@ -9,7 +9,7 @@ "scripts": { "tgui:analyze": "webpack --analyze", "tgui:bench": "webpack --env TGUI_BENCH=1 && node packages/tgui-bench/index.js", - "tgui:build": "BROWSERSLIST_IGNORE_OLD_DATA=true webpack && webpack --config ./webpack.config.edge.js", + "tgui:build": "BROWSERSLIST_IGNORE_OLD_DATA=true webpack", "tgui:dev": "node --experimental-modules packages/tgui-dev-server/index.js", "tgui:lint": "eslint packages --ext .js,.cjs,.ts,.tsx", "tgui:prettier": "prettier --check .", diff --git a/tgui/packages/common/redux.test.ts b/tgui/packages/common/redux.test.ts new file mode 100644 index 0000000000..d4af99907c --- /dev/null +++ b/tgui/packages/common/redux.test.ts @@ -0,0 +1,68 @@ +import { + Action, + applyMiddleware, + combineReducers, + createAction, + createStore, + Reducer, +} from './redux'; + +// Dummy Reducer +const counterReducer: Reducer> = (state = 0, action) => { + switch (action.type) { + case 'INCREMENT': + return state + 1; + case 'DECREMENT': + return state - 1; + default: + return state; + } +}; + +// Dummy Middleware +const loggingMiddleware = (storeApi) => (next) => (action) => { + console.log('Middleware:', action); + return next(action); +}; + +// Dummy Action Creators +const increment = createAction('INCREMENT'); +const decrement = createAction('DECREMENT'); + +describe('Redux implementation tests', () => { + test('createStore works', () => { + const store = createStore(counterReducer); + expect(store.getState()).toBe(0); + }); + + test('createStore with applyMiddleware works', () => { + const store = createStore( + counterReducer, + applyMiddleware(loggingMiddleware), + ); + expect(store.getState()).toBe(0); + }); + + test('dispatch works', () => { + const store = createStore(counterReducer); + store.dispatch(increment()); + expect(store.getState()).toBe(1); + store.dispatch(decrement()); + expect(store.getState()).toBe(0); + }); + + test('combineReducers works', () => { + const rootReducer = combineReducers({ + counter: counterReducer, + }); + const store = createStore(rootReducer); + expect(store.getState()).toEqual({ counter: 0 }); + }); + + test('createAction works', () => { + const incrementAction = increment(); + expect(incrementAction).toEqual({ type: 'INCREMENT' }); + const decrementAction = decrement(); + expect(decrementAction).toEqual({ type: 'DECREMENT' }); + }); +}); diff --git a/tgui/packages/common/storage.js b/tgui/packages/common/storage.js index 2235199075..fda143c97a 100644 --- a/tgui/packages/common/storage.js +++ b/tgui/packages/common/storage.js @@ -29,36 +29,6 @@ const testHubStorage = testGeneric( () => window.hubStorage && window.hubStorage.getItem, ); -// TODO: Remove with 516 -// prettier-ignore -const testIndexedDb = testGeneric(() => ( - (window.indexedDB || window.msIndexedDB) - && (window.IDBTransaction || window.msIDBTransaction) -)); - -class MemoryBackend { - constructor() { - this.impl = IMPL_MEMORY; - this.store = {}; - } - - async get(key) { - return this.store[key]; - } - - async set(key, value) { - this.store[key] = value; - } - - async remove(key) { - this.store[key] = undefined; - } - - async clear() { - this.store = {}; - } -} - class HubStorageBackend { constructor() { this.impl = IMPL_HUB_STORAGE; @@ -84,63 +54,6 @@ class HubStorageBackend { } } -class IndexedDbBackend { - // TODO: Remove with 516 - constructor() { - this.impl = IMPL_INDEXED_DB; - /** @type {Promise} */ - this.dbPromise = new Promise((resolve, reject) => { - const indexedDB = window.indexedDB || window.msIndexedDB; - const req = indexedDB.open(INDEXED_DB_NAME, INDEXED_DB_VERSION); - req.onupgradeneeded = () => { - try { - req.result.createObjectStore(INDEXED_DB_STORE_NAME); - } catch (err) { - reject(new Error('Failed to upgrade IDB: ' + req.error)); - } - }; - req.onsuccess = () => resolve(req.result); - req.onerror = () => { - reject(new Error('Failed to open IDB: ' + req.error)); - }; - }); - } - - async getStore(mode) { - // prettier-ignore - return this.dbPromise.then((db) => db - .transaction(INDEXED_DB_STORE_NAME, mode) - .objectStore(INDEXED_DB_STORE_NAME)); - } - - async get(key) { - const store = await this.getStore(READ_ONLY); - return new Promise((resolve, reject) => { - const req = store.get(key); - req.onsuccess = () => resolve(req.result); - req.onerror = () => reject(req.error); - }); - } - - async set(key, value) { - // NOTE: We deliberately make this operation transactionless - const store = await this.getStore(READ_WRITE); - store.put(value, key); - } - - async remove(key) { - // NOTE: We deliberately make this operation transactionless - const store = await this.getStore(READ_WRITE); - store.delete(key); - } - - async clear() { - // NOTE: We deliberately make this operation transactionless - const store = await this.getStore(READ_WRITE); - store.clear(); - } -} - /** * Web Storage Proxy object, which selects the best backend available * depending on the environment. @@ -151,18 +64,6 @@ class StorageProxy { if (!Byond.TRIDENT && testHubStorage()) { return new HubStorageBackend(); } - // TODO: Remove with 516 - if (testIndexedDb()) { - try { - const backend = new IndexedDbBackend(); - await backend.dbPromise; - return backend; - } catch {} - } - console.warn( - 'No supported storage backend found. Using in-memory storage.', - ); - return new MemoryBackend(); })(); } diff --git a/tgui/packages/common/vector.ts b/tgui/packages/common/vector.ts deleted file mode 100644 index c91715a8f9..0000000000 --- a/tgui/packages/common/vector.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * N-dimensional vector manipulation functions. - * - * Vectors are plain number arrays, i.e. [x, y, z]. - * - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { map, reduce, zip } from './collections'; - -const ADD = (a: number, b: number): number => a + b; -const SUB = (a: number, b: number): number => a - b; -const MUL = (a: number, b: number): number => a * b; -const DIV = (a: number, b: number): number => a / b; - -export type Vector = number[]; - -export const vecAdd = (...vecs: Vector[]): Vector => { - return map(zip(...vecs), (x) => reduce(x, ADD)); -}; - -export const vecSubtract = (...vecs: Vector[]): Vector => { - return map(zip(...vecs), (x) => reduce(x, SUB)); -}; - -export const vecMultiply = (...vecs: Vector[]): Vector => { - return map(zip(...vecs), (x) => reduce(x, MUL)); -}; - -export const vecDivide = (...vecs: Vector[]): Vector => { - return map(zip(...vecs), (x) => reduce(x, DIV)); -}; - -export const vecScale = (vec: Vector, n: number): Vector => { - return map(vec, (x) => x * n); -}; - -export const vecInverse = (vec: Vector): Vector => { - return map(vec, (x) => -x); -}; - -export const vecLength = (vec: Vector): number => { - return Math.sqrt(reduce(vecMultiply(vec, vec), ADD)); -}; - -export const vecNormalize = (vec: Vector): Vector => { - const length = vecLength(vec); - return map(vec, (c) => c / length); -}; diff --git a/tgui/packages/tgui-dev-server/webpack.js b/tgui/packages/tgui-dev-server/webpack.js index 4addb3486d..e4fbdeb9f1 100644 --- a/tgui/packages/tgui-dev-server/webpack.js +++ b/tgui/packages/tgui-dev-server/webpack.js @@ -31,7 +31,7 @@ class WebpackCompiler { // and retrieve all necessary dependencies. const requireFromRoot = createRequire(dirname(import.meta.url) + '/../..'); const webpack = await requireFromRoot('webpack'); - const createConfig = await requireFromRoot('./webpack.config.edge.js'); + const createConfig = await requireFromRoot('./webpack.config.js'); const config = createConfig({}, options); // Inject the HMR plugin into the config if we're using it if (options.hot) { diff --git a/tgui/packages/tgui-panel/chat/middleware.ts b/tgui/packages/tgui-panel/chat/middleware.ts index ab0fd29f87..fef65dc38d 100644 --- a/tgui/packages/tgui-panel/chat/middleware.ts +++ b/tgui/packages/tgui-panel/chat/middleware.ts @@ -217,6 +217,7 @@ export const chatMiddleware = (store) => { chatRenderer.processBatch([payload_obj.content], { doArchive: true, }); + sequences.push(sequence); if (game.roundId !== settings.lastId) { storedRounds.push(game.roundId); storedLines.push(settings.totalStoredMessages - 1); diff --git a/tgui/packages/tgui-panel/chat/renderer.tsx b/tgui/packages/tgui-panel/chat/renderer.tsx index fd8ff7ebeb..c70ed28c14 100644 --- a/tgui/packages/tgui-panel/chat/renderer.tsx +++ b/tgui/packages/tgui-panel/chat/renderer.tsx @@ -29,7 +29,7 @@ import { typeIsImportant, } from './model'; import { highlightNode, linkifyNode } from './replaceInTextNode'; -import { message } from './types'; +import type { message } from './types'; const logger = createLogger('chatRenderer'); diff --git a/tgui/packages/tgui-panel/panelFocus.ts b/tgui/packages/tgui-panel/panelFocus.ts index 4973e457ed..d47207d209 100644 --- a/tgui/packages/tgui-panel/panelFocus.ts +++ b/tgui/packages/tgui-panel/panelFocus.ts @@ -7,9 +7,9 @@ * @license MIT */ -import { vecLength, vecSubtract } from 'common/vector'; import { focusMap } from 'tgui/focus'; import { canStealFocus, globalEvents } from 'tgui-core/events'; +import { vecLength, vecSubtract } from 'tgui-core/vector'; // Empyrically determined number for the smallest possible // text you can select with the mouse. diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss index 1aa5fe24fb..dd7a16499a 100644 --- a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss +++ b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss @@ -15,7 +15,6 @@ img { margin: 0; padding: 0; line-height: 1; - -ms-interpolation-mode: nearest-neighbor; // TODO: Remove with 516 image-rendering: pixelated; } diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss index a769314e1b..ff4a35f8e4 100644 --- a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss +++ b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss @@ -33,7 +33,6 @@ img { margin: 0; padding: 0; line-height: 1; - -ms-interpolation-mode: nearest-neighbor; // TODO: Remove with 516 image-rendering: pixelated; } diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-vchatdark.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-vchatdark.scss index b660428ad9..3379b949ef 100644 --- a/tgui/packages/tgui-panel/styles/tgchat/chat-vchatdark.scss +++ b/tgui/packages/tgui-panel/styles/tgchat/chat-vchatdark.scss @@ -15,7 +15,6 @@ img { margin: 0; padding: 0; line-height: 1; - -ms-interpolation-mode: nearest-neighbor; // TODO: Remove with 516 image-rendering: pixelated; } diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-vchatlight.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-vchatlight.scss index 0345879cb0..f5b53a80c2 100644 --- a/tgui/packages/tgui-panel/styles/tgchat/chat-vchatlight.scss +++ b/tgui/packages/tgui-panel/styles/tgchat/chat-vchatlight.scss @@ -33,7 +33,6 @@ img { margin: 0; padding: 0; line-height: 1; - -ms-interpolation-mode: nearest-neighbor; // TODO: Remove with 516 image-rendering: pixelated; } diff --git a/tgui/packages/tgui-say/TguiSay.tsx b/tgui/packages/tgui-say/TguiSay.tsx index 040359b7c3..dd75fbf0e7 100644 --- a/tgui/packages/tgui-say/TguiSay.tsx +++ b/tgui/packages/tgui-say/TguiSay.tsx @@ -1,17 +1,15 @@ -import { Component, createRef, RefObject } from 'react'; +import './styles/main.scss'; + +import { FormEvent, KeyboardEvent, useEffect, useRef, useState } from 'react'; import { dragStartHandler } from 'tgui/drag'; -import { - removeAllSkiplines, - sanitizeMultiline, -} from 'tgui/interfaces/TextInputModal'; import { isEscape, KEY } from 'tgui-core/keys'; import { clamp } from 'tgui-core/math'; -import { BooleanLike } from 'tgui-core/react'; +import { type BooleanLike, classes } from 'tgui-core/react'; import { Channel, ChannelIterator } from './ChannelIterator'; import { ChatHistory } from './ChatHistory'; -import { LINE_LENGTHS, RADIO_PREFIXES, WINDOW_SIZES } from './constants'; -import { windowClose, windowOpen, windowSet } from './helpers'; +import { LineLength, RADIO_PREFIXES, WindowSize } from './constants'; +import { getPrefix, windowClose, windowOpen, windowSet } from './helpers'; import { byondMessages } from './timers'; type ByondOpen = { @@ -21,390 +19,330 @@ type ByondOpen = { type ByondProps = { maxLength: number; minimumHeight: number; + minimumWidth: number; lightMode: BooleanLike; }; -type State = { - buttonContent: string | number; - size: number; -}; - -const CHANNEL_REGEX = /^:\w\s|^,b\s/; - -const ROWS: Record = { - small: 1, - medium: 2, - large: 3, - max: 6, - width: 1, // not used +const ROWS: Record = { + Small: 1, + Medium: 2, + Large: 3, + Max: 20, + Width: 360, + MaxWidth: 800, } as const; -export class TguiSay extends Component<{}, State> { - private channelIterator: ChannelIterator; - private chatHistory: ChatHistory; - private currentPrefix: keyof typeof RADIO_PREFIXES | null; - private innerRef: RefObject; - private lightMode: boolean; - private minimumHeight: number; - private maxLength: number; - private messages: typeof byondMessages; - state: State; +export function TguiSay() { + const innerRef = useRef(null); + const channelIterator = useRef(new ChannelIterator()); + const chatHistory = useRef(new ChatHistory()); + const messages = useRef(byondMessages); - constructor(props: never) { - super(props); + // I initially wanted to make these an object or a reducer, but it's not really worth it. + // You lose the granulatity and add a lot of boilerplate. + const [buttonContent, setButtonContent] = useState(''); + const [currentPrefix, setCurrentPrefix] = useState< + keyof typeof RADIO_PREFIXES | null + >(null); + const [size, setSize] = useState(WindowSize.Small); + const [maxLength, setMaxLength] = useState(1024); + const [minimumHeight, setMinimumHeight] = useState(WindowSize.Small); + const [minimumWidth, setMinimumWidth] = useState(WindowSize.Width); + const [lightMode, setLightMode] = useState(false); + const [value, setValue] = useState(''); - this.channelIterator = new ChannelIterator(); - this.chatHistory = new ChatHistory(); - this.currentPrefix = null; - this.innerRef = createRef(); - this.lightMode = false; - this.minimumHeight = 1; - this.maxLength = 1024; - this.messages = byondMessages; - this.state = { - buttonContent: '', - size: WINDOW_SIZES.small, - }; + function handleArrowKeys(direction: KEY.Up | KEY.Down): void { + const chat = chatHistory.current; + const iterator = channelIterator.current; - this.handleArrowKeys = this.handleArrowKeys.bind(this); - this.handleBackspaceDelete = this.handleBackspaceDelete.bind(this); - this.handleClose = this.handleClose.bind(this); - this.handleEnter = this.handleEnter.bind(this); - this.handleForceSay = this.handleForceSay.bind(this); - this.handleIncrementChannel = this.handleIncrementChannel.bind(this); - this.handleInput = this.handleInput.bind(this); - this.handleKeyDown = this.handleKeyDown.bind(this); - this.handleOpen = this.handleOpen.bind(this); - this.handleProps = this.handleProps.bind(this); - this.reset = this.reset.bind(this); - this.setSize = this.setSize.bind(this); - this.setValue = this.setValue.bind(this); - } - - componentDidMount() { - Byond.subscribeTo('props', this.handleProps); - Byond.subscribeTo('force', this.handleForceSay); - Byond.subscribeTo('open', this.handleOpen); - } - - handleArrowKeys(direction: KEY.PageUp | KEY.PageDown) { - const currentValue = this.innerRef.current?.value; - - if (direction === KEY.PageUp) { - if (this.chatHistory.isAtLatest() && currentValue) { + if (direction === KEY.Up) { + if (chat.isAtLatest() && value) { // Save current message to temp history if at the most recent message - this.chatHistory.saveTemp(currentValue); + chat.saveTemp(value); } // Try to get the previous message, fall back to the current value if none - const prevMessage = this.chatHistory.getOlderMessage(); + const prevMessage = chat.getOlderMessage(); if (prevMessage) { - this.setState({ buttonContent: this.chatHistory.getIndex() }); - this.setSize(prevMessage.length); - this.setValue(prevMessage); + setButtonContent(chat.getIndex().toString()); + setValue(prevMessage); } } else { - const nextMessage = - this.chatHistory.getNewerMessage() || this.chatHistory.getTemp() || ''; + const nextMessage = chat.getNewerMessage() || chat.getTemp() || ''; - const buttonContent = this.chatHistory.isAtLatest() - ? this.channelIterator.current() - : this.chatHistory.getIndex(); + const newContent = chat.isAtLatest() + ? iterator.current() + : chat.getIndex().toString(); - this.setState({ buttonContent }); - this.setSize(nextMessage.length); - this.setValue(nextMessage); + setButtonContent(newContent); + setValue(nextMessage); } } - handleBackspaceDelete() { - const typed = this.innerRef.current?.value; + function handleBackspaceDelete(): void { + const chat = chatHistory.current; + const iterator = channelIterator.current; // User is on a chat history message - if (!this.chatHistory.isAtLatest()) { - this.chatHistory.reset(); - this.setState({ - buttonContent: this.currentPrefix ?? this.channelIterator.current(), - }); + if (!chat.isAtLatest()) { + chat.reset(); + setButtonContent(currentPrefix ?? iterator.current()); + // Empty input, resets the channel - } else if ( - !!this.currentPrefix && - this.channelIterator.isSay() && - typed?.length === 0 - ) { - this.currentPrefix = null; - this.setState({ buttonContent: this.channelIterator.current() }); + } else if (currentPrefix && iterator.isSay() && value?.length === 0) { + setCurrentPrefix(null); + setButtonContent(iterator.current()); } - - this.setSize(typed?.length); } - handleClose() { - const current = this.innerRef.current; - - if (current) { - current.blur(); - } - - this.reset(); - this.chatHistory.reset(); - this.channelIterator.reset(); - this.currentPrefix = null; + function handleClose(): void { + innerRef.current?.blur(); windowClose(); + + setTimeout(() => { + chatHistory.current.reset(); + channelIterator.current.reset(); + unloadChat(); + }, 25); } - handleEnter() { - const prefix = this.currentPrefix ?? ''; - const value = this.innerRef.current?.value; - - if (value?.length && value.length < this.maxLength) { - this.chatHistory.add(value); - - // Everything can be multiline, but only emotes get passed that way to the game - const sanitizedValue = this.channelIterator.isMultiline() - ? sanitizeMultiline(value) - : removeAllSkiplines(value); + function handleEnter(): void { + const iterator = channelIterator.current; + const prefix = currentPrefix ?? ''; + if (value?.length && value.length < maxLength) { + chatHistory.current.add(value); Byond.sendMessage('entry', { - channel: this.channelIterator.current(), - entry: this.channelIterator.isSay() - ? prefix + sanitizedValue - : sanitizedValue, + channel: iterator.current(), + entry: iterator.isSay() ? prefix + value : value, }); } - this.handleClose(); + handleClose(); } - handleForceSay() { - const currentValue = this.innerRef.current?.value; + function handleForceSay(): void { + const iterator = channelIterator.current; + // Only force say if we're on a visible channel and have typed something - if (!currentValue || !this.channelIterator.isVisible()) return; + if (!value || iterator.isVisible()) return; - const prefix = this.currentPrefix ?? ''; - const grunt = this.channelIterator.isSay() - ? prefix + currentValue - : currentValue; + const prefix = currentPrefix ?? ''; + const grunt = iterator.isSay() ? prefix + value : value; - this.messages.forceSayMsg(grunt); - this.reset(); + messages.current.forceSayMsg(grunt, iterator.current()); + unloadChat(); } - handleIncrementChannel() { - this.currentPrefix = null; + function handleIncrementChannel(): void { + const iterator = channelIterator.current; - this.channelIterator.next(); - - // If we've looped onto a quiet channel, tell byond to hide thinking indicators - if (!this.channelIterator.isVisible()) { - this.messages.channelIncrementMsg(false, this.channelIterator.current()); - } else { - this.messages.channelIncrementMsg(true, this.channelIterator.current()); - } - - this.setState({ buttonContent: this.channelIterator.current() }); + iterator.next(); + setButtonContent(iterator.current()); + setCurrentPrefix(null); + messages.current.channelIncrementMsg(iterator.isVisible()); } - handleDecrementChannel() { - this.currentPrefix = null; + function handleInput(event: FormEvent): void { + let newValue = event.currentTarget.value; - this.channelIterator.prev(); + let newPrefix = getPrefix(newValue) || currentPrefix; + // Handles switching prefixes + if (newPrefix && newPrefix !== currentPrefix) { + setButtonContent(RADIO_PREFIXES[newPrefix]); + setCurrentPrefix(newPrefix); + newValue = newValue.slice(3); - // If we've looped onto a quiet channel, tell byond to hide thinking indicators - if (!this.channelIterator.isVisible()) { - this.messages.channelIncrementMsg(false, this.channelIterator.current()); - } else { - this.messages.channelIncrementMsg(true, this.channelIterator.current()); + if (newPrefix === ',b ') { + Byond.sendMessage('thinking', { visible: false }); + } } - this.setState({ buttonContent: this.channelIterator.current() }); + // Handles typing indicators + if (channelIterator.current.isVisible() && newPrefix !== ',b ') { + messages.current.typingMsg(); + } + + setValue(newValue); } - handleInput() { - const typed = this.innerRef.current?.value; - - // If we're typing, send the message - if (this.channelIterator.isVisible()) { - this.messages.typingMsg(this.channelIterator.current()); - } - - this.setSize(typed?.length); - - // Is there a value? Is it long enough to be a prefix? - if (!typed || typed.length < 3) { - return; - } - - if (!CHANNEL_REGEX.test(typed)) { - return; - } - - // Is it a valid prefix? - const prefix = typed - .slice(0, 3) - ?.toLowerCase() as keyof typeof RADIO_PREFIXES; - if (!RADIO_PREFIXES[prefix] || prefix === this.currentPrefix) { - return; - } - - this.channelIterator.set('Say'); - this.currentPrefix = prefix; - this.setState({ buttonContent: RADIO_PREFIXES[prefix] }); - this.setValue(typed.slice(3)); + function getMarkupString( + inputText: string, + markupType: string, + startPosition: number, + endPosition: number, + ) { + return `${inputText.substring(0, startPosition)}${markupType}${inputText.substring(startPosition, endPosition)}${markupType}${inputText.substring(endPosition)}`; } - handleKeyDown(event: React.KeyboardEvent) { - const currentValue = this.innerRef.current?.value; + function handleKeyDown(event: KeyboardEvent): void { + if (event.getModifierState('AltGraph')) return; + switch (event.key) { - case KEY.PageUp: - case KEY.PageDown: - // Allow moving between lines if there are newlines - /* if (currentValue?.includes('\n')) { - break; - } */ + case 'u': // replace with tgui core 1.8.x + if (event.ctrlKey || event.metaKey) { + event.preventDefault(); + const { value, selectionStart, selectionEnd } = event.currentTarget; + event.currentTarget.value = getMarkupString( + value, + '_', + selectionStart, + selectionEnd, + ); + event.currentTarget.selectionEnd = selectionEnd + 2; + } + break; + case 'i': // replace with tgui core 1.8.x + if (event.ctrlKey || event.metaKey) { + event.preventDefault(); + const { value, selectionStart, selectionEnd } = event.currentTarget; + event.currentTarget.value = getMarkupString( + value, + '|', + selectionStart, + selectionEnd, + ); + event.currentTarget.selectionEnd = selectionEnd + 2; + } + break; + case 'b': // replace with tgui core 1.8.x + if (event.ctrlKey || event.metaKey) { + event.preventDefault(); + const { value, selectionStart, selectionEnd } = event.currentTarget; + event.currentTarget.value = getMarkupString( + value, + '+', + selectionStart, + selectionEnd, + ); + event.currentTarget.selectionEnd = selectionEnd + 2; + } + break; + case KEY.Up: + case KEY.Down: event.preventDefault(); - this.handleArrowKeys(event.key); + handleArrowKeys(event.key); break; case KEY.Delete: case KEY.Backspace: - this.handleBackspaceDelete(); + handleBackspaceDelete(); break; case KEY.Enter: - // Allow inputting newlines - if (event.shiftKey) { - break; - } event.preventDefault(); - this.handleEnter(); + handleEnter(); break; case KEY.Tab: event.preventDefault(); - if (event.shiftKey) { - this.handleDecrementChannel(); - } else { - this.handleIncrementChannel(); - } + handleIncrementChannel(); break; default: if (isEscape(event.key)) { - this.handleClose(); + handleClose(); } } } - handleOpen = (data: ByondOpen) => { + function handleOpen(data: ByondOpen): void { setTimeout(() => { - this.innerRef.current?.focus(); - }, 0); + innerRef.current?.focus(); + windowSet(WindowSize.Width, WindowSize.Small); + setSize(WindowSize.Width); + }, 1); const { channel } = data; + const iterator = channelIterator.current; // Catches the case where the modal is already open - if (this.channelIterator.isSay()) { - this.channelIterator.set(channel); + if (iterator.isSay()) { + iterator.set(channel); } - this.setState({ buttonContent: this.channelIterator.current() }); - windowOpen(this.channelIterator.current()); - }; - - handleProps = (data: ByondProps) => { - const { maxLength, minimumHeight, lightMode } = data; - this.maxLength = maxLength; - this.minimumHeight = minimumHeight; - this.setSize(); - this.lightMode = !!lightMode; - }; - - reset() { - this.setValue(''); - this.setSize(); - this.setState({ - buttonContent: this.channelIterator.current(), - }); + setButtonContent(iterator.current()); + windowOpen(iterator.current()); } - setSize(length = 0) { - let newSize: number; - - const currentValue = this.innerRef.current?.value; - - if (currentValue?.includes('\n')) { - newSize = WINDOW_SIZES.large; - } else if (length > LINE_LENGTHS.medium) { - newSize = WINDOW_SIZES.large; - } else if (length <= LINE_LENGTHS.medium && length > LINE_LENGTHS.small) { - newSize = WINDOW_SIZES.medium; - } else { - newSize = WINDOW_SIZES.small; - } - - newSize = clamp(newSize, this.minimumHeight * 20 + 10, WINDOW_SIZES.max); - console.log(newSize); - - if (this.state.size !== newSize) { - this.setState({ size: newSize }); - windowSet(newSize); - } - } - - setValue(value: string) { - const textArea = this.innerRef.current; - if (textArea) { - textArea.value = value; - } - } - - render() { - const theme = - (this.lightMode && 'lightMode') || - (this.currentPrefix && RADIO_PREFIXES[this.currentPrefix]) || - this.channelIterator.current(); - - return ( -
- -
- -
- -