web/settings: embed vs code for editing custom css

Not meant for mobile yet
This commit is contained in:
Tulir Asokan 2024-12-06 02:26:16 +02:00
parent 63798f2298
commit 803505385a
7 changed files with 163 additions and 6 deletions

7
web/package-lock.json generated
View file

@ -13,6 +13,7 @@
"blurhash": "^2.0.5", "blurhash": "^2.0.5",
"katex": "^0.16.11", "katex": "^0.16.11",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"monaco-editor": "^0.52.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-blurhash": "^0.3.0", "react-blurhash": "^0.3.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
@ -4301,6 +4302,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/monaco-editor": {
"version": "0.52.0",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz",
"integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==",
"license": "MIT"
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View file

@ -15,6 +15,7 @@
"blurhash": "^2.0.5", "blurhash": "^2.0.5",
"katex": "^0.16.11", "katex": "^0.16.11",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"monaco-editor": "^0.52.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-blurhash": "^0.3.0", "react-blurhash": "^0.3.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",

View file

@ -21,6 +21,21 @@ export enum PreferenceContext {
RoomDevice = "room_device", RoomDevice = "room_device",
} }
export function preferenceContextToInt(context: PreferenceContext): number {
switch (context) {
case PreferenceContext.Config:
return 0
case PreferenceContext.Account:
return 1
case PreferenceContext.Device:
return 2
case PreferenceContext.RoomAccount:
return 3
case PreferenceContext.RoomDevice:
return 4
}
}
export const anyContext = [ export const anyContext = [
PreferenceContext.RoomDevice, PreferenceContext.RoomDevice,
PreferenceContext.RoomAccount, PreferenceContext.RoomAccount,

View file

@ -56,6 +56,12 @@ div.settings-view {
font-family: var(--monospace-font-stack); font-family: var(--monospace-font-stack);
} }
> div.vscode-wrapper {
position: fixed;
inset: 0;
z-index: 10;
}
> div.buttons { > div.buttons {
display: flex; display: flex;
justify-content: right; justify-content: right;

View file

@ -13,9 +13,17 @@
// //
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { use, useCallback, useState } from "react" import { lazy, use, useCallback, useRef, useState } from "react"
import Client from "@/api/client.ts"
import { RoomStateStore, usePreferences } from "@/api/statestore" import { RoomStateStore, usePreferences } from "@/api/statestore"
import { Preference, PreferenceContext, PreferenceValueType, Preferences, preferences } from "@/api/types/preferences" import {
Preference,
PreferenceContext,
PreferenceValueType,
Preferences,
preferenceContextToInt,
preferences,
} from "@/api/types/preferences"
import useEvent from "@/util/useEvent.ts" import useEvent from "@/util/useEvent.ts"
import ClientContext from "../ClientContext.ts" import ClientContext from "../ClientContext.ts"
import JSONView from "../util/JSONView.tsx" import JSONView from "../util/JSONView.tsx"
@ -155,9 +163,24 @@ interface SettingsViewProps {
room: RoomStateStore room: RoomStateStore
} }
function getActiveCSSContext(client: Client, room: RoomStateStore): PreferenceContext {
if (room.localPreferenceCache.custom_css !== undefined) {
return PreferenceContext.RoomDevice
} else if (room.serverPreferenceCache.custom_css !== undefined) {
return PreferenceContext.RoomAccount
} else if (client.store.localPreferenceCache.custom_css !== undefined) {
return PreferenceContext.Device
} else {
return PreferenceContext.Account
}
}
const Monaco = lazy(() => import("../util/monaco.tsx"))
const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomStateStore }) => { const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomStateStore }) => {
const client = use(ClientContext)! const client = use(ClientContext)!
const [context, setContext] = useState(PreferenceContext.Account) const appliedContext = getActiveCSSContext(client, room)
const [context, setContext] = useState(appliedContext)
const getContextText = useCallback((context: PreferenceContext) => { const getContextText = useCallback((context: PreferenceContext) => {
if (context === PreferenceContext.Account) { if (context === PreferenceContext.Account) {
return client.store.serverPreferenceCache.custom_css return client.store.serverPreferenceCache.custom_css
@ -180,12 +203,30 @@ const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomSta
setText(evt.target.value) setText(evt.target.value)
}, []) }, [])
const onSave = useEvent(() => { const onSave = useEvent(() => {
if (vscodeOpen) {
setText(vscodeContentRef.current)
setPref(context, "custom_css", vscodeContentRef.current)
} else {
setPref(context, "custom_css", text) setPref(context, "custom_css", text)
}
}) })
const onDelete = useEvent(() => { const onDelete = useEvent(() => {
setPref(context, "custom_css", undefined) setPref(context, "custom_css", undefined)
setText("") setText("")
}) })
const [vscodeOpen, setVSCodeOpen] = useState(false)
const vscodeContentRef = useRef("")
const vscodeInitialContentRef = useRef("")
const onClickVSCode = useEvent(() => {
vscodeContentRef.current = text
vscodeInitialContentRef.current = text
setVSCodeOpen(true)
})
const closeVSCode = useCallback(() => {
setVSCodeOpen(false)
setText(vscodeContentRef.current)
vscodeContentRef.current = ""
}, [])
return <div className="custom-css-input"> return <div className="custom-css-input">
<div className="header"> <div className="header">
<h3>Custom CSS</h3> <h3>Custom CSS</h3>
@ -195,9 +236,21 @@ const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomSta
<option value={PreferenceContext.RoomAccount}>Room (account)</option> <option value={PreferenceContext.RoomAccount}>Room (account)</option>
<option value={PreferenceContext.RoomDevice}>Room (device)</option> <option value={PreferenceContext.RoomDevice}>Room (device)</option>
</select> </select>
{preferenceContextToInt(context) < preferenceContextToInt(appliedContext) &&
<span className="warning">
&#x26a0;&#xfe0f; This context will not be applied, <code>{appliedContext}</code> has content
</span>}
</div> </div>
<textarea value={text} onChange={onChangeText}/> {vscodeOpen ? <div className="vscode-wrapper">
<Monaco
initData={vscodeInitialContentRef.current}
onClose={closeVSCode}
onSave={onSave}
contentRef={vscodeContentRef}
/>
</div> : <textarea value={text} onChange={onChangeText}/>}
<div className="buttons"> <div className="buttons">
<button onClick={onClickVSCode}>Open in VS Code</button>
{origText !== undefined && <button className="delete" onClick={onDelete}>Delete</button>} {origText !== undefined && <button className="delete" onClick={onDelete}>Delete</button>}
<button className="save primary-color-button" onClick={onSave} disabled={origText === text}>Save</button> <button className="save primary-color-button" onClick={onSave} disabled={origText === text}>Save</button>
</div> </div>

View file

@ -0,0 +1,72 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import "monaco-editor/esm/vs/basic-languages/css/css.contribution.js"
import "monaco-editor/esm/vs/editor/edcore.main.js"
import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js"
import CSSWorker from "monaco-editor/esm/vs/language/css/css.worker.js?worker"
import "monaco-editor/esm/vs/language/css/monaco.contribution.js"
import { RefObject, memo, useLayoutEffect, useRef } from "react"
window.MonacoEnvironment = {
getWorker: function() {
return new CSSWorker()
},
}
export interface MonacoProps {
initData: string
onClose: () => void
onSave: () => void
contentRef: RefObject<string>
}
const Monaco = ({ initData, onClose, onSave, contentRef }: MonacoProps) => {
const container = useRef<HTMLDivElement>(null)
const editor = useRef<monaco.editor.IStandaloneCodeEditor>(null)
useLayoutEffect(() => {
if (!container.current) {
return
}
const newEditor = monaco.editor.create(container.current, {
language: "css",
value: initData,
fontLigatures: true,
fontFamily: `var(--monospace-font-stack)`,
theme: window.matchMedia("(prefers-color-scheme: dark)").matches ? "vs-dark" : "vs",
})
const model = newEditor.getModel()
if (!model) {
return
}
model.onDidChangeContent(() => contentRef.current = model.getValue(monaco.editor.EndOfLinePreference.LF))
newEditor.onKeyDown(evt => {
if (evt.keyCode === monaco.KeyCode.Escape) {
onClose()
} else if (evt.ctrlKey && evt.keyCode === monaco.KeyCode.KeyS) {
onSave()
evt.preventDefault()
}
})
newEditor.focus()
editor.current = newEditor
return () => newEditor.dispose()
// All props are intentionally immutable
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return <div style={{ width: "100%", height: "100%" }} ref={container}/>
}
export default memo(Monaco)

View file

@ -2,16 +2,19 @@ import react from "@vitejs/plugin-react-swc"
import { defineConfig } from "vite" import { defineConfig } from "vite"
import svgr from "vite-plugin-svgr" import svgr from "vite-plugin-svgr"
const splitDeps = ["katex", "leaflet", "monaco-editor"]
export default defineConfig({ export default defineConfig({
base: "./", base: "./",
build: { build: {
target: ["esnext", "firefox128", "chrome131", "safari18"], target: ["esnext", "firefox128", "chrome131", "safari18"],
chunkSizeWarningLimit: 3500,
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks: id => { manualChunks: id => {
if (id.includes("wailsio")) { if (id.includes("wailsio")) {
return "wails" return "wails"
} else if (id.includes("node_modules") && !id.includes("katex") && !id.includes("leaflet")) { } else if (id.includes("node_modules") && !splitDeps.some(dep => id.includes(dep))) {
return "vendor" return "vendor"
} else if (id.endsWith("/emoji/data.json")) { } else if (id.endsWith("/emoji/data.json")) {
return "emoji" return "emoji"