forked from mystiq/hydrogen-web
Merge branch 'master' into madlittlemods/686-682-local-friendly-development-and-commonjs
This commit is contained in:
commit
1b2a6b5d0e
12 changed files with 260 additions and 30 deletions
25
README.md
25
README.md
|
@ -10,13 +10,34 @@ Hydrogen's goals are:
|
||||||
- It is a standalone webapp, but can also be easily embedded into an existing website/webapp to add chat capabilities.
|
- It is a standalone webapp, but can also be easily embedded into an existing website/webapp to add chat capabilities.
|
||||||
- Loading (unused) parts of the application after initial page load should be supported
|
- Loading (unused) parts of the application after initial page load should be supported
|
||||||
|
|
||||||
|
For embedded usage, see the [SDK instructions](doc/SDK.md).
|
||||||
|
|
||||||
If you find this interesting, come and discuss on [`#hydrogen:matrix.org`](https://matrix.to/#/#hydrogen:matrix.org).
|
If you find this interesting, come and discuss on [`#hydrogen:matrix.org`](https://matrix.to/#/#hydrogen:matrix.org).
|
||||||
|
|
||||||
# How to use
|
# How to use
|
||||||
|
|
||||||
Hydrogen is deployed to [hydrogen.element.io](https://hydrogen.element.io). You can run it locally `yarn install` (only the first time) and `yarn start` in the terminal, and point your browser to `http://localhost:3000`. If you prefer, you can also [use docker](doc/docker.md).
|
Hydrogen is deployed to [hydrogen.element.io](https://hydrogen.element.io). You can also deploy Hydrogen on your own web server:
|
||||||
|
|
||||||
Hydrogen uses symbolic links in the codebase, so if you are on Windows, have a look at [making git & symlinks work](https://github.com/git-for-windows/git/wiki/Symbolic-Links) there.
|
1. Download the [latest release package](https://github.com/vector-im/hydrogen-web/releases).
|
||||||
|
1. Extract the package to the public directory of your web server.
|
||||||
|
1. If this is your first deploy:
|
||||||
|
1. copy `config.sample.json` to `config.json` and if needed, make any modifications (unless you've set up your own [sygnal](https://github.com/matrix-org/sygnal) instance, you don't need to change anything in the `push` section).
|
||||||
|
1. Disable caching entirely on the server for:
|
||||||
|
- `index.html`
|
||||||
|
- `sw.js`
|
||||||
|
- `config.json`
|
||||||
|
- All theme manifests referenced in the `themeManifests` of `config.json`, these files are typically called `theme-{name}.json`.
|
||||||
|
|
||||||
|
These resources will still be cached client-side by the service worker. Because of this; you'll still need to refresh the app twice before config.json changes are applied.
|
||||||
|
|
||||||
|
## Set up a dev environment
|
||||||
|
|
||||||
|
You can run Hydrogen locally by the following commands in the terminal:
|
||||||
|
|
||||||
|
- `yarn install` (only the first time)
|
||||||
|
- `yarn start` in the terminal
|
||||||
|
|
||||||
|
Now point your browser to `http://localhost:3000`. If you prefer, you can also [use docker](doc/docker.md).
|
||||||
|
|
||||||
# FAQ
|
# FAQ
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "hydrogen-web",
|
"name": "hydrogen-web",
|
||||||
"version": "0.2.28",
|
"version": "0.2.29",
|
||||||
"description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB",
|
"description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB",
|
||||||
"directories": {
|
"directories": {
|
||||||
"doc": "doc"
|
"doc": "doc"
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
"test:postcss": "impunity --entry-point scripts/postcss/tests/css-compile-variables.test.js scripts/postcss/tests/css-url-to-variables.test.js",
|
"test:postcss": "impunity --entry-point scripts/postcss/tests/css-compile-variables.test.js scripts/postcss/tests/css-url-to-variables.test.js",
|
||||||
"test:sdk": "yarn build:sdk && cd ./scripts/sdk/test/ && yarn --no-lockfile && node test-sdk-in-esm-vite-build-env.js && node test-sdk-in-commonjs-env.js",
|
"test:sdk": "yarn build:sdk && cd ./scripts/sdk/test/ && yarn --no-lockfile && node test-sdk-in-esm-vite-build-env.js && node test-sdk-in-commonjs-env.js",
|
||||||
"start": "vite --port 3000",
|
"start": "vite --port 3000",
|
||||||
"build": "vite build",
|
"build": "vite build && ./scripts/cleanup.sh",
|
||||||
"build:sdk": "./scripts/sdk/build.sh",
|
"build:sdk": "./scripts/sdk/build.sh",
|
||||||
"watch:sdk": "./scripts/sdk/build.sh && yarn run vite build -c vite.sdk-lib-config.js --watch"
|
"watch:sdk": "./scripts/sdk/build.sh && yarn run vite build -c vite.sdk-lib-config.js --watch"
|
||||||
},
|
},
|
||||||
|
|
|
@ -31,6 +31,18 @@ function appendVariablesToCSS(variables, cssSource) {
|
||||||
return cssSource + getRootSectionWithVariables(variables);
|
return cssSource + getRootSectionWithVariables(variables);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addThemesToConfig(bundle, manifestLocations, defaultThemes) {
|
||||||
|
for (const [fileName, info] of Object.entries(bundle)) {
|
||||||
|
if (fileName === "config.json") {
|
||||||
|
const source = new TextDecoder().decode(info.source);
|
||||||
|
const config = JSON.parse(source);
|
||||||
|
config["themeManifests"] = manifestLocations;
|
||||||
|
config["defaultTheme"] = defaultThemes;
|
||||||
|
info.source = new TextEncoder().encode(JSON.stringify(config, undefined, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function parseBundle(bundle) {
|
function parseBundle(bundle) {
|
||||||
const chunkMap = new Map();
|
const chunkMap = new Map();
|
||||||
const assetMap = new Map();
|
const assetMap = new Map();
|
||||||
|
@ -72,7 +84,7 @@ function parseBundle(bundle) {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = function buildThemes(options) {
|
module.exports = function buildThemes(options) {
|
||||||
let manifest, variants, defaultDark, defaultLight;
|
let manifest, variants, defaultDark, defaultLight, defaultThemes = {};
|
||||||
let isDevelopment = false;
|
let isDevelopment = false;
|
||||||
const virtualModuleId = '@theme/'
|
const virtualModuleId = '@theme/'
|
||||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||||
|
@ -99,9 +111,11 @@ module.exports = function buildThemes(options) {
|
||||||
// This is the default theme, stash the file name for later
|
// This is the default theme, stash the file name for later
|
||||||
if (details.dark) {
|
if (details.dark) {
|
||||||
defaultDark = fileName;
|
defaultDark = fileName;
|
||||||
|
defaultThemes["dark"] = `${name}-${variant}`;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
defaultLight = fileName;
|
defaultLight = fileName;
|
||||||
|
defaultThemes["light"] = `${name}-${variant}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// emit the css as built theme bundle
|
// emit the css as built theme bundle
|
||||||
|
@ -215,6 +229,7 @@ module.exports = function buildThemes(options) {
|
||||||
type: "text/css",
|
type: "text/css",
|
||||||
media: "(prefers-color-scheme: dark)",
|
media: "(prefers-color-scheme: dark)",
|
||||||
href: `./${darkThemeLocation}`,
|
href: `./${darkThemeLocation}`,
|
||||||
|
class: "theme",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -224,6 +239,7 @@ module.exports = function buildThemes(options) {
|
||||||
type: "text/css",
|
type: "text/css",
|
||||||
media: "(prefers-color-scheme: light)",
|
media: "(prefers-color-scheme: light)",
|
||||||
href: `./${lightThemeLocation}`,
|
href: `./${lightThemeLocation}`,
|
||||||
|
class: "theme",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -231,24 +247,36 @@ module.exports = function buildThemes(options) {
|
||||||
|
|
||||||
generateBundle(_, bundle) {
|
generateBundle(_, bundle) {
|
||||||
const { assetMap, chunkMap, runtimeThemeChunk } = parseBundle(bundle);
|
const { assetMap, chunkMap, runtimeThemeChunk } = parseBundle(bundle);
|
||||||
|
const manifestLocations = [];
|
||||||
for (const [location, chunkArray] of chunkMap) {
|
for (const [location, chunkArray] of chunkMap) {
|
||||||
const manifest = require(`${location}/manifest.json`);
|
const manifest = require(`${location}/manifest.json`);
|
||||||
const compiledVariables = options.compiledVariables.get(location);
|
const compiledVariables = options.compiledVariables.get(location);
|
||||||
const derivedVariables = compiledVariables["derived-variables"];
|
const derivedVariables = compiledVariables["derived-variables"];
|
||||||
const icon = compiledVariables["icon"];
|
const icon = compiledVariables["icon"];
|
||||||
|
const builtAssets = {};
|
||||||
|
/**
|
||||||
|
* Generate a mapping from theme name to asset hashed location of said theme in build output.
|
||||||
|
* This can be used to enumerate themes during runtime.
|
||||||
|
*/
|
||||||
|
for (const chunk of chunkArray) {
|
||||||
|
const [, name, variant] = chunk.fileName.match(/theme-(.+)-(.+)\.css/);
|
||||||
|
builtAssets[`${name}-${variant}`] = assetMap.get(chunk.fileName).fileName;
|
||||||
|
}
|
||||||
manifest.source = {
|
manifest.source = {
|
||||||
"built-asset": chunkArray.map(chunk => assetMap.get(chunk.fileName).fileName),
|
"built-assets": builtAssets,
|
||||||
"runtime-asset": assetMap.get(runtimeThemeChunk.fileName).fileName,
|
"runtime-asset": assetMap.get(runtimeThemeChunk.fileName).fileName,
|
||||||
"derived-variables": derivedVariables,
|
"derived-variables": derivedVariables,
|
||||||
"icon": icon
|
"icon": icon
|
||||||
};
|
};
|
||||||
const name = `theme-${manifest.name}.json`;
|
const name = `theme-${manifest.name}.json`;
|
||||||
|
manifestLocations.push(`assets/${name}`);
|
||||||
this.emitFile({
|
this.emitFile({
|
||||||
type: "asset",
|
type: "asset",
|
||||||
name,
|
name,
|
||||||
source: JSON.stringify(manifest),
|
source: JSON.stringify(manifest),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
addThemesToConfig(bundle, manifestLocations, defaultThemes);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ function contentHash(str) {
|
||||||
return hasher.digest();
|
return hasher.digest();
|
||||||
}
|
}
|
||||||
|
|
||||||
function injectServiceWorker(swFile, otherUnhashedFiles, placeholdersPerChunk) {
|
function injectServiceWorker(swFile, findUnhashedFileNamesFromBundle, placeholdersPerChunk) {
|
||||||
const swName = path.basename(swFile);
|
const swName = path.basename(swFile);
|
||||||
let root;
|
let root;
|
||||||
let version;
|
let version;
|
||||||
|
@ -31,6 +31,7 @@ function injectServiceWorker(swFile, otherUnhashedFiles, placeholdersPerChunk) {
|
||||||
logger = config.logger;
|
logger = config.logger;
|
||||||
},
|
},
|
||||||
generateBundle: async function(options, bundle) {
|
generateBundle: async function(options, bundle) {
|
||||||
|
const otherUnhashedFiles = findUnhashedFileNamesFromBundle(bundle);
|
||||||
const unhashedFilenames = [swName].concat(otherUnhashedFiles);
|
const unhashedFilenames = [swName].concat(otherUnhashedFiles);
|
||||||
const unhashedFileContentMap = unhashedFilenames.reduce((map, fileName) => {
|
const unhashedFileContentMap = unhashedFilenames.reduce((map, fileName) => {
|
||||||
const chunkOrAsset = bundle[fileName];
|
const chunkOrAsset = bundle[fileName];
|
||||||
|
|
3
scripts/cleanup.sh
Executable file
3
scripts/cleanup.sh
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Remove icons created in .tmp
|
||||||
|
rm -rf .tmp
|
|
@ -2,6 +2,9 @@ VERSION=$(jq -r ".version" package.json)
|
||||||
PACKAGE=hydrogen-web-$VERSION.tar.gz
|
PACKAGE=hydrogen-web-$VERSION.tar.gz
|
||||||
yarn build
|
yarn build
|
||||||
pushd target
|
pushd target
|
||||||
|
# move config file so we don't override it
|
||||||
|
# when deploying a new version
|
||||||
|
mv config.json config.sample.json
|
||||||
tar -czvf ../$PACKAGE ./
|
tar -czvf ../$PACKAGE ./
|
||||||
popd
|
popd
|
||||||
echo $PACKAGE
|
echo $PACKAGE
|
||||||
|
|
|
@ -50,6 +50,7 @@ export class SettingsViewModel extends ViewModel {
|
||||||
this.minSentImageSizeLimit = 400;
|
this.minSentImageSizeLimit = 400;
|
||||||
this.maxSentImageSizeLimit = 4000;
|
this.maxSentImageSizeLimit = 4000;
|
||||||
this.pushNotifications = new PushNotificationStatus();
|
this.pushNotifications = new PushNotificationStatus();
|
||||||
|
this._activeTheme = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
get _session() {
|
get _session() {
|
||||||
|
@ -76,6 +77,9 @@ export class SettingsViewModel extends ViewModel {
|
||||||
this.sentImageSizeLimit = await this.platform.settingsStorage.getInt("sentImageSizeLimit");
|
this.sentImageSizeLimit = await this.platform.settingsStorage.getInt("sentImageSizeLimit");
|
||||||
this.pushNotifications.supported = await this.platform.notificationService.supportsPush();
|
this.pushNotifications.supported = await this.platform.notificationService.supportsPush();
|
||||||
this.pushNotifications.enabled = await this._session.arePushNotificationsEnabled();
|
this.pushNotifications.enabled = await this._session.arePushNotificationsEnabled();
|
||||||
|
if (!import.meta.env.DEV) {
|
||||||
|
this._activeTheme = await this.platform.themeLoader.getActiveTheme();
|
||||||
|
}
|
||||||
this.emitChange("");
|
this.emitChange("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,6 +131,18 @@ export class SettingsViewModel extends ViewModel {
|
||||||
return this._formatBytes(this._estimate?.usage);
|
return this._formatBytes(this._estimate?.usage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get themes() {
|
||||||
|
return this.platform.themeLoader.themes;
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeTheme() {
|
||||||
|
return this._activeTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(name) {
|
||||||
|
this.platform.themeLoader.setTheme(name);
|
||||||
|
}
|
||||||
|
|
||||||
_formatBytes(n) {
|
_formatBytes(n) {
|
||||||
if (typeof n === "number") {
|
if (typeof n === "number") {
|
||||||
return Math.round(n / (1024 * 1024)).toFixed(1) + " MB";
|
return Math.round(n / (1024 * 1024)).toFixed(1) + " MB";
|
||||||
|
|
|
@ -38,6 +38,7 @@ import {downloadInIframe} from "./dom/download.js";
|
||||||
import {Disposables} from "../../utils/Disposables";
|
import {Disposables} from "../../utils/Disposables";
|
||||||
import {parseHTML} from "./parsehtml.js";
|
import {parseHTML} from "./parsehtml.js";
|
||||||
import {handleAvatarError} from "./ui/avatar";
|
import {handleAvatarError} from "./ui/avatar";
|
||||||
|
import {ThemeLoader} from "./ThemeLoader";
|
||||||
|
|
||||||
function addScript(src) {
|
function addScript(src) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
|
@ -164,20 +165,36 @@ export class Platform {
|
||||||
this._disposables = new Disposables();
|
this._disposables = new Disposables();
|
||||||
this._olmPromise = undefined;
|
this._olmPromise = undefined;
|
||||||
this._workerPromise = undefined;
|
this._workerPromise = undefined;
|
||||||
|
this._themeLoader = import.meta.env.DEV? null: new ThemeLoader(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
try {
|
||||||
|
await this.logger.run("Platform init", async (log) => {
|
||||||
if (!this._config) {
|
if (!this._config) {
|
||||||
if (!this._configURL) {
|
if (!this._configURL) {
|
||||||
throw new Error("Neither config nor configURL was provided!");
|
throw new Error("Neither config nor configURL was provided!");
|
||||||
}
|
}
|
||||||
const {body}= await this.request(this._configURL, {method: "GET", format: "json", cache: true}).response();
|
const {status, body}= await this.request(this._configURL, {method: "GET", format: "json", cache: true}).response();
|
||||||
|
if (status === 404) {
|
||||||
|
throw new Error(`Could not find ${this._configURL}. Did you copy over config.sample.json?`);
|
||||||
|
} else if (status >= 400) {
|
||||||
|
throw new Error(`Got status ${status} while trying to fetch ${this._configURL}`);
|
||||||
|
}
|
||||||
this._config = body;
|
this._config = body;
|
||||||
}
|
}
|
||||||
this.notificationService = new NotificationService(
|
this.notificationService = new NotificationService(
|
||||||
this._serviceWorkerHandler,
|
this._serviceWorkerHandler,
|
||||||
this._config.push
|
this._config.push
|
||||||
);
|
);
|
||||||
|
const manifests = this.config["themeManifests"];
|
||||||
|
await this._themeLoader?.init(manifests);
|
||||||
|
this._themeLoader?.setTheme(await this._themeLoader.getActiveTheme(), log);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this._container.innerText = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_createLogger(isDevelopment) {
|
_createLogger(isDevelopment) {
|
||||||
|
@ -307,6 +324,23 @@ export class Platform {
|
||||||
return DEFINE_VERSION;
|
return DEFINE_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get themeLoader() {
|
||||||
|
return this._themeLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceStylesheet(newPath) {
|
||||||
|
const head = document.querySelector("head");
|
||||||
|
// remove default theme
|
||||||
|
document.querySelectorAll(".theme").forEach(e => e.remove());
|
||||||
|
// add new theme
|
||||||
|
const styleTag = document.createElement("link");
|
||||||
|
styleTag.href = `./${newPath}`;
|
||||||
|
styleTag.rel = "stylesheet";
|
||||||
|
styleTag.type = "text/css";
|
||||||
|
styleTag.className = "theme";
|
||||||
|
head.appendChild(styleTag);
|
||||||
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
this._disposables.dispose();
|
this._disposables.dispose();
|
||||||
}
|
}
|
||||||
|
|
75
src/platform/web/ThemeLoader.ts
Normal file
75
src/platform/web/ThemeLoader.ts
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {ILogItem} from "../../logging/types.js";
|
||||||
|
import type {Platform} from "./Platform.js";
|
||||||
|
|
||||||
|
export class ThemeLoader {
|
||||||
|
private _platform: Platform;
|
||||||
|
private _themeMapping: Record<string, string> = {};
|
||||||
|
|
||||||
|
constructor(platform: Platform) {
|
||||||
|
this._platform = platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(manifestLocations: string[]): Promise<void> {
|
||||||
|
for (const manifestLocation of manifestLocations) {
|
||||||
|
const { body } = await this._platform
|
||||||
|
.request(manifestLocation, {
|
||||||
|
method: "GET",
|
||||||
|
format: "json",
|
||||||
|
cache: true,
|
||||||
|
})
|
||||||
|
.response();
|
||||||
|
/*
|
||||||
|
After build has finished, the source section of each theme manifest
|
||||||
|
contains `built-assets` which is a mapping from the theme-name to the
|
||||||
|
location of the css file in build.
|
||||||
|
*/
|
||||||
|
Object.assign(this._themeMapping, body["source"]["built-assets"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(themeName: string, log?: ILogItem) {
|
||||||
|
this._platform.logger.wrapOrRun(log, {l: "change theme", id: themeName}, () => {
|
||||||
|
const themeLocation = this._themeMapping[themeName];
|
||||||
|
if (!themeLocation) {
|
||||||
|
throw new Error( `Cannot find theme location for theme "${themeName}"!`);
|
||||||
|
}
|
||||||
|
this._platform.replaceStylesheet(themeLocation);
|
||||||
|
this._platform.settingsStorage.setString("theme", themeName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get themes(): string[] {
|
||||||
|
return Object.keys(this._themeMapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getActiveTheme(): Promise<string|undefined> {
|
||||||
|
// check if theme is set via settings
|
||||||
|
let theme = await this._platform.settingsStorage.getString("theme");
|
||||||
|
if (theme) {
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
// return default theme
|
||||||
|
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
|
return this._platform.config["defaultTheme"].dark;
|
||||||
|
} else if (window.matchMedia("(prefers-color-scheme: light)").matches) {
|
||||||
|
return this._platform.config["defaultTheme"].light;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
|
@ -95,8 +95,8 @@ let pendingFetchAbortController = new AbortController();
|
||||||
|
|
||||||
async function handleRequest(request) {
|
async function handleRequest(request) {
|
||||||
try {
|
try {
|
||||||
if (request.url.includes("config.json")) {
|
if (request.url.includes("config.json") || /theme-.+\.json/.test(request.url)) {
|
||||||
return handleConfigRequest(request);
|
return handleStaleWhileRevalidateRequest(request);
|
||||||
}
|
}
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
// rewrite / to /index.html so it hits the cache
|
// rewrite / to /index.html so it hits the cache
|
||||||
|
@ -123,9 +123,13 @@ async function handleRequest(request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleConfigRequest(request) {
|
/**
|
||||||
|
* Stale-while-revalidate caching for certain files
|
||||||
|
* see https://developer.chrome.com/docs/workbox/caching-strategies-overview/#stale-while-revalidate
|
||||||
|
*/
|
||||||
|
async function handleStaleWhileRevalidateRequest(request) {
|
||||||
let response = await readCache(request);
|
let response = await readCache(request);
|
||||||
const networkResponsePromise = fetchAndUpdateConfig(request);
|
const networkResponsePromise = fetchAndUpdateCache(request);
|
||||||
if (response) {
|
if (response) {
|
||||||
return response;
|
return response;
|
||||||
} else {
|
} else {
|
||||||
|
@ -133,7 +137,7 @@ async function handleConfigRequest(request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAndUpdateConfig(request) {
|
async function fetchAndUpdateCache(request) {
|
||||||
const response = await fetch(request, {
|
const response = await fetch(request, {
|
||||||
signal: pendingFetchAbortController.signal,
|
signal: pendingFetchAbortController.signal,
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -156,8 +160,14 @@ async function updateCache(request, response) {
|
||||||
cache.put(request, response.clone());
|
cache.put(request, response.clone());
|
||||||
} else if (request.url.startsWith(baseURL)) {
|
} else if (request.url.startsWith(baseURL)) {
|
||||||
let assetName = request.url.substr(baseURL.length);
|
let assetName = request.url.substr(baseURL.length);
|
||||||
|
let cacheName;
|
||||||
if (HASHED_CACHED_ON_REQUEST_ASSETS.includes(assetName)) {
|
if (HASHED_CACHED_ON_REQUEST_ASSETS.includes(assetName)) {
|
||||||
const cache = await caches.open(hashedCacheName);
|
cacheName = hashedCacheName;
|
||||||
|
} else if (UNHASHED_PRECACHED_ASSETS.includes(assetName)) {
|
||||||
|
cacheName = unhashedCacheName;
|
||||||
|
}
|
||||||
|
if (cacheName) {
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
await cache.put(request, response.clone());
|
await cache.put(request, response.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,6 +97,9 @@ export class SettingsView extends TemplateView {
|
||||||
settingNodes.push(
|
settingNodes.push(
|
||||||
t.h3("Preferences"),
|
t.h3("Preferences"),
|
||||||
row(t, vm.i18n`Scale down images when sending`, this._imageCompressionRange(t, vm)),
|
row(t, vm.i18n`Scale down images when sending`, this._imageCompressionRange(t, vm)),
|
||||||
|
t.if(vm => !import.meta.env.DEV && vm.activeTheme, (t, vm) => {
|
||||||
|
return row(t, vm.i18n`Use the following theme`, this._themeOptions(t, vm));
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
settingNodes.push(
|
settingNodes.push(
|
||||||
t.h3("Application"),
|
t.h3("Application"),
|
||||||
|
@ -135,4 +138,13 @@ export class SettingsView extends TemplateView {
|
||||||
vm.i18n`no resizing`;
|
vm.i18n`no resizing`;
|
||||||
})];
|
})];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_themeOptions(t, vm) {
|
||||||
|
const activeTheme = vm.activeTheme;
|
||||||
|
const optionTags = [];
|
||||||
|
for (const name of vm.themes) {
|
||||||
|
optionTags.push(t.option({value: name, selected: name === activeTheme}, name));
|
||||||
|
}
|
||||||
|
return t.select({onChange: (e) => vm.setTheme(e.target.value)}, optionTags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,27 +16,54 @@ export default defineConfig(({mode}) => {
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
assetFileNames: (asset) => asset.name.includes("config.json") ? "assets/[name][extname]": "assets/[name].[hash][extname]",
|
assetFileNames: (asset) => {
|
||||||
|
if (asset.name.includes("config.json")) {
|
||||||
|
return "[name][extname]";
|
||||||
|
}
|
||||||
|
else if (asset.name.match(/theme-.+\.json/)) {
|
||||||
|
return "assets/[name][extname]";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "assets/[name].[hash][extname]";
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
themeBuilder({
|
themeBuilder({
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
themes: {"element": "./src/platform/web/ui/css/themes/element"},
|
themes: {
|
||||||
|
element: "./src/platform/web/ui/css/themes/element",
|
||||||
|
},
|
||||||
default: "element",
|
default: "element",
|
||||||
},
|
},
|
||||||
compiledVariables
|
compiledVariables,
|
||||||
}),
|
}),
|
||||||
// important this comes before service worker
|
// important this comes before service worker
|
||||||
// otherwise the manifest and the icons it refers to won't be cached
|
// otherwise the manifest and the icons it refers to won't be cached
|
||||||
injectWebManifest("assets/manifest.json"),
|
injectWebManifest("assets/manifest.json"),
|
||||||
injectServiceWorker("./src/platform/web/sw.js", ["index.html"], {
|
injectServiceWorker("./src/platform/web/sw.js", findUnhashedFileNamesFromBundle, {
|
||||||
// placeholders to replace at end of build by chunk name
|
// placeholders to replace at end of build by chunk name
|
||||||
"index": {DEFINE_GLOBAL_HASH: definePlaceholders.DEFINE_GLOBAL_HASH},
|
index: {
|
||||||
"sw": definePlaceholders
|
DEFINE_GLOBAL_HASH: definePlaceholders.DEFINE_GLOBAL_HASH,
|
||||||
|
},
|
||||||
|
sw: definePlaceholders,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
define: definePlaceholders,
|
define: definePlaceholders,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function findUnhashedFileNamesFromBundle(bundle) {
|
||||||
|
const names = ["index.html"];
|
||||||
|
for (const fileName of Object.keys(bundle)) {
|
||||||
|
if (fileName.includes("config.json")) {
|
||||||
|
names.push(fileName);
|
||||||
|
}
|
||||||
|
if (/theme-.+\.json/.test(fileName)) {
|
||||||
|
names.push(fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue