Compare commits

..

7 commits

Author SHA1 Message Date
RMidhunSuresh
5d63069f31 Check status code instead of throwing error 2022-08-16 14:32:18 +05:30
RMidhunSuresh
08f9edaf68 Use Error LogLevel 2022-08-15 22:58:26 +05:30
RMidhunSuresh
6335da0932 Throw error from outside log method
This will show the error in the UI
2022-08-15 22:52:02 +05:30
RMidhunSuresh
7590c55404 Log error when loading css file fails 2022-08-15 22:28:40 +05:30
RMidhunSuresh
2e12ce74b7 Show parse errors in the UI as well 2022-08-15 17:23:27 +05:30
RMidhunSuresh
27363b3f63 Throw and log errors if manifests cannot be loaded 2022-08-10 22:25:56 +05:30
RMidhunSuresh
ff706e542d Throw ConnectionError instead of swallowing error 2022-08-10 22:23:51 +05:30
9 changed files with 63 additions and 224 deletions

1
.gitignore vendored
View file

@ -10,4 +10,3 @@ lib
*.tar.gz
.eslintcache
.tmp
tmp/

View file

@ -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 ]

View file

@ -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}'

View file

@ -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.

View file

@ -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

View file

@ -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() {

View file

@ -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"
}

View file

@ -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;
}

View file

@ -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);