[MIRROR] allow chat setting ex / import (#10315)

Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
This commit is contained in:
CHOMPStation2StaffMirrorBot
2025-03-06 12:08:06 +01:00
committed by GitHub
co-authored by Kashargul
parent 751179cdbe
commit 96ffd04c6a
13 changed files with 228 additions and 45 deletions
+1
View File
@@ -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(),
+4 -8
View File
@@ -135,14 +135,10 @@ async function getRound(
'</body>\n' +
'</html>\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();
}
+12 -4
View File
@@ -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 = [];
+20
View File
@@ -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<string, Page>[] = 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 = {
+27 -2
View File
@@ -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,
@@ -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 += '|';
+1 -1
View File
@@ -1,5 +1,5 @@
export type message = {
node?: Element | string;
node?: HTMLElement | string;
type: string;
text?: string;
html?: string;
@@ -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 ? (
<Button.Confirm
tooltip="Disable local chat logging"
icon="ban"
color="red"
confirmIcon="ban"
@@ -59,6 +65,7 @@ export const ExportTab = (props) => {
</Button.Confirm>
) : (
<Button
tooltip="Enable local chat logging"
icon="download"
color="green"
onClick={() => {
@@ -272,28 +279,67 @@ export const ExportTab = (props) => {
)}
</LabeledList>
<Divider />
<Button icon="save" onClick={() => dispatch(saveChatToDisk())}>
Save chat log
</Button>
{!game.databaseBackendEnabled && (
<Button.Confirm
disabled={purgeButtonText === 'Purged!'}
icon="trash"
color="red"
confirmIcon="trash"
confirmColor="red"
confirmContent="Are you sure?"
onClick={() => {
dispatch(purgeChatMessageArchive());
setPurgeButtonText('Purged!');
setTimeout(() => {
setPurgeButtonText('Purge message archive');
}, 1000);
}}
>
{purgeButtonText}
</Button.Confirm>
)}
<Stack fill>
<Stack.Item mt={0.15}>
<Button
icon="compact-disc"
tooltip="Export chat settings"
onClick={() => dispatch(exportSettings())}
>
Export settings
</Button>
</Stack.Item>
<Stack.Item mt={0.15}>
<Button.File
accept=".json"
tooltip="Import chat settings"
icon="arrow-up-from-bracket"
onSelectFiles={(files) => importChatSettings(files)}
>
Import settings
</Button.File>
</Stack.Item>
<Stack.Item grow mt={0.15}>
<Button
icon="save"
tooltip="Export current tab history into HTML file"
onClick={() => dispatch(saveChatToDisk())}
>
Save chat log
</Button>
</Stack.Item>
<Stack.Item mt={0.15}>
<Button.Confirm
icon="trash"
tooltip="Erase current tab history"
onClick={() => dispatch(clearChat())}
>
Clear chat
</Button.Confirm>
</Stack.Item>
{!game.databaseBackendEnabled && (
<Stack.Item mt={0.15}>
<Button.Confirm
disabled={purgeButtonText === 'Purged!'}
icon="trash"
tooltip="Erase current tab history"
color="red"
confirmIcon="trash"
confirmColor="red"
confirmContent="Are you sure?"
onClick={() => {
dispatch(purgeChatMessageArchive());
setPurgeButtonText('Purged!');
setTimeout(() => {
setPurgeButtonText('Purge message archive');
}, 1000);
}}
>
{purgeButtonText}
</Button.Confirm>
</Stack.Item>
)}
</Stack>
</Section>
);
};
@@ -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 },
}),
);
@@ -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);
+14 -2
View File
@@ -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,
@@ -0,0 +1,55 @@
import { useDispatch } from 'tgui/backend';
import type { Page } from '../chat/types';
import { importSettings } from './actions';
export function exportChatSettings(
settings: Record<string, any>,
pages: Record<string, Page>[],
) {
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<string, Page>[] = [];
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));
}
@@ -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