From 96ffd04c6a1f755ea79683f9ab0f8d23295eea37 Mon Sep 17 00:00:00 2001 From: CHOMPStation2StaffMirrorBot <94713762+CHOMPStation2StaffMirrorBot@users.noreply.github.com> Date: Thu, 6 Mar 2025 04:08:06 -0700 Subject: [PATCH] [MIRROR] allow chat setting ex / import (#10315) Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com> --- tgui/packages/tgui-panel/chat/actions.ts | 1 + tgui/packages/tgui-panel/chat/chatExport.ts | 12 +-- tgui/packages/tgui-panel/chat/middleware.ts | 16 +++- tgui/packages/tgui-panel/chat/reducer.ts | 20 ++++ tgui/packages/tgui-panel/chat/renderer.tsx | 29 +++++- .../tgui-panel/chat/replaceInTextNode.ts | 4 +- tgui/packages/tgui-panel/chat/types.ts | 2 +- .../settings/SettingTabs/ExportTab.tsx | 94 ++++++++++++++----- tgui/packages/tgui-panel/settings/actions.ts | 7 ++ .../tgui-panel/settings/middleware.ts | 15 ++- tgui/packages/tgui-panel/settings/reducer.ts | 16 +++- .../tgui-panel/settings/settingsImExport.ts | 55 +++++++++++ .../tgui-panel/styles/themes/vchatdark.scss | 2 +- 13 files changed, 228 insertions(+), 45 deletions(-) create mode 100644 tgui/packages/tgui-panel/settings/settingsImExport.ts diff --git a/tgui/packages/tgui-panel/chat/actions.ts b/tgui/packages/tgui-panel/chat/actions.ts index e231a3292d..44aa825703 100644 --- a/tgui/packages/tgui-panel/chat/actions.ts +++ b/tgui/packages/tgui-panel/chat/actions.ts @@ -11,6 +11,7 @@ import { createPage } from './model'; export const getChatData = createAction('chat/getChatData'); export const loadChat = createAction('chat/load'); export const rebuildChat = createAction('chat/rebuild'); +export const clearChat = createAction('chat/clear'); export const updateMessageCount = createAction('chat/updateMessageCount'); export const addChatPage = createAction('chat/addPage', () => ({ payload: createPage(), diff --git a/tgui/packages/tgui-panel/chat/chatExport.ts b/tgui/packages/tgui-panel/chat/chatExport.ts index 1c6886c187..1dc954955c 100644 --- a/tgui/packages/tgui-panel/chat/chatExport.ts +++ b/tgui/packages/tgui-panel/chat/chatExport.ts @@ -135,14 +135,10 @@ async function getRound( '\n' + '\n'; - try { - fileHandle.createWritable().then((writableHandle) => { - writableHandle.write(pageHtml); - writableHandle.close(); - }); - } catch (e) { - console.error(e); - } + fileHandle.createWritable().then((writableHandle) => { + writableHandle.write(pageHtml); + writableHandle.close(); + }); resolve(); } diff --git a/tgui/packages/tgui-panel/chat/middleware.ts b/tgui/packages/tgui-panel/chat/middleware.ts index 528702a5bb..a3f8dcd01a 100644 --- a/tgui/packages/tgui-panel/chat/middleware.ts +++ b/tgui/packages/tgui-panel/chat/middleware.ts @@ -11,6 +11,7 @@ import DOMPurify from 'dompurify'; import { selectGame } from '../game/selectors'; import { addHighlightSetting, + importSettings, loadSettings, removeHighlightSetting, updateHighlightSetting, @@ -22,6 +23,7 @@ import { addChatPage, changeChatPage, changeScrollTracking, + clearChat, getChatData, loadChat, moveChatPageLeft, @@ -254,7 +256,7 @@ export const chatMiddleware = (store) => { game.databaseBackendEnabled, ); // Load the chat once settings are loaded - if (!initialized && (settings.initialized || settings.firstLoad)) { + if (!initialized && settings.initialized) { initialized = true; setInterval(() => { saveChatToStorage(store); @@ -338,12 +340,14 @@ export const chatMiddleware = (store) => { type === loadSettings.type || type === addHighlightSetting.type || type === removeHighlightSetting.type || - type === updateHighlightSetting.type + type === updateHighlightSetting.type || + type === importSettings.type ) { next(action); + const nextSettings = selectSettings(store.getState()); chatRenderer.setHighlight( - settings.highlightSettings, - settings.highlightSettingById, + nextSettings.highlightSettings, + nextSettings.highlightSettingById, ); return; @@ -367,6 +371,10 @@ export const chatMiddleware = (store) => { ); return; } + if (type === clearChat.type) { + chatRenderer.clearChat(); + return; + } if (type === purgeChatMessageArchive.type) { chatRenderer.purgeMessageArchive(); storedRounds = []; diff --git a/tgui/packages/tgui-panel/chat/reducer.ts b/tgui/packages/tgui-panel/chat/reducer.ts index effc942395..fc91438530 100644 --- a/tgui/packages/tgui-panel/chat/reducer.ts +++ b/tgui/packages/tgui-panel/chat/reducer.ts @@ -4,6 +4,7 @@ * @license MIT */ +import { importSettings } from '../settings/actions'; import { addChatPage, changeChatPage, @@ -17,6 +18,7 @@ import { updateMessageCount, } from './actions'; import { canPageAcceptType, createMainPage } from './model'; +import type { Page } from './types'; const mainPage = createMainPage(); @@ -127,6 +129,24 @@ export const chatReducer = (state = initialState, action) => { }, }; } + if (type === importSettings.type) { + const pagesById: Record[] = payload.newPages; + if (!pagesById) { + return state; + } + const newPageIds: string[] = Object.keys(pagesById); + if (!newPageIds) { + return state; + } + + const nextState = { + ...state, + currentPageId: newPageIds[0], + pages: [...newPageIds], + pageById: { ...pagesById }, + }; + return nextState; + } if (type === changeChatPage.type) { const { pageId } = payload; const page = { diff --git a/tgui/packages/tgui-panel/chat/renderer.tsx b/tgui/packages/tgui-panel/chat/renderer.tsx index 808544d699..68fb51b6d6 100644 --- a/tgui/packages/tgui-panel/chat/renderer.tsx +++ b/tgui/packages/tgui-panel/chat/renderer.tsx @@ -292,7 +292,7 @@ class ChatRenderer { } assignStyle(style = {}) { - for (let key of Object.keys(style)) { + for (const key of Object.keys(style)) { if (this.rootNode) { this.rootNode.style.setProperty(key, style[key]); } @@ -313,7 +313,7 @@ class ChatRenderer { const highlightWholeMessage = setting.highlightWholeMessage; const matchWord = setting.matchWord; const matchCase = setting.matchCase; - const allowedRegex = /^[a-z0-9_\-$/^[\s\]\\]+$/gi; + const allowedRegex = /^[a-zа-яё0-9_\-$/^[\s\]\\]+$/gi; const regexEscapeCharacters = /[!#$%^&*)(+=.<>{}[\]:;'"|~`_\-\\/]/g; const lines = String(text) .split(',') @@ -822,6 +822,31 @@ class ChatRenderer { }); } + /** + * @clearChat + * @copyright 2023 + * @author Cheffie + * @link https://github.com/CheffieGithub + * @license MIT + */ + clearChat() { + const messages = this.visibleMessages; + this.visibleMessages = []; + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; + if (this.rootNode && message.node instanceof HTMLElement) { + this.rootNode.removeChild(message.node); + } + // Mark this message as pruned + message.node = 'pruned'; + } + // Remove pruned messages from the message array + this.messages = this.messages.filter( + (message) => message.node !== 'pruned', + ); + logger.log(`Cleared chat`); + } + saveToDisk( logLineCount: number = 0, startLine: number = 0, diff --git a/tgui/packages/tgui-panel/chat/replaceInTextNode.ts b/tgui/packages/tgui-panel/chat/replaceInTextNode.ts index b758f3611a..f2e16124ad 100644 --- a/tgui/packages/tgui-panel/chat/replaceInTextNode.ts +++ b/tgui/packages/tgui-panel/chat/replaceInTextNode.ts @@ -64,7 +64,7 @@ const regexParseNode = (params: { fragment.appendChild(new_node); } // Commit the fragment - if (node && node.parentNode) { + if (node?.parentNode) { node.parentNode.replaceChild(fragment, node); } } @@ -106,7 +106,7 @@ export const replaceInTextNode = for (let word of words) { // Capture if the word is at the beginning, end, middle, // or by itself in a message - wordRegexStr += `^${word}\\W|\\W${word}\\W|\\W${word}$|^${word}$`; + wordRegexStr += `^${word}\\s\\W|\\s\\W${word}\\s\\W|\\s\\W${word}$|^${word}\\s\\W$`; // Make sure the last character for the expression is NOT '|' if (++i !== words.length) { wordRegexStr += '|'; diff --git a/tgui/packages/tgui-panel/chat/types.ts b/tgui/packages/tgui-panel/chat/types.ts index 6393def33d..c42ed491f3 100644 --- a/tgui/packages/tgui-panel/chat/types.ts +++ b/tgui/packages/tgui-panel/chat/types.ts @@ -1,5 +1,5 @@ export type message = { - node?: Element | string; + node?: HTMLElement | string; type: string; text?: string; html?: string; diff --git a/tgui/packages/tgui-panel/settings/SettingTabs/ExportTab.tsx b/tgui/packages/tgui-panel/settings/SettingTabs/ExportTab.tsx index 73d9908da6..e5f5ab85c0 100644 --- a/tgui/packages/tgui-panel/settings/SettingTabs/ExportTab.tsx +++ b/tgui/packages/tgui-panel/settings/SettingTabs/ExportTab.tsx @@ -13,11 +13,16 @@ import { } from 'tgui-core/components'; import { toFixed } from 'tgui-core/math'; -import { purgeChatMessageArchive, saveChatToDisk } from '../../chat/actions'; +import { + clearChat, + purgeChatMessageArchive, + saveChatToDisk, +} from '../../chat/actions'; import { MESSAGE_TYPES } from '../../chat/constants'; import { useGame } from '../../game'; -import { updateSettings, updateToggle } from '../actions'; +import { exportSettings, updateSettings, updateToggle } from '../actions'; import { selectSettings } from '../selectors'; +import { importChatSettings } from '../settingsImExport'; export const ExportTab = (props) => { const dispatch = useDispatch(); @@ -42,6 +47,7 @@ export const ExportTab = (props) => { {!game.databaseBackendEnabled && (logEnable ? ( { ) : ( - {!game.databaseBackendEnabled && ( - { - dispatch(purgeChatMessageArchive()); - setPurgeButtonText('Purged!'); - setTimeout(() => { - setPurgeButtonText('Purge message archive'); - }, 1000); - }} - > - {purgeButtonText} - - )} + + + + + + importChatSettings(files)} + > + Import settings + + + + + + + dispatch(clearChat())} + > + Clear chat + + + {!game.databaseBackendEnabled && ( + + { + dispatch(purgeChatMessageArchive()); + setPurgeButtonText('Purged!'); + setTimeout(() => { + setPurgeButtonText('Purge message archive'); + }, 1000); + }} + > + {purgeButtonText} + + + )} + ); }; diff --git a/tgui/packages/tgui-panel/settings/actions.ts b/tgui/packages/tgui-panel/settings/actions.ts index 1e94bf5b46..9e44ba8970 100644 --- a/tgui/packages/tgui-panel/settings/actions.ts +++ b/tgui/packages/tgui-panel/settings/actions.ts @@ -26,3 +26,10 @@ export const removeHighlightSetting = createAction( export const updateHighlightSetting = createAction( 'settings/updateHighlightSetting', ); +export const exportSettings = createAction('settings/export'); +export const importSettings = createAction( + 'settings/import', + (settings, pages) => ({ + payload: { newSettings: settings, newPages: pages }, + }), +); diff --git a/tgui/packages/tgui-panel/settings/middleware.ts b/tgui/packages/tgui-panel/settings/middleware.ts index e67fe5b23e..b871eaf91a 100644 --- a/tgui/packages/tgui-panel/settings/middleware.ts +++ b/tgui/packages/tgui-panel/settings/middleware.ts @@ -9,6 +9,8 @@ import { storage } from 'common/storage'; import { setClientTheme } from '../themes'; import { addHighlightSetting, + exportSettings, + importSettings, loadSettings, removeHighlightSetting, updateHighlightSetting, @@ -17,6 +19,7 @@ import { } from './actions'; import { FONTS_DISABLED } from './constants'; import { selectSettings } from './selectors'; +import { exportChatSettings } from './settingsImExport'; let statFontTimer: NodeJS.Timeout; let statTabsTimer: NodeJS.Timeout; @@ -90,13 +93,20 @@ export function settingsMiddleware(store) { store.dispatch(loadSettings(settings)); }); } + if (type === exportSettings.type) { + const state = store.getState(); + const settings = selectSettings(state); + exportChatSettings(settings, state.chat.pageById); + return; + } if ( type !== updateSettings.type && type !== updateToggle.type && type !== loadSettings.type && type !== addHighlightSetting.type && type !== removeHighlightSetting.type && - type !== updateHighlightSetting.type + type !== updateHighlightSetting.type && + type !== importSettings.type ) { return next(action); } @@ -112,6 +122,9 @@ export function settingsMiddleware(store) { const settings = selectSettings(store.getState()); + if (importSettings.type) { + setClientTheme(settings.theme); + } // Update stat panel settings setStatTabsStyle(settings.statTabsStyle); diff --git a/tgui/packages/tgui-panel/settings/reducer.ts b/tgui/packages/tgui-panel/settings/reducer.ts index 2fd2e0b94d..e4e21ffaca 100644 --- a/tgui/packages/tgui-panel/settings/reducer.ts +++ b/tgui/packages/tgui-panel/settings/reducer.ts @@ -8,6 +8,7 @@ import { MESSAGE_TYPES } from '../chat/constants'; import { addHighlightSetting, changeSettingsTab, + importSettings, loadSettings, openChatSettings, removeHighlightSetting, @@ -57,7 +58,6 @@ const initialState = { exportEnd: 0, lastId: null, initialized: false, - firstLoad: false, storedTypes: {}, hideImportantInAdminTab: false, interleave: false, @@ -97,7 +97,7 @@ export function settingsReducer( ...state, ...payload, }; - nextState.firstLoad = true; + nextState.initialized = true; return nextState; } @@ -146,6 +146,18 @@ export function settingsReducer( } } + case importSettings.type: { + const newSettings = payload.newSettings; + if (!newSettings) { + return state; + } + const nextState = { + ...state, + ...newSettings, + }; + return nextState; + } + case toggleSettings.type: { return { ...state, diff --git a/tgui/packages/tgui-panel/settings/settingsImExport.ts b/tgui/packages/tgui-panel/settings/settingsImExport.ts new file mode 100644 index 0000000000..70771ef2fa --- /dev/null +++ b/tgui/packages/tgui-panel/settings/settingsImExport.ts @@ -0,0 +1,55 @@ +import { useDispatch } from 'tgui/backend'; + +import type { Page } from '../chat/types'; +import { importSettings } from './actions'; + +export function exportChatSettings( + settings: Record, + pages: Record[], +) { + const opts: SaveFilePickerOptions = { + id: `ss13-chatprefs-${Date.now()}`, + suggestedName: `ss13-chatsettings-${new Date().toJSON().slice(0, 10)}.json`, + types: [ + { + description: 'SS13 file', + accept: { 'application/json': ['.json'] }, + }, + ], + }; + + const pagesEntry: Record[] = []; + pagesEntry['chatPages'] = pages; + + const exportObject = Object.assign(settings, pagesEntry); + + window + .showSaveFilePicker(opts) + .then((fileHandle) => { + fileHandle.createWritable().then((writableHandle) => { + writableHandle.write(JSON.stringify(exportObject)); + writableHandle.close(); + }); + }) + .catch((e) => { + // Log the error if the error has nothing to do with the user aborting the download + if (e.name !== 'AbortError') { + console.error(e); + } + }); +} + +export function importChatSettings(settings: string | string[]) { + if (Array.isArray(settings)) { + return; + } + const dispatch = useDispatch(); + const ourImport = JSON.parse(settings); + if (!ourImport?.version) { + return; + } + const pageRecord = ourImport['chatPages']; + delete ourImport['chatPages']; + + dispatch(importSettings(ourImport, pageRecord)); +} diff --git a/tgui/packages/tgui-panel/styles/themes/vchatdark.scss b/tgui/packages/tgui-panel/styles/themes/vchatdark.scss index cbf544ba1a..52c4be42c0 100644 --- a/tgui/packages/tgui-panel/styles/themes/vchatdark.scss +++ b/tgui/packages/tgui-panel/styles/themes/vchatdark.scss @@ -29,7 +29,7 @@ // Components specific to tgui-panel @include meta.load-css( '../components/Chat.scss', - $with: ('text-color': #ffffff) + $with: ('text-color': hsl(0, 0%, 100%)) ); // Layouts