riot-web/src/vector/platform/WebPlatform.ts

233 lines
8.4 KiB
TypeScript
Raw Normal View History

/*
2016-11-03 12:48:49 +01:00
Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd
2020-04-30 17:02:37 +02:00
Copyright 2017-2020 New Vector Ltd
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.
*/
2021-06-30 14:28:31 +02:00
import { UpdateCheckStatus } from "matrix-react-sdk/src/BasePlatform";
import request from 'browser-request';
import dis from 'matrix-react-sdk/src/dispatcher/dispatcher';
2021-06-30 14:28:31 +02:00
import { _t } from 'matrix-react-sdk/src/languageHandler';
import { hideToast as hideUpdateToast, showToast as showUpdateToast } from "matrix-react-sdk/src/toasts/UpdateToast";
import { Action } from "matrix-react-sdk/src/dispatcher/actions";
import { CheckUpdatesPayload } from 'matrix-react-sdk/src/dispatcher/payloads/CheckUpdatesPayload';
import UAParser from 'ua-parser-js';
import { logger } from "matrix-js-sdk/src/logger";
import VectorBasePlatform from './VectorBasePlatform';
import { parseQs } from "../url_utils";
const POKE_RATE_MS = 10 * 60 * 1000; // 10 min
export default class WebPlatform extends VectorBasePlatform {
2021-01-15 13:35:42 +01:00
constructor() {
super();
// Register service worker if available on this platform
2021-01-15 13:25:21 +01:00
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js');
}
}
2021-01-15 13:35:42 +01:00
getHumanReadableName(): string {
return 'Web Platform'; // no translation required: only used for analytics
}
/**
* Returns true if the platform supports displaying
* notifications, otherwise false.
*/
supportsNotifications(): boolean {
return Boolean(window.Notification);
}
/**
* Returns true if the application currently has permission
* to display notifications. Otherwise false.
*/
maySendNotifications(): boolean {
return window.Notification.permission === 'granted';
}
/**
* Requests permission to send notifications. Returns
* a promise that is resolved when the user has responded
* to the request. The promise has a single string argument
* that is 'granted' if the user allowed the request or
* 'denied' otherwise.
*/
requestNotificationPermission(): Promise<string> {
// annoyingly, the latest spec says this returns a
// promise, but this is only supported in Chrome 46
// and Firefox 47, so adapt the callback API.
return new Promise(function(resolve, reject) {
window.Notification.requestPermission((result) => {
resolve(result);
});
});
}
private getMostRecentVersion(): Promise<string> {
2016-11-17 18:27:21 +01:00
// We add a cachebuster to the request to make sure that we know about
// the most recent version on the origin server. That might not
// actually be the version we'd get on a reload (particularly in the
// presence of intermediate caching proxies), but still: we're trying
// to tell the user that there is a new version.
return new Promise((resolve, reject) => {
request(
{
method: "GET",
url: "version",
qs: { cachebuster: Date.now() },
},
(err, response, body) => {
if (err || response.status < 200 || response.status >= 300) {
if (err === null) err = { status: response.status };
reject(err);
return;
}
resolve(this.getNormalizedAppVersion(body.trim()));
},
);
});
}
getNormalizedAppVersion(version: string): string {
// if version looks like semver with leading v, strip it
// (matches scripts/normalize-version.sh)
const semVerRegex = new RegExp("^v[0-9]+.[0-9]+.[0-9]+(-.+)?$");
if (semVerRegex.test(version)) {
return version.substr(1);
2016-11-08 11:47:01 +01:00
}
return version;
}
getAppVersion(): Promise<string> {
return Promise.resolve(this.getNormalizedAppVersion(process.env.VERSION));
2016-11-08 11:47:01 +01:00
}
startUpdater() {
// Poll for an update immediately, and reload the page now if we're out of date
// already as we've just initialised an old version of the app somehow.
//
// Forcibly reloading the page aims to avoid users interacting at all with the old
// and potentially broken version of the app.
//
// Ideally, loading an old copy would be impossible with the
// cache-control: nocache HTTP header set, but Firefox doesn't always obey it :/
this.pollForUpdate((version: string, newVersion: string) => {
const query = parseQs(location);
if (query.updated === "1") {
// We just reloaded already and are still on the old version!
// Show the toast rather than reload in a loop.
showUpdateToast(version, newVersion);
return;
}
// Set updated=1 as a query param so we can detect that we've already done this once
// and reload the page.
let suffix = "updated=1";
if (window.location.search.length === 0) {
suffix = "?" + suffix;
} else {
suffix = "&" + suffix;
}
// This line has the effect of loading the page at the new location
window.location.href = window.location.href + suffix;
});
setInterval(() => this.pollForUpdate(showUpdateToast, hideUpdateToast), POKE_RATE_MS);
}
async canSelfUpdate(): Promise<boolean> {
return true;
}
pollForUpdate = (
showUpdate: (currentVersion: string, mostRecentVersion: string) => void,
showNoUpdate?: () => void,
) => {
return this.getMostRecentVersion().then((mostRecentVersion) => {
const currentVersion = this.getNormalizedAppVersion(process.env.VERSION);
if (currentVersion !== mostRecentVersion) {
if (this.shouldShowUpdate(mostRecentVersion)) {
showUpdate(currentVersion, mostRecentVersion);
}
return { status: UpdateCheckStatus.Ready };
} else {
showNoUpdate?.();
}
return { status: UpdateCheckStatus.NotAvailable };
}, (err) => {
logger.error("Failed to poll for update", err);
return {
status: UpdateCheckStatus.Error,
detail: err.message || err.status ? err.status.toString() : 'Unknown Error',
};
});
};
startUpdateCheck() {
super.startUpdateCheck();
this.pollForUpdate(showUpdateToast, hideUpdateToast).then((updateState) => {
dis.dispatch<CheckUpdatesPayload>({
action: Action.CheckUpdates,
...updateState,
});
});
}
installUpdate() {
window.location.reload();
}
getDefaultDeviceDisplayName(): string {
2020-07-15 10:38:55 +02:00
// strip query-string and fragment from uri
const url = new URL(window.location.href);
// `appName` in the format `develop.element.io/abc/xyz`
const appName = [
url.host,
url.pathname.replace(/\/$/, ""), // Remove trailing slash if present
].join("");
const ua = new UAParser();
const browserName = ua.getBrowser().name || "unknown browser";
let osName = ua.getOS().name || "unknown OS";
// Stylise the value from the parser to match Apple's current branding.
if (osName === "Mac OS") osName = "macOS";
return _t('%(appName)s (%(browserName)s, %(osName)s)', {
appName,
browserName,
osName,
});
}
screenCaptureErrorString(): string | null {
// it won't work at all if you're not on HTTPS so whine whine whine
if (window.location.protocol !== "https:") {
2017-05-31 15:51:08 +02:00
return _t("You need to be using HTTPS to place a screen-sharing call.");
}
return null;
}
reload() {
window.location.reload();
}
}