Compare commits
7 commits
Author | SHA1 | Date | |
---|---|---|---|
|
5d63069f31 | ||
|
08f9edaf68 | ||
|
6335da0932 | ||
|
7590c55404 | ||
|
2e12ce74b7 | ||
|
27363b3f63 | ||
|
ff706e542d |
9 changed files with 63 additions and 224 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -10,4 +10,3 @@ lib
|
|||
*.tar.gz
|
||||
.eslintcache
|
||||
.tmp
|
||||
tmp/
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
pipeline:
|
||||
buildfrontend:
|
||||
image: node:16
|
||||
commands:
|
||||
- yarn install --prefer-offline --frozen-lockfile
|
||||
- yarn test
|
||||
- yarn run lint-ci
|
||||
- yarn run tsc
|
||||
- yarn build
|
||||
|
||||
deploy:
|
||||
image: python
|
||||
when:
|
||||
event: push
|
||||
branch: master
|
||||
commands:
|
||||
- make ci-deploy
|
||||
secrets: [ GITEA_WRITE_DEPLOY_KEY, LIBREPAGES_DEPLOY_SECRET ]
|
14
Makefile
14
Makefile
|
@ -1,14 +0,0 @@
|
|||
ci-deploy: ## Deploy from CI/CD. Only call from within CI
|
||||
@if [ "${CI}" != "woodpecker" ]; \
|
||||
then echo "Only call from within CI. Will re-write your local Git configuration. To override, set export CI=woodpecker"; \
|
||||
exit 1; \
|
||||
fi
|
||||
git config --global user.email "${CI_COMMIT_AUTHOR_EMAIL}"
|
||||
git config --global user.name "${CI_COMMIT_AUTHOR}"
|
||||
./scripts/ci.sh --commit-files librepages target "${CI_COMMIT_AUTHOR} <${CI_COMMIT_AUTHOR_EMAIL}>"
|
||||
./scripts/ci.sh --init "$$GITEA_WRITE_DEPLOY_KEY"
|
||||
./scripts/ci.sh --deploy ${LIBREPAGES_DEPLOY_SECRET} librepages
|
||||
./scripts/ci.sh --clean
|
||||
|
||||
help: ## Prints help for targets with comments
|
||||
@cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
|
@ -1,5 +1,3 @@
|
|||
[![status-badge](https://ci.batsense.net/api/badges/mystiq/hydrogen-web/status.svg)](https://ci.batsense.net/mystiq/hydrogen-web)
|
||||
|
||||
# Hydrogen
|
||||
|
||||
A minimal [Matrix](https://matrix.org/) chat client, focused on performance, offline functionality, and broad browser support. This is work in progress and not yet ready for primetime. Bug reports are welcome, but please don't file any feature requests or other missing things to be on par with Element Web.
|
||||
|
|
165
scripts/ci.sh
165
scripts/ci.sh
|
@ -1,165 +0,0 @@
|
|||
#!/bin/bash
|
||||
# ci.sh: Helper script to automate deployment operations on CI/CD
|
||||
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
set -xEeuo pipefail
|
||||
#source $(pwd)/scripts/lib.sh
|
||||
|
||||
readonly SSH_ID_FILE=/tmp/ci-ssh-id
|
||||
readonly SSH_REMOTE_NAME=origin-ssh
|
||||
readonly PROJECT_ROOT=$(pwd)
|
||||
|
||||
match_arg() {
|
||||
if [ $1 == $2 ] || [ $1 == $3 ]
|
||||
then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
help() {
|
||||
cat << EOF
|
||||
USAGE: ci.sh [SUBCOMMAND]
|
||||
Helper script to automate deployment operations on CI/CD
|
||||
|
||||
Subcommands
|
||||
|
||||
-c --clean cleanup secrets, SSH key and other runtime data
|
||||
-i --init <SSH_PRIVATE_KEY> initialize environment, write SSH private to file
|
||||
-d --deploy <PAGES-SECRET> <TARGET BRANCH> push branch to Gitea and call Pages server
|
||||
-h --help print this help menu
|
||||
EOF
|
||||
}
|
||||
|
||||
# $1: SSH private key
|
||||
write_ssh(){
|
||||
truncate --size 0 $SSH_ID_FILE
|
||||
echo "$1" > $SSH_ID_FILE
|
||||
chmod 600 $SSH_ID_FILE
|
||||
}
|
||||
|
||||
set_ssh_remote() {
|
||||
http_remote_url=$(git remote get-url origin)
|
||||
remote_hostname=$(echo $http_remote_url | cut -d '/' -f 3)
|
||||
repository_owner=$(echo $http_remote_url | cut -d '/' -f 4)
|
||||
repository_name=$(echo $http_remote_url | cut -d '/' -f 5)
|
||||
ssh_remote="git@$remote_hostname:$repository_owner/$repository_name"
|
||||
ssh_remote="git@git.batsense.net:mystiq/hydrogen-web.git"
|
||||
git remote add $SSH_REMOTE_NAME $ssh_remote
|
||||
}
|
||||
|
||||
clean() {
|
||||
if [ -f $SSH_ID_FILE ]
|
||||
then
|
||||
shred $SSH_ID_FILE
|
||||
rm $SSH_ID_FILE
|
||||
fi
|
||||
}
|
||||
|
||||
# $1: branch name
|
||||
# $2: directory containing build assets
|
||||
# $3: Author in <author-name author@example.com> format
|
||||
commit_files() {
|
||||
cd $PROJECT_ROOT
|
||||
original_branch=$(git branch --show-current)
|
||||
tmp_dir=$(mktemp -d)
|
||||
cp -r $2/* $tmp_dir
|
||||
|
||||
if [[ -z $(git ls-remote --heads origin ${1}) ]]
|
||||
then
|
||||
echo "[*] Creating deployment branch $1"
|
||||
git checkout --orphan $1
|
||||
else
|
||||
echo "[*] Deployment branch $1 exists, pulling changes from remote"
|
||||
git fetch origin $1
|
||||
git switch $1
|
||||
fi
|
||||
|
||||
git rm -rf .
|
||||
/bin/rm -rf *
|
||||
cp -r $tmp_dir/* .
|
||||
git add --all
|
||||
if [ $(git status --porcelain | xargs | sed '/^$/d' | wc -l) -gt 0 ];
|
||||
then
|
||||
echo "[*] Repository has changed, committing changes"
|
||||
git commit \
|
||||
--author="$3" \
|
||||
--message="new deploy: $(date --iso-8601=seconds)"
|
||||
fi
|
||||
git checkout $original_branch
|
||||
}
|
||||
|
||||
# $1: Pages API secret
|
||||
# $2: Deployment target branch
|
||||
deploy() {
|
||||
if (( "$#" < 2 ))
|
||||
then
|
||||
help
|
||||
else
|
||||
git -c core.sshCommand="/usr/bin/ssh -oStrictHostKeyChecking=no -i $SSH_ID_FILE"\
|
||||
push --force $SSH_REMOTE_NAME $2
|
||||
curl -vv --location --request \
|
||||
POST "https://deploy.batsense.net/api/v1/update"\
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw "{ \"secret\": \"$1\", \"branch\": \"$2\" }"
|
||||
fi
|
||||
}
|
||||
|
||||
if (( "$#" < 1 ))
|
||||
then
|
||||
help
|
||||
exit -1
|
||||
fi
|
||||
|
||||
|
||||
if match_arg $1 '-i' '--init'
|
||||
then
|
||||
if (( "$#" < 2 ))
|
||||
then
|
||||
help
|
||||
exit -1
|
||||
fi
|
||||
set_ssh_remote
|
||||
write_ssh "$2"
|
||||
elif match_arg $1 '-c' '--clean'
|
||||
then
|
||||
clean
|
||||
elif match_arg $1 '-cf' '--commit-files'
|
||||
then
|
||||
if (( "$#" < 4 ))
|
||||
then
|
||||
help
|
||||
exit -1
|
||||
fi
|
||||
commit_files $2 $3 $4
|
||||
elif match_arg $1 '-d' '--deploy'
|
||||
then
|
||||
if (( "$#" < 3 ))
|
||||
then
|
||||
help
|
||||
exit -1
|
||||
fi
|
||||
deploy $2 $3
|
||||
elif match_arg $1 '-h' '--help'
|
||||
then
|
||||
help
|
||||
else
|
||||
help
|
||||
fi
|
||||
|
||||
|
||||
|
|
@ -192,7 +192,7 @@ export class Platform {
|
|||
await this._themeLoader?.init(manifests, log);
|
||||
const { themeName, themeVariant } = await this._themeLoader.getActiveTheme();
|
||||
log.log({ l: "Active theme", name: themeName, variant: themeVariant });
|
||||
this._themeLoader.setTheme(themeName, themeVariant, log);
|
||||
await this._themeLoader.setTheme(themeName, themeVariant, log);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
|
@ -332,17 +332,34 @@ export class Platform {
|
|||
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);
|
||||
async replaceStylesheet(newPath, log) {
|
||||
const error = await this.logger.wrapOrRun(log, { l: "replaceStylesheet", location: newPath, }, async (l) => {
|
||||
let resolve, error;
|
||||
const promise = new Promise(r => resolve = r);
|
||||
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";
|
||||
styleTag.onerror = () => {
|
||||
error = new Error(`Failed to load stylesheet from ${newPath}`);
|
||||
l.catch(error);
|
||||
resolve();
|
||||
};
|
||||
styleTag.onload = () => {
|
||||
resolve();
|
||||
};
|
||||
head.appendChild(styleTag);
|
||||
await promise;
|
||||
return error;
|
||||
});
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
get description() {
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
"gatewayUrl": "https://matrix.org",
|
||||
"applicationServerKey": "BC-gpSdVHEXhvHSHS0AzzWrQoukv2BE7KzpoPO_FfPacqOo3l1pdqz7rSgmB04pZCWaHPz7XRe6fjLaC-WPDopM"
|
||||
},
|
||||
"defaultHomeServer": "matrix.test.mystiq.app",
|
||||
"bugReportEndpointUrl": "https://rageshake.test.mystiq.app/api/submit"
|
||||
"defaultHomeServer": "matrix.org",
|
||||
"bugReportEndpointUrl": "https://element.io/bugreports/submit"
|
||||
}
|
||||
|
|
|
@ -120,6 +120,7 @@ export function createFetchRequest(createTimeout, serviceWorkerHandler) {
|
|||
}
|
||||
} catch (err) {
|
||||
// some error pages return html instead of json, ignore error
|
||||
// detect these ignored errors from the response status
|
||||
if (!(err.name === "SyntaxError" && status >= 400)) {
|
||||
throw err;
|
||||
}
|
||||
|
|
|
@ -14,12 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type {ILogItem} from "../../../logging/types";
|
||||
import type {Platform} from "../Platform.js";
|
||||
import {RuntimeThemeParser} from "./parsers/RuntimeThemeParser";
|
||||
import type {Variant, ThemeInformation} from "./parsers/types";
|
||||
import {ColorSchemePreference} from "./parsers/types";
|
||||
import {BuiltThemeParser} from "./parsers/BuiltThemeParser";
|
||||
import type {Variant, ThemeInformation} from "./parsers/types";
|
||||
import type {ThemeManifest} from "../../types/theme";
|
||||
import type {ILogItem} from "../../../logging/types";
|
||||
import type {Platform} from "../Platform.js";
|
||||
import {LogLevel} from "../../../logging/LogFilter";
|
||||
|
||||
export class ThemeLoader {
|
||||
private _platform: Platform;
|
||||
|
@ -32,6 +34,9 @@ export class ThemeLoader {
|
|||
|
||||
async init(manifestLocations: string[], log?: ILogItem): Promise<void> {
|
||||
await this._platform.logger.wrapOrRun(log, "ThemeLoader.init", async (log) => {
|
||||
let noManifestsAvailable = true;
|
||||
const failedManifestLoads: string[] = [];
|
||||
const parseErrors: string[] = [];
|
||||
const results = await Promise.all(
|
||||
manifestLocations.map(location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response())
|
||||
);
|
||||
|
@ -39,14 +44,22 @@ export class ThemeLoader {
|
|||
const builtThemeParser = new BuiltThemeParser(this.preferredColorScheme);
|
||||
const runtimeThemePromises: Promise<void>[] = [];
|
||||
for (let i = 0; i < results.length; ++i) {
|
||||
const { body } = results[i];
|
||||
const result = results[i];
|
||||
const { status, body } = result;
|
||||
if (!(status >= 200 && status <= 299)) {
|
||||
console.error(`Failed to load manifest at ${manifestLocations[i]}, status: ${status}`);
|
||||
log.log({ l: "Manifest fetch failed", location: manifestLocations[i], status }, LogLevel.Error);
|
||||
failedManifestLoads.push(manifestLocations[i])
|
||||
continue;
|
||||
}
|
||||
noManifestsAvailable = false;
|
||||
try {
|
||||
if (body.extends) {
|
||||
const indexOfBaseManifest = results.findIndex(manifest => manifest.body.id === body.extends);
|
||||
const indexOfBaseManifest = results.findIndex(result => "value" in result && result.value.body.id === body.extends);
|
||||
if (indexOfBaseManifest === -1) {
|
||||
throw new Error(`Base manifest for derived theme at ${manifestLocations[i]} not found!`);
|
||||
}
|
||||
const {body: baseManifest} = results[indexOfBaseManifest];
|
||||
const { body: baseManifest } = (results[indexOfBaseManifest] as PromiseFulfilledResult<{ body: ThemeManifest }>).value;
|
||||
const baseManifestLocation = manifestLocations[indexOfBaseManifest];
|
||||
const promise = runtimeThemeParser.parse(body, baseManifest, baseManifestLocation, log);
|
||||
runtimeThemePromises.push(promise);
|
||||
|
@ -57,19 +70,27 @@ export class ThemeLoader {
|
|||
}
|
||||
catch(e) {
|
||||
console.error(e);
|
||||
parseErrors.push(e.message);
|
||||
}
|
||||
}
|
||||
await Promise.all(runtimeThemePromises);
|
||||
this._themeMapping = { ...builtThemeParser.themeMapping, ...runtimeThemeParser.themeMapping };
|
||||
Object.assign(this._themeMapping, builtThemeParser.themeMapping, runtimeThemeParser.themeMapping);
|
||||
if (noManifestsAvailable) {
|
||||
// We need at least one working theme manifest!
|
||||
throw new Error(`All configured theme manifests failed to load, the following were tried: ${failedManifestLoads.join(", ")}`);
|
||||
}
|
||||
else if (Object.keys(this._themeMapping).length === 0 && parseErrors.length) {
|
||||
// Something is wrong..., themeMapping is empty!
|
||||
throw new Error(`Failed to parse theme manifests, the following errors were encountered: ${parseErrors.join(", ")}`);
|
||||
}
|
||||
this._addDefaultThemeToMapping(log);
|
||||
log.log({ l: "Preferred colorscheme", scheme: this.preferredColorScheme === ColorSchemePreference.Dark ? "dark" : "light" });
|
||||
log.log({ l: "Result", themeMapping: this._themeMapping });
|
||||
});
|
||||
}
|
||||
|
||||
setTheme(themeName: string, themeVariant?: "light" | "dark" | "default", log?: ILogItem) {
|
||||
this._platform.logger.wrapOrRun(log, { l: "change theme", name: themeName, variant: themeVariant }, () => {
|
||||
async setTheme(themeName: string, themeVariant?: "light" | "dark" | "default", log?: ILogItem) {
|
||||
await this._platform.logger.wrapOrRun(log, { l: "change theme", name: themeName, variant: themeVariant }, async (l) => {
|
||||
let cssLocation: string, variables: Record<string, string>;
|
||||
let themeDetails = this._themeMapping[themeName];
|
||||
if ("id" in themeDetails) {
|
||||
|
@ -83,7 +104,7 @@ export class ThemeLoader {
|
|||
cssLocation = themeDetails[themeVariant].cssLocation;
|
||||
variables = themeDetails[themeVariant].variables;
|
||||
}
|
||||
this._platform.replaceStylesheet(cssLocation);
|
||||
await this._platform.replaceStylesheet(cssLocation, l);
|
||||
if (variables) {
|
||||
log?.log({l: "Derived Theme", variables});
|
||||
this._injectCSSVariables(variables);
|
||||
|
|
Reference in a new issue