Skip to content

Commit

Permalink
Merge branch 'master' into madlittlemods/686-682-local-friendly-devel…
Browse files Browse the repository at this point in the history
…opment-and-commonjs
  • Loading branch information
bwindels committed May 30, 2022
2 parents b725269 + ed8c985 commit 1b2a6b5
Show file tree
Hide file tree
Showing 12 changed files with 260 additions and 30 deletions.
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
- 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).

# 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:

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

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.
Now point your browser to `http://localhost:3000`. If you prefer, you can also [use docker](doc/docker.md).

# FAQ

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"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",
"directories": {
"doc": "doc"
Expand All @@ -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: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",
"build": "vite build",
"build": "vite build && ./scripts/cleanup.sh",
"build:sdk": "./scripts/sdk/build.sh",
"watch:sdk": "./scripts/sdk/build.sh && yarn run vite build -c vite.sdk-lib-config.js --watch"
},
Expand Down
32 changes: 30 additions & 2 deletions scripts/build-plugins/rollup-plugin-build-themes.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ function appendVariablesToCSS(variables, cssSource) {
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) {
const chunkMap = new Map();
const assetMap = new Map();
Expand Down Expand Up @@ -72,7 +84,7 @@ function parseBundle(bundle) {
}

module.exports = function buildThemes(options) {
let manifest, variants, defaultDark, defaultLight;
let manifest, variants, defaultDark, defaultLight, defaultThemes = {};
let isDevelopment = false;
const virtualModuleId = '@theme/'
const resolvedVirtualModuleId = '\0' + virtualModuleId;
Expand All @@ -99,9 +111,11 @@ module.exports = function buildThemes(options) {
// This is the default theme, stash the file name for later
if (details.dark) {
defaultDark = fileName;
defaultThemes["dark"] = `${name}-${variant}`;
}
else {
defaultLight = fileName;
defaultThemes["light"] = `${name}-${variant}`;
}
}
// emit the css as built theme bundle
Expand Down Expand Up @@ -215,6 +229,7 @@ module.exports = function buildThemes(options) {
type: "text/css",
media: "(prefers-color-scheme: dark)",
href: `./${darkThemeLocation}`,
class: "theme",
}
},
{
Expand All @@ -224,31 +239,44 @@ module.exports = function buildThemes(options) {
type: "text/css",
media: "(prefers-color-scheme: light)",
href: `./${lightThemeLocation}`,
class: "theme",
}
},
];
},

generateBundle(_, bundle) {
const { assetMap, chunkMap, runtimeThemeChunk } = parseBundle(bundle);
const manifestLocations = [];
for (const [location, chunkArray] of chunkMap) {
const manifest = require(`${location}/manifest.json`);
const compiledVariables = options.compiledVariables.get(location);
const derivedVariables = compiledVariables["derived-variables"];
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 = {
"built-asset": chunkArray.map(chunk => assetMap.get(chunk.fileName).fileName),
"built-assets": builtAssets,
"runtime-asset": assetMap.get(runtimeThemeChunk.fileName).fileName,
"derived-variables": derivedVariables,
"icon": icon
};
const name = `theme-${manifest.name}.json`;
manifestLocations.push(`assets/${name}`);
this.emitFile({
type: "asset",
name,
source: JSON.stringify(manifest),
});
}
addThemesToConfig(bundle, manifestLocations, defaultThemes);
},
}
}
3 changes: 2 additions & 1 deletion scripts/build-plugins/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ function contentHash(str) {
return hasher.digest();
}

function injectServiceWorker(swFile, otherUnhashedFiles, placeholdersPerChunk) {
function injectServiceWorker(swFile, findUnhashedFileNamesFromBundle, placeholdersPerChunk) {
const swName = path.basename(swFile);
let root;
let version;
Expand All @@ -31,6 +31,7 @@ function injectServiceWorker(swFile, otherUnhashedFiles, placeholdersPerChunk) {
logger = config.logger;
},
generateBundle: async function(options, bundle) {
const otherUnhashedFiles = findUnhashedFileNamesFromBundle(bundle);
const unhashedFilenames = [swName].concat(otherUnhashedFiles);
const unhashedFileContentMap = unhashedFilenames.reduce((map, fileName) => {
const chunkOrAsset = bundle[fileName];
Expand Down
3 changes: 3 additions & 0 deletions scripts/cleanup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
# Remove icons created in .tmp
rm -rf .tmp
3 changes: 3 additions & 0 deletions scripts/package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ VERSION=$(jq -r ".version" package.json)
PACKAGE=hydrogen-web-$VERSION.tar.gz
yarn build
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 ./
popd
echo $PACKAGE
16 changes: 16 additions & 0 deletions src/domain/session/settings/SettingsViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class SettingsViewModel extends ViewModel {
this.minSentImageSizeLimit = 400;
this.maxSentImageSizeLimit = 4000;
this.pushNotifications = new PushNotificationStatus();
this._activeTheme = undefined;
}

get _session() {
Expand All @@ -76,6 +77,9 @@ export class SettingsViewModel extends ViewModel {
this.sentImageSizeLimit = await this.platform.settingsStorage.getInt("sentImageSizeLimit");
this.pushNotifications.supported = await this.platform.notificationService.supportsPush();
this.pushNotifications.enabled = await this._session.arePushNotificationsEnabled();
if (!import.meta.env.DEV) {
this._activeTheme = await this.platform.themeLoader.getActiveTheme();
}
this.emitChange("");
}

Expand Down Expand Up @@ -127,6 +131,18 @@ export class SettingsViewModel extends ViewModel {
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) {
if (typeof n === "number") {
return Math.round(n / (1024 * 1024)).toFixed(1) + " MB";
Expand Down
54 changes: 44 additions & 10 deletions src/platform/web/Platform.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {downloadInIframe} from "./dom/download.js";
import {Disposables} from "../../utils/Disposables";
import {parseHTML} from "./parsehtml.js";
import {handleAvatarError} from "./ui/avatar";
import {ThemeLoader} from "./ThemeLoader";

function addScript(src) {
return new Promise(function (resolve, reject) {
Expand Down Expand Up @@ -164,20 +165,36 @@ export class Platform {
this._disposables = new Disposables();
this._olmPromise = undefined;
this._workerPromise = undefined;
this._themeLoader = import.meta.env.DEV? null: new ThemeLoader(this);
}

async init() {
if (!this._config) {
if (!this._configURL) {
throw new Error("Neither config nor configURL was provided!");
}
const {body}= await this.request(this._configURL, {method: "GET", format: "json", cache: true}).response();
this._config = body;
try {
await this.logger.run("Platform init", async (log) => {
if (!this._config) {
if (!this._configURL) {
throw new Error("Neither config nor configURL was provided!");
}
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.notificationService = new NotificationService(
this._serviceWorkerHandler,
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;
}
this.notificationService = new NotificationService(
this._serviceWorkerHandler,
this._config.push
);
}

_createLogger(isDevelopment) {
Expand Down Expand Up @@ -307,6 +324,23 @@ export class Platform {
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() {
this._disposables.dispose();
}
Expand Down
75 changes: 75 additions & 0 deletions src/platform/web/ThemeLoader.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit 1b2a6b5

Please sign in to comment.