From e9b2d9499abe6d8722c4b72ee0b983c7cfcd8bec Mon Sep 17 00:00:00 2001 From: John McLear Date: Sat, 18 Apr 2026 14:20:23 +0100 Subject: [PATCH 1/7] Add creator-owned pad settings defaults Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/locales/en.json | 3 + src/node/db/Pad.ts | 54 ++++ src/node/handler/PadMessageHandler.ts | 46 ++- src/static/js/chat.ts | 13 +- src/static/js/pad.ts | 264 ++++++++++++++---- src/static/js/pad_editor.ts | 88 ++++-- src/static/js/types/SocketIOMessage.ts | 7 +- src/templates/pad.html | 60 +++- .../frontend-new/specs/font_type.spec.ts | 2 +- src/tests/frontend-new/specs/language.spec.ts | 10 +- .../frontend-new/specs/pad_settings.spec.ts | 119 ++++++++ .../frontend-new/specs/rtl_url_param.spec.ts | 8 +- 12 files changed, 572 insertions(+), 102 deletions(-) create mode 100644 src/tests/frontend-new/specs/pad_settings.spec.ts diff --git a/src/locales/en.json b/src/locales/en.json index 964619bca8e..98461cba4e0 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -84,12 +84,15 @@ "pad.permissionDenied": "You do not have permission to access this pad", "pad.settings.padSettings": "Pad Settings", + "pad.settings.userSettings": "User Settings", "pad.settings.myView": "My View", + "pad.settings.disablechat": "Disable Chat", "pad.settings.stickychat": "Chat always on screen", "pad.settings.chatandusers": "Show Chat and Users", "pad.settings.colorcheck": "Authorship colors", "pad.settings.linenocheck": "Line numbers", "pad.settings.rtlcheck": "Read content from right to left?", + "pad.settings.enforceSettings": "Enforce Settings", "pad.settings.fontType": "Font type:", "pad.settings.fontType.normal": "Normal", "pad.settings.language": "Language:", diff --git a/src/node/db/Pad.ts b/src/node/db/Pad.ts index 821487cda08..7f400623336 100644 --- a/src/node/db/Pad.ts +++ b/src/node/db/Pad.ts @@ -27,6 +27,22 @@ import pad_utils from "../../static/js/pad_utils"; import {SmartOpAssembler} from "../../static/js/SmartOpAssembler"; import {timesLimit} from "async"; +type PadViewSettings = { + showAuthorColors: boolean; + showLineNumbers: boolean; + rtlIsTrue: boolean; + padFontFamily: string; +}; + +type PadSettings = { + enforceSettings: boolean; + showChat: boolean; + alwaysShowChat: boolean; + chatAndUsers: boolean; + lang: string; + view: PadViewSettings; +}; + /** * Copied from the Etherpad source code. It converts Windows line breaks to Unix * line breaks and convert Tabs to spaces @@ -47,6 +63,7 @@ class Pad { private publicStatus: boolean; private id: string; private savedRevisions: any[]; + private padSettings: PadSettings; /** * @param id * @param [database] - Database object to access this pad's records (and only this pad's records; @@ -64,6 +81,26 @@ class Pad { this.publicStatus = false; this.id = id; this.savedRevisions = []; + this.padSettings = Pad.normalizePadSettings(); + } + + static normalizePadSettings(rawPadSettings: any = {}): PadSettings { + const rawView = rawPadSettings.view ?? {}; + return { + enforceSettings: !!rawPadSettings.enforceSettings, + showChat: rawPadSettings.showChat == null ? settings.padOptions.showChat !== false : + !!rawPadSettings.showChat, + alwaysShowChat: !!rawPadSettings.alwaysShowChat, + chatAndUsers: !!rawPadSettings.chatAndUsers, + lang: typeof rawPadSettings.lang === 'string' ? rawPadSettings.lang : 'en', + view: { + showAuthorColors: rawView.showAuthorColors == null ? true : !!rawView.showAuthorColors, + showLineNumbers: rawView.showLineNumbers == null ? + settings.padOptions.showLineNumbers !== false : !!rawView.showLineNumbers, + rtlIsTrue: !!rawView.rtlIsTrue, + padFontFamily: typeof rawView.padFontFamily === 'string' ? rawView.padFontFamily : '', + }, + }; } apool() { @@ -88,6 +125,22 @@ class Pad { return this.publicStatus; } + getPadSettings() { + return Pad.normalizePadSettings(this.padSettings); + } + + setPadSettings(rawPadSettings: any) { + const nextPadSettings = { + ...this.getPadSettings(), + ...rawPadSettings, + view: { + ...this.getPadSettings().view, + ...(rawPadSettings?.view ?? {}), + }, + }; + this.padSettings = Pad.normalizePadSettings(nextPadSettings); + } + /** * Appends a new revision * @param {Object} aChangeset The changeset to append to the pad @@ -400,6 +453,7 @@ class Pad { const firstChangeset = makeSplice('\n', 0, 0, text, firstAttribs, this.pool); await this.appendRevision(firstChangeset, authorId); } + this.padSettings = Pad.normalizePadSettings(this.padSettings); await hooks.aCallAll('padLoad', {pad: this}); } diff --git a/src/node/handler/PadMessageHandler.ts b/src/node/handler/PadMessageHandler.ts index 072ae648ba5..2aa5895d0a5 100644 --- a/src/node/handler/PadMessageHandler.ts +++ b/src/node/handler/PadMessageHandler.ts @@ -46,7 +46,7 @@ import {RateLimiterMemory} from 'rate-limiter-flexible'; import {ChangesetRequest, PadUserInfo, SocketClientRequest} from "../types/SocketClientRequest"; import {APool, AText, PadAuthor, PadType} from "../types/PadType"; import {ChangeSet} from "../types/ChangeSet"; -import {ChatMessageMessage, ClientReadyMessage, ClientSaveRevisionMessage, ClientSuggestUserName, ClientUserChangesMessage, ClientVarMessage, CustomMessage, PadDeleteMessage, UserNewInfoMessage} from "../../static/js/types/SocketIOMessage"; +import {ChatMessageMessage, ClientReadyMessage, ClientSaveRevisionMessage, ClientSuggestUserName, ClientUserChangesMessage, ClientVarMessage, CustomMessage, PadDeleteMessage, PadOptionsMessage, UserNewInfoMessage} from "../../static/js/types/SocketIOMessage"; import {Builder} from "../../static/js/Builder"; const webaccess = require('../hooks/express/webaccess'); const { checkValidRev } = require('../utils/checkValidRev'); @@ -263,6 +263,36 @@ const handlePadDelete = async (socket: any, padDeleteMessage: PadDeleteMessage) } } +const isPadCreator = async (pad: any, authorId: string) => authorId === await pad.getRevisionAuthor(0); + +const handlePadOptionsMessage = async ( + socket: any, message: PadOptionsMessage & {data: {payload: PadOptionsMessage}}) => { + const session = sessioninfos[socket.id]; + if (!session || !session.author || !session.padId) throw new Error('session not ready'); + const pad = await padManager.getPad(session.padId); + if (!await isPadCreator(pad, session.author)) { + socket.emit('shout', { + type: 'COLLABROOM', + data: { + type: 'shoutMessage', + payload: { + message: { + message: 'Only the pad creator can change pad settings', + sticky: false, + }, + timestamp: Date.now(), + }, + }, + }); + return; + } + pad.setPadSettings(message.data.payload.options); + await pad.saveToDatabase(); + _getRoomSockets(session.padId).forEach((socket) => { + socket.emit('message', message); + }); +}; + /** * Handles a message from a user @@ -413,6 +443,11 @@ exports.handleMessage = async (socket:any, message: ClientVarMessage) => { try { switch (type) { case 'suggestUserName': handleSuggestUserName(socket, message as unknown as ClientSuggestUserName); break; + case 'padoptions': + await handlePadOptionsMessage( + socket, + message as unknown as PadOptionsMessage & {data: {payload: PadOptionsMessage}}); + break; default: throw new Error('unknown message type'); } } catch (err) { @@ -883,8 +918,13 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => { ]); ({colorId: authorColorId, name: authorName} = await authorManager.getAuthor(sessionInfo.author)); + const padExisted = await padManager.doesPadExist(sessionInfo.padId); // load the pad-object from the database const pad = await padManager.getPad(sessionInfo.padId, null, sessionInfo.author); + if (!padExisted && message.padSettingsDefaults) { + pad.setPadSettings(message.padSettingsDefaults); + await pad.saveToDatabase(); + } // these db requests all need the pad object (timestamp of latest revision, author data) const authors = pad.getAllAuthors(); @@ -1025,6 +1065,7 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => { // Warning: never ever send sessionInfo.padId to the client. If the client is read only you // would open a security hole 1 swedish mile wide... + const canEditPadSettings = !sessionInfo.readonly && await isPadCreator(pad, sessionInfo.author); const clientVars:MapArrayType = { skinName: settings.skinName, skinVariants: settings.skinVariants, @@ -1035,7 +1076,7 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => { enableDarkMode: settings.enableDarkMode, automaticReconnectionTimeout: settings.automaticReconnectionTimeout, initialRevisionList: [], - initialOptions: {}, + initialOptions: pad.getPadSettings(), savedRevisions: pad.getSavedRevisions(), collab_client_vars: { initialAttributedText: atext, @@ -1060,6 +1101,7 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => { numConnectedUsers: roomSockets.length + 1, // +1 for this user (not yet in room) readOnlyId: sessionInfo.readOnlyPadId, readonly: sessionInfo.readonly, + canEditPadSettings, serverTimestamp: Date.now(), sessionRefreshInterval: settings.cookie.sessionRefreshInterval, userId: sessionInfo.author, diff --git a/src/static/js/chat.ts b/src/static/js/chat.ts index 6357663c4f6..fdd8b017b3c 100644 --- a/src/static/js/chat.ts +++ b/src/static/js/chat.ts @@ -34,6 +34,7 @@ exports.chat = (() => { let chatMentions = 0; return { show() { + if (pad.settings.hideChat) return; $('#chaticon').removeClass('visible'); $('#chatbox').addClass('visible'); this.scrollDown(true); @@ -49,7 +50,7 @@ exports.chat = (() => { }, 100); }, // Make chat stick to right hand side of screen - stickToScreen(fromInitialCall) { + stickToScreen(fromInitialCall, persistPreference = true) { if ($('#options-stickychat').prop('checked')) { $('#options-stickychat').prop('checked', false); } @@ -65,13 +66,13 @@ exports.chat = (() => { $('#chatbox').css('display', 'flex'); }, 0); - padcookie.setPref('chatAlwaysVisible', isStuck); + if (persistPreference) padcookie.setPref('chatAlwaysVisible', isStuck); $('#options-stickychat').prop('checked', isStuck); }, - chatAndUsers(fromInitialCall) { + chatAndUsers(fromInitialCall, persistPreference = true) { const toEnable = $('#options-chatandusers').is(':checked'); if (toEnable || !userAndChat || fromInitialCall) { - this.stickToScreen(true); + this.stickToScreen(true, persistPreference); $('#options-stickychat').prop('checked', true); $('#options-chatandusers').prop('checked', true); $('#options-stickychat').prop('disabled', true); @@ -80,7 +81,7 @@ exports.chat = (() => { $('#options-stickychat').prop('disabled', false); userAndChat = false; } - padcookie.setPref('chatAndUsers', userAndChat); + if (persistPreference) padcookie.setPref('chatAndUsers', userAndChat); $('#users, .sticky-container') .toggleClass('chatAndUsers popup-show stickyUsers', userAndChat); $('#chatbox').toggleClass('chatAndUsersChat', userAndChat); @@ -204,7 +205,7 @@ exports.chat = (() => { count++; $('#chatcounter').text(count); - if (!chatOpen && ctx.duration > 0) { + if (!pad.settings.hideChat && !chatOpen && ctx.duration > 0) { const text = $('

') .append($('').addClass('author-name').text(ctx.authorName)) // ctx.text was HTML-escaped before calling the hook. Hook functions are trusted diff --git a/src/static/js/pad.ts b/src/static/js/pad.ts index d9cc4e902ed..15c813ebca6 100644 --- a/src/static/js/pad.ts +++ b/src/static/js/pad.ts @@ -80,10 +80,13 @@ const getParameters = [ name: 'showChat', checkVal: null, callback: (val) => { + clientVars.initialOptions.showChat = val !== 'false'; if (val === 'false') { settings.hideChat = true; chat.hide(); $('#chaticon').hide(); + } else { + settings.hideChat = false; } }, }, @@ -176,6 +179,59 @@ const getParams = () => { const getUrlVars = () => new URL(window.location.href).searchParams; +const getCookieLanguage = () => { + const cp = (window as any).clientVars?.cookiePrefix || ''; + return Cookies.get(`${cp}language`) || Cookies.get('language'); +}; + +const getMyViewOverrides = () => { + const language = getCookieLanguage(); + const overrides = { + showChat: padcookie.getPref('showChat'), + alwaysShowChat: padcookie.getPref('chatAlwaysVisible'), + chatAndUsers: padcookie.getPref('chatAndUsers'), + lang: language, + view: { + showAuthorColors: padcookie.getPref('showAuthorshipColors'), + showLineNumbers: padcookie.getPref('showLineNumbers'), + rtlIsTrue: padcookie.getPref('rtlIsTrue'), + padFontFamily: padcookie.getPref('padFontFamily'), + }, + }; + if (language == null) delete overrides.lang; + return overrides; +}; + +const getMyViewDefaults = () => { + const overrides = getMyViewOverrides(); + return { + showChat: overrides.showChat == null ? true : overrides.showChat, + alwaysShowChat: overrides.alwaysShowChat === true, + chatAndUsers: overrides.chatAndUsers === true, + lang: overrides.lang || 'en', + view: { + showAuthorColors: overrides.view.showAuthorColors == null ? true : overrides.view.showAuthorColors, + showLineNumbers: overrides.view.showLineNumbers == null ? true : overrides.view.showLineNumbers, + rtlIsTrue: overrides.view.rtlIsTrue === true, + padFontFamily: overrides.view.padFontFamily || '', + }, + }; +}; + +const normalizeChatOptions = (options) => { + if (options.showChat === false) { + options.alwaysShowChat = false; + options.chatAndUsers = false; + } + if (options.chatAndUsers === true) { + options.showChat = true; + options.alwaysShowChat = true; + } else if (options.alwaysShowChat === true) { + options.showChat = true; + } + return options; +}; + const sendClientReady = (isReconnect) => { let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1); // unescape necessary due to Safari and Opera interpretation of spaces @@ -211,6 +267,7 @@ const sendClientReady = (isReconnect) => { sessionID: Cookies.get(`${cp}sessionID`) || Cookies.get('sessionID'), token, userInfo, + padSettingsDefaults: getMyViewDefaults(), }; // this is a reconnect, lets tell the server our revisionnumber @@ -405,12 +462,121 @@ const pad = { getClientIp: () => clientVars.clientIp, getColorPalette: () => clientVars.colorPalette, getPrivilege: (name) => clientVars.accountPrivs[name], + canEditPadSettings: () => !!clientVars.canEditPadSettings, getUserId: () => pad.myUserInfo.userId, getUserName: () => pad.myUserInfo.name, userList: () => paduserlist.users(), sendClientMessage: (msg) => { pad.collabClient.sendClientMessage(msg); }, + getEffectivePadOptions: () => { + const effectiveOptions = $.extend(true, {}, pad.padOptions); + if (pad.padOptions.enforceSettings) return normalizeChatOptions(effectiveOptions); + const overrides = getMyViewOverrides(); + for (const key of ['showChat', 'alwaysShowChat', 'chatAndUsers', 'lang']) { + if (overrides[key] != null) effectiveOptions[key] = overrides[key]; + } + if (!effectiveOptions.view) effectiveOptions.view = {}; + for (const [key, value] of Object.entries(overrides.view)) { + if (value != null) effectiveOptions.view[key] = value; + } + return normalizeChatOptions(effectiveOptions); + }, + refreshPadSettingsControls: () => { + const padOptions = normalizeChatOptions($.extend(true, {}, pad.padOptions || {})); + const view = padOptions.view || {}; + $('#padsettings-options-disablechat').prop('checked', padOptions.showChat === false); + $('#padsettings-options-stickychat').prop('checked', !!padOptions.alwaysShowChat); + $('#padsettings-options-chatandusers').prop('checked', !!padOptions.chatAndUsers); + $('#padsettings-options-colorscheck').prop('checked', view.showAuthorColors !== false); + $('#padsettings-options-linenoscheck').prop('checked', view.showLineNumbers !== false); + $('#padsettings-options-rtlcheck').prop('checked', !!view.rtlIsTrue); + $('#padsettings-viewfontmenu').val(view.padFontFamily || ''); + $('#padsettings-languagemenu').val(padOptions.lang || 'en'); + $('#padsettings-enforcecheck').prop('checked', !!padOptions.enforceSettings); + $('#padsettings-options-stickychat, #padsettings-options-chatandusers') + .prop('disabled', padOptions.showChat === false); + if ($('select').niceSelect) $('select').niceSelect('update'); + }, + refreshMyViewControls: () => { + const effectiveOptions = pad.getEffectivePadOptions(); + const disabled = !!pad.padOptions.enforceSettings; + $('#options-disablechat').prop('checked', effectiveOptions.showChat === false); + $('#options-stickychat').prop('checked', !!effectiveOptions.alwaysShowChat); + $('#options-chatandusers').prop('checked', !!effectiveOptions.chatAndUsers); + $('#options-colorscheck').prop('checked', effectiveOptions.view?.showAuthorColors !== false); + $('#options-linenoscheck').prop('checked', effectiveOptions.view?.showLineNumbers !== false); + $('#options-rtlcheck').prop('checked', !!effectiveOptions.view?.rtlIsTrue); + $('#viewfontmenu').val(effectiveOptions.view?.padFontFamily || ''); + $('#languagemenu').val(effectiveOptions.lang || 'en'); + $('#settings input[id^="options-"]').prop('disabled', disabled); + $('#viewfontmenu, #languagemenu').prop('disabled', disabled); + $('#options-stickychat, #options-chatandusers') + .prop('disabled', disabled || effectiveOptions.showChat === false); + if ($('select').niceSelect) $('select').niceSelect('update'); + }, + setMyViewOption: (key, value) => { + switch (key) { + case 'showChat': + padcookie.setPref('showChat', value); + if (!value) { + padcookie.setPref('chatAlwaysVisible', false); + padcookie.setPref('chatAndUsers', false); + } + break; + case 'alwaysShowChat': + padcookie.setPref('chatAlwaysVisible', value); + if (value) padcookie.setPref('showChat', true); + break; + case 'chatAndUsers': + padcookie.setPref('chatAndUsers', value); + if (value) padcookie.setPref('chatAlwaysVisible', true); + if (value) padcookie.setPref('showChat', true); + break; + case 'showAuthorColors': + padcookie.setPref('showAuthorshipColors', value); + break; + default: + padcookie.setPref(key, value); + break; + } + pad.refreshMyViewControls(); + pad.applyOptionsChange(); + }, + setMyViewLanguage: (lang) => { + const cp = (window as any).clientVars?.cookiePrefix || ''; + Cookies.set(`${cp}language`, lang); + pad.refreshMyViewControls(); + pad.applyOptionsChange(); + }, + applyShowChat: (enabled) => { + settings.hideChat = !enabled; + if (enabled) { + if (!window.clientVars.readonly) $('#chaticon').show(); + } else { + $('#users, .sticky-container').removeClass('chatAndUsers popup-show stickyUsers'); + $('#chatbox').removeClass('chatAndUsersChat stickyChat visible').hide(); + $('#options-stickychat, #options-chatandusers').prop('checked', false); + $('#chaticon').hide(); + } + }, + applyStickyChat: (enabled) => { + const isSticky = $('#chatbox').hasClass('stickyChat'); + $('#options-stickychat').prop('checked', enabled); + if (enabled !== isSticky) chat.stickToScreen(enabled, false); + if (!enabled) $('#options-stickychat').prop('disabled', false); + }, + applyChatAndUsers: (enabled) => { + const isEnabled = $('#users').hasClass('chatAndUsers'); + $('#options-chatandusers').prop('checked', enabled); + if (enabled !== isEnabled) chat.chatAndUsers(enabled, false); + if (!enabled) $('#options-stickychat').prop('disabled', false); + }, + applyLanguage: (lang) => { + html10n.localize([lang, 'en']); + $('#languagemenu').val(lang); + if ($('select').niceSelect) $('select').niceSelect('update'); + }, init() { padutils.setupGlobalExceptionHandler(); @@ -449,32 +615,13 @@ const pad = { setTimeout(() => { padeditor.ace.focus(); }, 0); - const optionsStickyChat = $('#options-stickychat'); - optionsStickyChat.on('click', () => { chat.stickToScreen(); }); - // if we have a cookie for always showing chat then show it - if (padcookie.getPref('chatAlwaysVisible')) { - chat.stickToScreen(true); // stick it to the screen - optionsStickyChat.prop('checked', true); // set the checkbox to on - } - // if we have a cookie for always showing chat then show it - if (padcookie.getPref('chatAndUsers')) { - chat.chatAndUsers(true); // stick it to the screen - $('#options-chatandusers').prop('checked', true); // set the checkbox to on - } - if (padcookie.getPref('showAuthorshipColors') === false) { - pad.changeViewOption('showAuthorColors', false); - } - if (padcookie.getPref('showLineNumbers') === false) { - pad.changeViewOption('showLineNumbers', false); - } + pad.refreshPadSettingsControls(); + pad.applyOptionsChange(); + pad.refreshMyViewControls(); if (settings.rtlIsExplicit) { // URL or server config explicitly set RTL — takes priority over cookie pad.changeViewOption('rtlIsTrue', settings.rtlIsTrue === true); - } else if (padcookie.getPref('rtlIsTrue') === true) { - pad.changeViewOption('rtlIsTrue', true); } - pad.changeViewOption('padFontFamily', padcookie.getPref('padFontFamily')); - $('#viewfontmenu').val(padcookie.getPref('padFontFamily')).niceSelect('update'); // Prevent sticky chat or chat and users to be checked for mobiles const checkChatAndUsersVisibility = (x) => { @@ -503,7 +650,7 @@ const pad = { // order of inits is important here: padimpexp.init(this); padsavedrevs.init(this); - padeditor.init(pad.padOptions.view || {}, this).then(postAceInit); + padeditor.init(pad.getEffectivePadOptions().view || {}, this).then(postAceInit); paduserlist.init(pad.myUserInfo, this); padconnectionstatus.init(); padmodals.init(this); @@ -592,7 +739,7 @@ const pad = { changePadOption: (key, value) => { const options = {}; options[key] = value; - pad.handleOptionsChange(options); + pad.applyPadSettings(options); pad.collabClient.sendClientMessage( { type: 'padoptions', @@ -600,26 +747,57 @@ const pad = { changedBy: pad.myUserInfo.name || 'unnamed', }); }, - changeViewOption: (key, value) => { + changePadViewOption: (key, value) => { const options = { view: {}, }; options.view[key] = value; - pad.handleOptionsChange(options); + pad.applyPadSettings(options); + pad.collabClient.sendClientMessage( + { + type: 'padoptions', + options, + changedBy: pad.myUserInfo.name || 'unnamed', + }); }, - handleOptionsChange: (opts) => { + changeViewOption: (key, value) => { + const effectiveOptions = pad.getEffectivePadOptions(); + if (!effectiveOptions.view) effectiveOptions.view = {}; + effectiveOptions.view[key] = value; + padeditor.setViewOptions(effectiveOptions.view); + }, + applyPadSettings: (opts = {}) => { // opts object is a full set of options or just // some options to change + for (const key of ['enforceSettings', 'showChat', 'alwaysShowChat', 'chatAndUsers', 'lang']) { + if (opts[key] == null) continue; + pad.padOptions[key] = key === 'lang' ? opts[key] : `${opts[key]}` === 'true'; + } if (opts.view) { if (!pad.padOptions.view) { pad.padOptions.view = {}; } for (const [k, v] of Object.entries(opts.view)) { pad.padOptions.view[k] = v; - padcookie.setPref(k, v); } - padeditor.setViewOptions(pad.padOptions.view); } + normalizeChatOptions(pad.padOptions); + pad.refreshPadSettingsControls(); + pad.applyOptionsChange(); + }, + applyOptionsChange: () => { + const effectiveOptions = pad.getEffectivePadOptions(); + padeditor.setViewOptions(effectiveOptions.view || {}); + pad.applyShowChat(effectiveOptions.showChat !== false); + if (effectiveOptions.showChat !== false) { + if (effectiveOptions.lang) pad.applyLanguage(effectiveOptions.lang); + pad.applyChatAndUsers(!!effectiveOptions.chatAndUsers); + if (!effectiveOptions.chatAndUsers) pad.applyStickyChat(!!effectiveOptions.alwaysShowChat); + } + pad.refreshMyViewControls(); + }, + handleOptionsChange: (opts) => { + pad.applyPadSettings(opts); }, // caller shouldn't mutate the object getPadOptions: () => pad.padOptions, @@ -699,39 +877,19 @@ const pad = { } }, handleIsFullyConnected: (isConnected, isInitialConnect) => { - pad.determineChatVisibility(isConnected && !isInitialConnect); - pad.determineChatAndUsersVisibility(isConnected && !isInitialConnect); - pad.determineAuthorshipColorsVisibility(); + pad.refreshMyViewControls(); setTimeout(() => { padeditbar.toggleDropDown('none'); }, 1000); }, determineChatVisibility: (asNowConnectedFeedback) => { - const chatVisCookie = padcookie.getPref('chatAlwaysVisible'); - if (chatVisCookie) { // if the cookie is set for chat always visible - chat.stickToScreen(true); // stick it to the screen - $('#options-stickychat').prop('checked', true); // set the checkbox to on - } else { - $('#options-stickychat').prop('checked', false); // set the checkbox for off - } + pad.refreshMyViewControls(); }, determineChatAndUsersVisibility: (asNowConnectedFeedback) => { - const chatAUVisCookie = padcookie.getPref('chatAndUsersVisible'); - if (chatAUVisCookie) { // if the cookie is set for chat always visible - chat.chatAndUsers(true); // stick it to the screen - $('#options-chatandusers').prop('checked', true); // set the checkbox to on - } else { - $('#options-chatandusers').prop('checked', false); // set the checkbox for off - } + pad.refreshMyViewControls(); }, determineAuthorshipColorsVisibility: () => { - const authColCookie = padcookie.getPref('showAuthorshipColors'); - if (authColCookie) { - pad.changeViewOption('showAuthorColors', true); - $('#options-colorscheck').prop('checked', true); - } else { - $('#options-colorscheck').prop('checked', false); - } + pad.refreshMyViewControls(); }, handleCollabAction: (action) => { if (action === 'commitPerformed') { diff --git a/src/static/js/pad_editor.ts b/src/static/js/pad_editor.ts index 7feb81e30ea..95263608c82 100644 --- a/src/static/js/pad_editor.ts +++ b/src/static/js/pad_editor.ts @@ -22,8 +22,7 @@ * limitations under the License. */ -import padutils,{Cookies} from "./pad_utils"; -const padcookie = require('./pad_cookie').padcookie; +import padutils from "./pad_utils"; const Ace2Editor = require('./ace').Ace2Editor; import html10n from '../js/vendors/html10n' const skinVariants = require('./skin_variants'); @@ -56,34 +55,74 @@ const padeditor = (() => { $('#viewbarcontents').show(); }, initViewOptions: () => { - // Line numbers + // My View + padutils.bindCheckboxChange($('#options-disablechat'), () => { + pad.setMyViewOption('showChat', !padutils.getCheckbox($('#options-disablechat'))); + }); + padutils.bindCheckboxChange($('#options-stickychat'), () => { + pad.setMyViewOption('alwaysShowChat', padutils.getCheckbox($('#options-stickychat'))); + }); + padutils.bindCheckboxChange($('#options-chatandusers'), () => { + pad.setMyViewOption('chatAndUsers', padutils.getCheckbox($('#options-chatandusers'))); + }); + padutils.bindCheckboxChange($('#options-colorscheck'), () => { + pad.setMyViewOption('showAuthorColors', padutils.getCheckbox($('#options-colorscheck'))); + }); padutils.bindCheckboxChange($('#options-linenoscheck'), () => { - pad.changeViewOption('showLineNumbers', padutils.getCheckbox($('#options-linenoscheck'))); + pad.setMyViewOption('showLineNumbers', padutils.getCheckbox($('#options-linenoscheck'))); + }); + padutils.bindCheckboxChange($('#options-rtlcheck'), () => { + pad.setMyViewOption('rtlIsTrue', padutils.getCheckbox($('#options-rtlcheck'))); + }); + $('#viewfontmenu').on('change', () => { + pad.setMyViewOption('padFontFamily', $('#viewfontmenu').val()); + }); + $('#languagemenu').on('change', () => { + pad.setMyViewLanguage($('#languagemenu').val()); + }); + + // Pad settings + padutils.bindCheckboxChange($('#padsettings-enforcecheck'), () => { + pad.changePadOption('enforceSettings', padutils.getCheckbox($('#padsettings-enforcecheck'))); + }); + padutils.bindCheckboxChange($('#padsettings-options-disablechat'), () => { + pad.changePadOption('showChat', !padutils.getCheckbox($('#padsettings-options-disablechat'))); + }); + padutils.bindCheckboxChange($('#padsettings-options-stickychat'), () => { + pad.changePadOption( + 'alwaysShowChat', padutils.getCheckbox($('#padsettings-options-stickychat'))); + }); + padutils.bindCheckboxChange($('#padsettings-options-chatandusers'), () => { + pad.changePadOption( + 'chatAndUsers', padutils.getCheckbox($('#padsettings-options-chatandusers'))); + }); + // Line numbers + padutils.bindCheckboxChange($('#padsettings-options-linenoscheck'), () => { + pad.changePadViewOption( + 'showLineNumbers', padutils.getCheckbox($('#padsettings-options-linenoscheck'))); }); // Author colors - padutils.bindCheckboxChange($('#options-colorscheck'), () => { - padcookie.setPref('showAuthorshipColors', padutils.getCheckbox('#options-colorscheck')); - pad.changeViewOption('showAuthorColors', padutils.getCheckbox('#options-colorscheck')); + padutils.bindCheckboxChange($('#padsettings-options-colorscheck'), () => { + pad.changePadViewOption( + 'showAuthorColors', padutils.getCheckbox('#padsettings-options-colorscheck')); }); // Right to left - padutils.bindCheckboxChange($('#options-rtlcheck'), () => { - pad.changeViewOption('rtlIsTrue', padutils.getCheckbox($('#options-rtlcheck'))); + padutils.bindCheckboxChange($('#padsettings-options-rtlcheck'), () => { + pad.changePadViewOption( + 'rtlIsTrue', padutils.getCheckbox($('#padsettings-options-rtlcheck'))); }); html10n.bind('localized', () => { - // Don't override RTL when explicitly set via URL/server or user cookie - if (settings && settings.rtlIsExplicit) return; - if (padcookie.getPref('rtlIsTrue') === true) return; - pad.changeViewOption('rtlIsTrue', ('rtl' === html10n.getDirection())); - padutils.setCheckbox($('#options-rtlcheck'), ('rtl' === html10n.getDirection())); + $('#languagemenu').val(html10n.getLanguage()); + $('#padsettings-languagemenu').val(html10n.getLanguage()); }); // font family change - $('#viewfontmenu').on('change', () => { - pad.changeViewOption('padFontFamily', $('#viewfontmenu').val()); + $('#padsettings-viewfontmenu').on('change', () => { + pad.changePadViewOption('padFontFamily', $('#padsettings-viewfontmenu').val()); }); // delete pad @@ -129,7 +168,6 @@ const padeditor = (() => { // Language html10n.bind('localized', () => { - $('#languagemenu').val(html10n.getLanguage()); // translate the value of 'unnamed' and 'Enter your name' textboxes in the userlist // this does not interfere with html10n's normal value-setting because @@ -143,15 +181,13 @@ const padeditor = (() => { } }); }); - $('#languagemenu').val(html10n.getLanguage()); - $('#languagemenu').on('change', () => { - const cp = (window as any).clientVars?.cookiePrefix || ''; - Cookies.set(`${cp}language`, $('#languagemenu').val()); - html10n.localize([$('#languagemenu').val(), 'en']); - if ($('select').niceSelect) { - $('select').niceSelect('update'); - } + $('#padsettings-languagemenu').val(html10n.getLanguage()); + $('#padsettings-languagemenu').on('change', () => { + pad.changePadOption('lang', $('#padsettings-languagemenu').val()); }); + if (pad.canEditPadSettings()) { + $('#pad-settings-section').prop('hidden', false); + } }, setViewOptions: (newOptions) => { const getOption = (key, defaultValue) => { @@ -183,6 +219,8 @@ const padeditor = (() => { } self.ace.setProperty('textface', newOptions.padFontFamily || ''); + $('#viewfontmenu').val(newOptions.padFontFamily || ''); + if ($('select').niceSelect) $('select').niceSelect('update'); }, dispose: () => { if (self.ace) { diff --git a/src/static/js/types/SocketIOMessage.ts b/src/static/js/types/SocketIOMessage.ts index 690c293cba8..fdda78022b2 100644 --- a/src/static/js/types/SocketIOMessage.ts +++ b/src/static/js/types/SocketIOMessage.ts @@ -73,8 +73,9 @@ export type ClientVarPayload = { chatHead: number, readonly: boolean, serverTimestamp: number, - initialOptions: MapArrayType, + initialOptions: PadOption, userId: string, + canEditPadSettings?: boolean, mode: string, randomVersionString: string, skinName: string @@ -184,6 +185,7 @@ export type ClientReadyMessage = { sessionID: string, token: string, userInfo: UserInfo, + padSettingsDefaults?: PadOption, reconnect?: boolean client_rev?: number } @@ -249,7 +251,7 @@ export type PadOption = { "alwaysShowChat"?: boolean, "chatAndUsers"?: boolean, "lang"?: null|string, - view? : MapArrayType + view? : MapArrayType } @@ -322,4 +324,3 @@ export type SocketClientReadyMessage = { reconnect?: boolean client_rev?: number } - diff --git a/src/templates/pad.html b/src/templates/pad.html index 926d16c0305..0723ada9110 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -115,15 +115,19 @@