mirror of
https://github.com/vector-im/element-web.git
synced 2024-09-10 07:05:14 +00:00
Merge branch 'develop' into travis/room-list-2
This commit is contained in:
commit
6f7570d877
17 changed files with 410 additions and 342 deletions
78
CHANGELOG.md
78
CHANGELOG.md
|
@ -1,3 +1,81 @@
|
|||
Changes in [1.6.1](https://github.com/vector-im/riot-web/releases/tag/v1.6.1) (2020-05-19)
|
||||
==========================================================================================
|
||||
[Full Changelog](https://github.com/vector-im/riot-web/compare/v1.6.1-rc.1...v1.6.1)
|
||||
|
||||
* Upgrade to React SDK 2.6.0 and JS SDK 6.1.0
|
||||
|
||||
Changes in [1.6.1-rc.1](https://github.com/vector-im/riot-web/releases/tag/v1.6.1-rc.1) (2020-05-14)
|
||||
====================================================================================================
|
||||
[Full Changelog](https://github.com/vector-im/riot-web/compare/v1.6.0...v1.6.1-rc.1)
|
||||
|
||||
* Upgrade to React SDK 2.6.0-rc.1 and JS SDK 6.1.0-rc.1
|
||||
* Update from Weblate
|
||||
[\#13673](https://github.com/vector-im/riot-web/pull/13673)
|
||||
* Add notranslate class to matrixchat to prevent translation by Google
|
||||
Translate
|
||||
[\#13669](https://github.com/vector-im/riot-web/pull/13669)
|
||||
* Added Anchor Link to the development of matrix sdk
|
||||
[\#13638](https://github.com/vector-im/riot-web/pull/13638)
|
||||
* Prefetch the formatting button mask svg images
|
||||
[\#13631](https://github.com/vector-im/riot-web/pull/13631)
|
||||
* use a different image in previews
|
||||
[\#13488](https://github.com/vector-im/riot-web/pull/13488)
|
||||
* Update from Weblate
|
||||
[\#13625](https://github.com/vector-im/riot-web/pull/13625)
|
||||
* Remove electron_app as we now have riot-desktop repo
|
||||
[\#13544](https://github.com/vector-im/riot-web/pull/13544)
|
||||
* add new images for PWA icons
|
||||
[\#13556](https://github.com/vector-im/riot-web/pull/13556)
|
||||
* Remove unused feature flag from config
|
||||
[\#13504](https://github.com/vector-im/riot-web/pull/13504)
|
||||
* Update from Weblate
|
||||
[\#13486](https://github.com/vector-im/riot-web/pull/13486)
|
||||
* Developer tool: convert rageshake error locations back to sourcecode
|
||||
locations
|
||||
[\#13357](https://github.com/vector-im/riot-web/pull/13357)
|
||||
* App load tweaks, improve error pages
|
||||
[\#13329](https://github.com/vector-im/riot-web/pull/13329)
|
||||
* Tweak default device name to be more compact
|
||||
[\#13465](https://github.com/vector-im/riot-web/pull/13465)
|
||||
* Tweak default device name on macOS
|
||||
[\#13460](https://github.com/vector-im/riot-web/pull/13460)
|
||||
* Update docs with custom theming changes
|
||||
[\#13406](https://github.com/vector-im/riot-web/pull/13406)
|
||||
* Update from Weblate
|
||||
[\#13395](https://github.com/vector-im/riot-web/pull/13395)
|
||||
* Remove docs and config for invite only padlocks
|
||||
[\#13374](https://github.com/vector-im/riot-web/pull/13374)
|
||||
* Revert "Add font scaling labs setting."
|
||||
[\#13351](https://github.com/vector-im/riot-web/pull/13351)
|
||||
* Expand feature flag docs to cover additional release channels
|
||||
[\#13341](https://github.com/vector-im/riot-web/pull/13341)
|
||||
* Optimized image assets by recompressing without affecting quality.
|
||||
[\#13034](https://github.com/vector-im/riot-web/pull/13034)
|
||||
* Add font scaling labs setting.
|
||||
[\#13199](https://github.com/vector-im/riot-web/pull/13199)
|
||||
* Remove encrypted message search feature flag
|
||||
[\#13325](https://github.com/vector-im/riot-web/pull/13325)
|
||||
* Fix `default_federate` settting description
|
||||
[\#13312](https://github.com/vector-im/riot-web/pull/13312)
|
||||
* Clarify that the .well-known method for Jitsi isn't available yet
|
||||
[\#13314](https://github.com/vector-im/riot-web/pull/13314)
|
||||
* add config option to tsc resolveJsonModule
|
||||
[\#13296](https://github.com/vector-im/riot-web/pull/13296)
|
||||
* Fix dispatcher import to be extension agnostic
|
||||
[\#13297](https://github.com/vector-im/riot-web/pull/13297)
|
||||
* Document more config options in config.md (fixes #13089)
|
||||
[\#13260](https://github.com/vector-im/riot-web/pull/13260)
|
||||
* Fix tests post-js-sdk-filters change
|
||||
[\#13295](https://github.com/vector-im/riot-web/pull/13295)
|
||||
* Make Jitsi download script a JS script
|
||||
[\#13227](https://github.com/vector-im/riot-web/pull/13227)
|
||||
* Use matrix-react-sdk type extensions as a base
|
||||
[\#13271](https://github.com/vector-im/riot-web/pull/13271)
|
||||
* Allow Riot Web to randomly pick welcome backgrounds
|
||||
[\#13235](https://github.com/vector-im/riot-web/pull/13235)
|
||||
* Update cross-signing feature docs and document fallback procedures
|
||||
[\#13224](https://github.com/vector-im/riot-web/pull/13224)
|
||||
|
||||
Changes in [1.6.0](https://github.com/vector-im/riot-web/releases/tag/v1.6.0) (2020-05-05)
|
||||
==========================================================================================
|
||||
[Full Changelog](https://github.com/vector-im/riot-web/compare/v1.6.0-rc.6...v1.6.0)
|
||||
|
|
|
@ -26,10 +26,12 @@
|
|||
"defaultCountryCode": "GB",
|
||||
"showLabsSettings": false,
|
||||
"features": {
|
||||
"feature_font_scaling": "labs",
|
||||
"feature_pinning": "labs",
|
||||
"feature_custom_status": "labs",
|
||||
"feature_custom_tags": "labs",
|
||||
"feature_state_counters": "labs"
|
||||
"feature_state_counters": "labs",
|
||||
"feature_irc_ui": "labs"
|
||||
},
|
||||
"default_federate": true,
|
||||
"default_theme": "light",
|
||||
|
|
11
docs/labs.md
11
docs/labs.md
|
@ -7,6 +7,11 @@ to `Settings->Labs`. This list is non-exhaustive and subject to change, chat in
|
|||
**Be warned! Labs features are not finalised, they may be fragile, they may change, they may be
|
||||
dropped. Ask in the room if you are unclear about any details here.**
|
||||
|
||||
## Font scaling (`feature_font_scaling`)
|
||||
|
||||
Enables font scaling options for accessibility. To alter the scale check the
|
||||
appearance tab in settings.
|
||||
|
||||
## Message pinning (`feature_pinning`)
|
||||
|
||||
Allows you to pin messages in the room. To pin a message, use the 3 dots to the right of the message
|
||||
|
@ -112,3 +117,9 @@ For some sample themes, check out [aaronraimist/riot-web-themes](https://github.
|
|||
A complete rewrite of the room list is underway to increase performance, stability,
|
||||
and general usability. This is expected to take a while to complete and as such might
|
||||
be missing critical features - please use at your own risk, though do report bugs!
|
||||
|
||||
## IRC message layout (`feature_irc_ui`)
|
||||
|
||||
Adds an option to change the message layout to an IRC style ` timestamp | avatar
|
||||
| name | message `
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "riot-web",
|
||||
"productName": "Riot",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1",
|
||||
"description": "A feature-rich client for Matrix.org",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
|
@ -58,7 +58,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"browser-request": "^0.3.3",
|
||||
"favico.js": "^0.3.10",
|
||||
"gfm.css": "^1.1.2",
|
||||
"highlight.js": "^9.13.1",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
|
|
12
release.sh
12
release.sh
|
@ -45,6 +45,8 @@ do
|
|||
fi
|
||||
done
|
||||
|
||||
./node_modules/matrix-js-sdk/release.sh -u vector-im -z "$orig_args"
|
||||
|
||||
release="${1#v}"
|
||||
tag="v${release}"
|
||||
prerelease=0
|
||||
|
@ -54,16 +56,6 @@ prerelease=0
|
|||
# with a hyphen is a prerelease.
|
||||
echo $release | grep -q '-' && prerelease=1
|
||||
|
||||
# bump Electron's package.json first
|
||||
echo "electron yarn version"
|
||||
cd electron_app
|
||||
yarn version --no-git-tag-version --new-version "$release"
|
||||
git commit package.json -m "$tag"
|
||||
|
||||
cd ..
|
||||
|
||||
./node_modules/matrix-js-sdk/release.sh -u vector-im -z "$orig_args"
|
||||
|
||||
if [ $prerelease -eq 0 ]
|
||||
then
|
||||
# For a release, reset SDK deps back to the `develop` branch.
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"hosting_signup_link": "https://modular.im/?utm_source=riot-web&utm_medium=web",
|
||||
"bug_report_endpoint_url": "https://riot.im/bugreports/submit",
|
||||
"features": {
|
||||
"feature_font_scaling": "labs",
|
||||
"feature_pinning": "labs",
|
||||
"feature_custom_status": "labs",
|
||||
"feature_custom_tags": "labs",
|
||||
|
@ -23,7 +24,8 @@
|
|||
"feature_bridge_state": "labs",
|
||||
"feature_presence_in_room_list": "labs",
|
||||
"feature_custom_themes": "labs",
|
||||
"feature_new_room_list": "labs"
|
||||
"feature_new_room_list": "labs",
|
||||
"feature_irc_ui": "labs"
|
||||
},
|
||||
"piwik": {
|
||||
"url": "https://piwik.riot.im/",
|
||||
|
|
|
@ -1,170 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 -v <version> -d <config directory> [-n]"
|
||||
echo
|
||||
echo "version: commit-ish to check out and build"
|
||||
echo "config directory: a path to a directory containing"
|
||||
echo "config.json, a json config file to ship with the build"
|
||||
echo "and env.sh, a file to source environment variables"
|
||||
echo "from."
|
||||
echo "-n: build with no config file."
|
||||
echo
|
||||
echo "The update_base_url value from config.json is used to set up auto-update."
|
||||
echo
|
||||
echo "Environment variables:"
|
||||
echo " OSSLSIGNCODE_SIGNARGS: Arguments to pass to osslsigncode when signing"
|
||||
echo " NOTARIZE_APPLE_ID: Apple ID to use for notarisation. The password for"
|
||||
echo " this account must be set in NOTARIZE_CREDS in the keychain."
|
||||
}
|
||||
|
||||
confdir=
|
||||
version=
|
||||
skipcfg=0
|
||||
while getopts "d:v:n" opt; do
|
||||
case $opt in
|
||||
d)
|
||||
confdir=$OPTARG
|
||||
;;
|
||||
v)
|
||||
version=$OPTARG
|
||||
;;
|
||||
n)
|
||||
skipcfg=1
|
||||
;;
|
||||
\?)
|
||||
echo "Invalid option: -$OPTARG" >&2
|
||||
usage
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
echo "No version supplied"
|
||||
usage
|
||||
exit
|
||||
fi
|
||||
|
||||
conffile="$confdir/config.json"
|
||||
|
||||
if [ -z "$conffile" ] && [ "$skipcfg" = 0 ]; then
|
||||
echo "No config file given. Use -c to supply a config file or"
|
||||
echo "-n to build with no config file (and no auto update)."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ -n "$conffile" ]; then
|
||||
update_base_url=`jq -r .update_base_url $conffile`
|
||||
|
||||
if [ -z "$update_base_url" ]; then
|
||||
echo "No update URL supplied. Use update_base_url: null if you really"
|
||||
echo "want a build with no auto-update."
|
||||
usage
|
||||
exit
|
||||
fi
|
||||
# Make sure the base URL ends in a slash if it doesn't already
|
||||
update_base_url=`echo $update_base_url | sed -e 's#\([^\/]\)$#\1\/#'`
|
||||
fi
|
||||
|
||||
if [ ! -f package.json ]; then
|
||||
echo "No package.json found. This script must be run from"
|
||||
echo "the riot-web directory."
|
||||
exit
|
||||
fi
|
||||
|
||||
[ -f "$confdir/env.sh" ] && . "$confdir/env.sh"
|
||||
|
||||
if [ -z "$NOTARIZE_APPLE_ID" ]; then
|
||||
echo "NOTARIZE_APPLE_ID is not set"
|
||||
exit
|
||||
fi
|
||||
|
||||
osslsigncode -h 2> /dev/null
|
||||
if [ $? -ne 255 ]; then # osslsigncode exits with 255 after printing usage...
|
||||
echo "osslsigncode not found"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Test that altool can get its credentials for notarising the mac app
|
||||
xcrun altool -u "$NOTARIZE_APPLE_ID" -p '@keychain:NOTARIZE_CREDS' --list-apps || exit
|
||||
|
||||
# Get the token password: we'll need it later, but get it now so we fail early if it's not there
|
||||
token_password=`security find-generic-password -s riot_signing_token -w`
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "riot_signing_token not found in keychain"
|
||||
exit
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
echo "Building $version using Update base URL $update_base_url"
|
||||
|
||||
projdir=`pwd`
|
||||
builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'buildtmp'`
|
||||
pushd "$builddir"
|
||||
|
||||
git clone "$projdir" .
|
||||
git checkout "$version"
|
||||
|
||||
# Figure out what version we're building
|
||||
vername=`jq -r .version package.json`
|
||||
|
||||
if [ -n "$conffile" ]; then
|
||||
popd
|
||||
cp "$conffile" "$builddir/"
|
||||
pushd "$builddir"
|
||||
fi
|
||||
|
||||
# We use Git branch / commit dependencies for some packages, and Yarn seems
|
||||
# to have a hard time getting that right. See also
|
||||
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
|
||||
# global cache here to ensure we get the right thing.
|
||||
yarn cache clean
|
||||
yarn install
|
||||
yarn build:electron
|
||||
|
||||
popd
|
||||
|
||||
distdir="$builddir/electron_app/dist"
|
||||
pubdir="$projdir/electron_app/pub"
|
||||
rm -r "$pubdir" || true
|
||||
mkdir -p "$pubdir"
|
||||
rm -r "$projdir/electron_app/dist" || true
|
||||
mkdir -p "$projdir/electron_app/dist"
|
||||
|
||||
# Install packages: what the user downloads the first time,
|
||||
# (DMGs for mac, exe installer for windows)
|
||||
mkdir -p "$pubdir/install/macos"
|
||||
cp $distdir/*.dmg "$pubdir/install/macos/"
|
||||
|
||||
mkdir -p "$pubdir/install/win32/ia32/"
|
||||
cp $distdir/squirrel-windows-ia32/*.exe "$pubdir/install/win32/ia32/"
|
||||
|
||||
mkdir -p "$pubdir/install/win32/x64/"
|
||||
cp $distdir/squirrel-windows/*.exe "$pubdir/install/win32/x64/"
|
||||
|
||||
# Packages for auto-update
|
||||
mkdir -p "$pubdir/update/macos"
|
||||
cp $distdir/*-mac.zip "$pubdir/update/macos/"
|
||||
echo "$vername" > "$pubdir/update/macos/latest"
|
||||
|
||||
mkdir -p "$pubdir/update/win32/ia32/"
|
||||
cp $distdir/squirrel-windows-ia32/*.nupkg "$pubdir/update/win32/ia32/"
|
||||
cp $distdir/squirrel-windows-ia32/RELEASES "$pubdir/update/win32/ia32/"
|
||||
|
||||
mkdir -p "$pubdir/update/win32/x64/"
|
||||
cp $distdir/squirrel-windows/*.nupkg "$pubdir/update/win32/x64/"
|
||||
cp $distdir/squirrel-windows/RELEASES "$pubdir/update/win32/x64/"
|
||||
|
||||
# Move the deb to the main project dir's dist folder
|
||||
# (just the 64 bit one - the 32 bit one still gets built because
|
||||
# it's one arch argument for all platforms and we still want 32 bit
|
||||
# windows, but 32 bit linux is unsupported as of electron 4 and no
|
||||
# longer appears to work).
|
||||
cp $distdir/*_amd64.deb "$projdir/electron_app/dist/"
|
||||
|
||||
rm -rf "$builddir"
|
||||
|
||||
echo "$pubdir can now be hosted on your web server."
|
||||
echo "deb archives are in electron_app/dist/ - these should be added into your debian repository"
|
|
@ -1,30 +0,0 @@
|
|||
const { notarize } = require('electron-notarize');
|
||||
|
||||
exports.default = async function(context) {
|
||||
const { electronPlatformName, appOutDir } = context;
|
||||
|
||||
if (electronPlatformName === 'darwin') {
|
||||
const appName = context.packager.appInfo.productFilename;
|
||||
// We get the password from keychain. The keychain stores
|
||||
// user IDs too, but apparently altool can't get the user ID
|
||||
// from the keychain, so we need to get it from the environment.
|
||||
const userId = process.env.NOTARIZE_APPLE_ID;
|
||||
if (userId === undefined) {
|
||||
console.warn(
|
||||
"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" +
|
||||
"! Skipping macOS notarisation. !\n" +
|
||||
"! User ID not found, set NOTARIZE_APPLE_ID. !\n" +
|
||||
"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Notarising macOS app. This may be some time.");
|
||||
return await notarize({
|
||||
appBundleId: 'im.riot.app',
|
||||
appPath: `${appOutDir}/${appName}.app`,
|
||||
appleId: userId,
|
||||
appleIdPassword: '@keychain:NOTARIZE_CREDS',
|
||||
});
|
||||
}
|
||||
};
|
|
@ -1,77 +0,0 @@
|
|||
const { exec, execFile } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const shellescape = require('shell-escape');
|
||||
|
||||
exports.default = async function(options) {
|
||||
const inPath = options.path;
|
||||
const appOutDir = path.dirname(inPath);
|
||||
|
||||
// get the token passphrase from the keychain
|
||||
let tokenPassphrase;
|
||||
try {
|
||||
tokenPassphrase = await new Promise((resolve, reject) => {
|
||||
execFile(
|
||||
'security',
|
||||
['find-generic-password', '-s', 'riot_signing_token', '-w'],
|
||||
{},
|
||||
(err, stdout) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(stdout.trim());
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" +
|
||||
"! Skipping Windows signing. !\n" +
|
||||
"! Signing token not found in keychain. !\n" +
|
||||
"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let cmdLine = 'osslsigncode sign ';
|
||||
if (process.env.OSSLSIGNCODE_SIGNARGS) {
|
||||
cmdLine += process.env.OSSLSIGNCODE_SIGNARGS + ' ';
|
||||
}
|
||||
const tmpFile = path.join(
|
||||
appOutDir,
|
||||
'tmp_' + Math.random().toString(36).substring(2, 15) + '.exe',
|
||||
);
|
||||
const args = [
|
||||
'-h', options.hash,
|
||||
'-pass', tokenPassphrase,
|
||||
'-in', inPath,
|
||||
'-out', tmpFile,
|
||||
];
|
||||
if (options.isNest) args.push('-nest');
|
||||
cmdLine += shellescape(args);
|
||||
|
||||
let signStdout;
|
||||
const signproc = exec(cmdLine, {}, (error, stdout) => {
|
||||
signStdout = stdout;
|
||||
});
|
||||
signproc.on('exit', (code) => {
|
||||
if (code !== 0) {
|
||||
console.log("Running", cmdLine);
|
||||
console.log(signStdout);
|
||||
console.error("osslsigncode failed with code " + code);
|
||||
reject("osslsigncode failed with code " + code);
|
||||
return;
|
||||
}
|
||||
fs.rename(tmpFile, inPath, (err) => {
|
||||
if (err) {
|
||||
console.error("Error renaming file", err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
6
src/@types/global.d.ts
vendored
6
src/@types/global.d.ts
vendored
|
@ -24,5 +24,11 @@ declare global {
|
|||
|
||||
// electron-only
|
||||
ipcRenderer: any;
|
||||
|
||||
// opera-only
|
||||
opera: any;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/InstallTrigger
|
||||
InstallTrigger: any;
|
||||
}
|
||||
}
|
||||
|
|
255
src/favicon.ts
Normal file
255
src/favicon.ts
Normal file
|
@ -0,0 +1,255 @@
|
|||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
interface IParams {
|
||||
// colour parameters
|
||||
bgColor: string;
|
||||
textColor: string;
|
||||
// font styling parameters
|
||||
fontFamily: string;
|
||||
fontWeight: "normal" | "italic" | "bold" | "bolder" | "lighter" | number;
|
||||
|
||||
// positioning parameters
|
||||
isUp: boolean;
|
||||
isLeft: boolean;
|
||||
}
|
||||
|
||||
const defaults: IParams = {
|
||||
bgColor: "#d00",
|
||||
textColor: "#fff",
|
||||
fontFamily: "sans-serif", // Arial,Verdana,Times New Roman,serif,sans-serif,...
|
||||
fontWeight: "bold", // normal,italic,oblique,bold,bolder,lighter,100,200,300,400,500,600,700,800,900
|
||||
|
||||
isUp: false,
|
||||
isLeft: false,
|
||||
};
|
||||
|
||||
// Allows dynamic rendering of a circular badge atop the loaded favicon
|
||||
// supports colour, font and basic positioning parameters.
|
||||
// Based upon https://github.com/ejci/favico.js/blob/master/favico.js [MIT license]
|
||||
export default class Favicon {
|
||||
private readonly browser = {
|
||||
ff: typeof window.InstallTrigger !== "undefined",
|
||||
opera: !!window.opera || navigator.userAgent.includes("Opera"),
|
||||
};
|
||||
|
||||
private readonly params: IParams;
|
||||
private readonly canvas: HTMLCanvasElement;
|
||||
private readonly baseImage: HTMLImageElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
private icons: HTMLLinkElement[];
|
||||
|
||||
private isReady: boolean = false;
|
||||
// callback to run once isReady is asserted, allows for a badge to be queued for when it can be shown
|
||||
private readyCb = () => {};
|
||||
|
||||
constructor(params: Partial<IParams> = {}) {
|
||||
this.params = {...defaults, ...params};
|
||||
|
||||
this.icons = Favicon.getIcons();
|
||||
// create work canvas
|
||||
this.canvas = document.createElement("canvas");
|
||||
// create clone of favicon as a base
|
||||
this.baseImage = document.createElement("img");
|
||||
|
||||
const lastIcon = this.icons[this.icons.length - 1];
|
||||
if (lastIcon.hasAttribute("href")) {
|
||||
this.baseImage.setAttribute("crossOrigin", "anonymous");
|
||||
this.baseImage.onload = () => {
|
||||
// get height and width of the favicon
|
||||
this.canvas.height = (this.baseImage.height > 0) ? this.baseImage.height : 32;
|
||||
this.canvas.width = (this.baseImage.width > 0) ? this.baseImage.width : 32;
|
||||
this.context = this.canvas.getContext("2d");
|
||||
this.ready();
|
||||
};
|
||||
this.baseImage.setAttribute("src", lastIcon.getAttribute("href"));
|
||||
} else {
|
||||
this.canvas.height = this.baseImage.height = 32;
|
||||
this.canvas.width = this.baseImage.width = 32;
|
||||
this.context = this.canvas.getContext("2d");
|
||||
this.ready();
|
||||
}
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.context.drawImage(this.baseImage, 0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
|
||||
private options(n: number | string, params: IParams) {
|
||||
const opt = {
|
||||
n: ((typeof n) === "number") ? Math.abs(n as number | 0) : n,
|
||||
len: ("" + n).length,
|
||||
// badge positioning constants as percentages
|
||||
x: 0.4,
|
||||
y: 0.4,
|
||||
w: 0.6,
|
||||
h: 0.6,
|
||||
};
|
||||
|
||||
// apply positional transformations
|
||||
if (params.isUp) {
|
||||
if (opt.y < 0.6) {
|
||||
opt.y = opt.y - 0.4;
|
||||
} else {
|
||||
opt.y = opt.y - 2 * opt.y + (1 - opt.w);
|
||||
}
|
||||
}
|
||||
if (params.isLeft) {
|
||||
if (opt.x < 0.6) {
|
||||
opt.x = opt.x - 0.4;
|
||||
} else {
|
||||
opt.x = opt.x - 2 * opt.x + (1 - opt.h);
|
||||
}
|
||||
}
|
||||
|
||||
// scale the position to the canvas
|
||||
opt.x = this.canvas.width * opt.x;
|
||||
opt.y = this.canvas.height * opt.y;
|
||||
opt.w = this.canvas.width * opt.w;
|
||||
opt.h = this.canvas.height * opt.h;
|
||||
return opt;
|
||||
}
|
||||
|
||||
private circle(n: number | string, opts?: Partial<IParams>) {
|
||||
const params = {...this.params, ...opts};
|
||||
const opt = this.options(n, params);
|
||||
|
||||
let more = false;
|
||||
if (opt.len === 2) {
|
||||
opt.x = opt.x - opt.w * 0.4;
|
||||
opt.w = opt.w * 1.4;
|
||||
more = true;
|
||||
} else if (opt.len >= 3) {
|
||||
opt.x = opt.x - opt.w * 0.65;
|
||||
opt.w = opt.w * 1.65;
|
||||
more = true;
|
||||
}
|
||||
|
||||
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.context.drawImage(this.baseImage, 0, 0, this.canvas.width, this.canvas.height);
|
||||
this.context.beginPath();
|
||||
const fontSize = Math.floor(opt.h * (opt.n > 99 ? 0.85 : 1)) + "px";
|
||||
this.context.font = `${params.fontWeight} ${fontSize} ${params.fontFamily}`;
|
||||
this.context.textAlign = "center";
|
||||
|
||||
if (more) {
|
||||
this.context.moveTo(opt.x + opt.w / 2, opt.y);
|
||||
this.context.lineTo(opt.x + opt.w - opt.h / 2, opt.y);
|
||||
this.context.quadraticCurveTo(opt.x + opt.w, opt.y, opt.x + opt.w, opt.y + opt.h / 2);
|
||||
this.context.lineTo(opt.x + opt.w, opt.y + opt.h - opt.h / 2);
|
||||
this.context.quadraticCurveTo(opt.x + opt.w, opt.y + opt.h, opt.x + opt.w - opt.h / 2, opt.y + opt.h);
|
||||
this.context.lineTo(opt.x + opt.h / 2, opt.y + opt.h);
|
||||
this.context.quadraticCurveTo(opt.x, opt.y + opt.h, opt.x, opt.y + opt.h - opt.h / 2);
|
||||
this.context.lineTo(opt.x, opt.y + opt.h / 2);
|
||||
this.context.quadraticCurveTo(opt.x, opt.y, opt.x + opt.h / 2, opt.y);
|
||||
} else {
|
||||
this.context.arc(opt.x + opt.w / 2, opt.y + opt.h / 2, opt.h / 2, 0, 2 * Math.PI);
|
||||
}
|
||||
|
||||
this.context.fillStyle = params.bgColor;
|
||||
this.context.fill();
|
||||
this.context.closePath();
|
||||
this.context.beginPath();
|
||||
this.context.stroke();
|
||||
this.context.fillStyle = params.textColor;
|
||||
|
||||
if ((typeof opt.n) === "number" && opt.n > 999) {
|
||||
const count = ((opt.n > 9999) ? 9 : Math.floor(opt.n as number / 1000)) + "k+";
|
||||
this.context.fillText(count, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.2));
|
||||
} else {
|
||||
this.context.fillText("" + opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15));
|
||||
}
|
||||
|
||||
this.context.closePath();
|
||||
}
|
||||
|
||||
private ready() {
|
||||
if (this.isReady) return;
|
||||
this.isReady = true;
|
||||
this.readyCb();
|
||||
}
|
||||
|
||||
private setIcon(canvas) {
|
||||
setImmediate(() => {
|
||||
this.setIconSrc(canvas.toDataURL("image/png"));
|
||||
});
|
||||
}
|
||||
|
||||
private setIconSrc(url) {
|
||||
// if is attached to fav icon
|
||||
if (this.browser.ff || this.browser.opera) {
|
||||
// for FF we need to "recreate" element, attach to dom and remove old <link>
|
||||
const old = this.icons[this.icons.length - 1];
|
||||
const newIcon = window.document.createElement("link");
|
||||
this.icons = [newIcon];
|
||||
newIcon.setAttribute("rel", "icon");
|
||||
newIcon.setAttribute("type", "image/png");
|
||||
window.document.getElementsByTagName("head")[0].appendChild(newIcon);
|
||||
newIcon.setAttribute("href", url);
|
||||
if (old.parentNode) {
|
||||
old.parentNode.removeChild(old);
|
||||
}
|
||||
} else {
|
||||
this.icons.forEach(icon => {
|
||||
icon.setAttribute("href", url);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public badge(content: number | string, opts?: Partial<IParams>) {
|
||||
if (!this.isReady) {
|
||||
this.readyCb = () => {
|
||||
this.badge(content, opts);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof content === "string" || content > 0) {
|
||||
this.circle(content, opts);
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
this.setIcon(this.canvas);
|
||||
}
|
||||
|
||||
private static getLinks() {
|
||||
const icons: HTMLLinkElement[] = [];
|
||||
const links = window.document.getElementsByTagName("head")[0].getElementsByTagName("link");
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
if ((/(^|\s)icon(\s|$)/i).test(links[i].getAttribute("rel"))) {
|
||||
icons.push(links[i]);
|
||||
}
|
||||
}
|
||||
return icons;
|
||||
}
|
||||
|
||||
private static getIcons() {
|
||||
// get favicon link elements
|
||||
let elms = Favicon.getLinks();
|
||||
if (elms.length === 0) {
|
||||
elms = [window.document.createElement("link")];
|
||||
elms[0].setAttribute("rel", "icon");
|
||||
window.document.getElementsByTagName("head")[0].appendChild(elms[0]);
|
||||
}
|
||||
|
||||
elms.forEach(item => {
|
||||
item.setAttribute("type", "image/png");
|
||||
});
|
||||
return elms;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"Dismiss": "Hylkää",
|
||||
"Unknown device": "Tuntematon laite",
|
||||
"Welcome to Riot.im": "Tervetuloa Riot.im-palveluun",
|
||||
"Welcome to Riot.im": "Tervetuloa Riot.im-sovellukseen",
|
||||
"Custom Server Options": "Palvelinasetukset",
|
||||
"Riot Desktop on %(platformName)s": "Riot Desktop, %(platformName)s",
|
||||
"You need to be using HTTPS to place a screen-sharing call.": "Sinun täytyy käyttää HTTPS-yhteyttä, jotta voit jakaa ruudun puhelussa.",
|
||||
|
@ -22,5 +22,18 @@
|
|||
"Your Riot configuration contains invalid JSON. Please correct the problem and reload the page.": "Riot-asetuksissasi on virheellistä JSONia. Korjaa ongelma ja lataa sivu uudelleen.",
|
||||
"The message from the parser is: %(message)s": "Viesti jäsentimeltä: %(message)s",
|
||||
"Invalid JSON": "Virheellinen JSON",
|
||||
"Missing indexeddb worker script!": "Indexeddb-suorittajan skripti puuttuu!"
|
||||
"Missing indexeddb worker script!": "Indexeddb-suorittajan skripti puuttuu!",
|
||||
"Unable to load config file: please refresh the page to try again.": "Asetustiedostoa ei voi ladata. Yritä uudelleen lataamalla sivu uudelleen.",
|
||||
"Open user settings": "Avaa käyttäjäasetukset",
|
||||
"Previous/next recently visited room or community": "Edellinen/seuraava hiljattain vierailtu huone tai yhteisö",
|
||||
"Riot Desktop (%(platformName)s)": "Riot Desktop (%(platformName)s)",
|
||||
"Go to your browser to complete Sign In": "Tee kirjautuminen loppuun selaimessasi",
|
||||
"%(appName)s (%(browserName)s, %(osName)s)": "%(appName)s (%(browserName)s, %(osName)s)",
|
||||
"Unsupported browser": "Selainta ei tueta",
|
||||
"Your browser can't run Riot": "Riot ei toimi selaimellasi",
|
||||
"Riot uses advanced browser features which aren't supported by your current browser.": "Riot käyttää edistyneitä selaimen ominaisuuksia, joita nykyinen selaimesi ei tue.",
|
||||
"Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.": "Asenna <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink> tai <safariLink>Safari</safariLink>, jotta kaikki toimii parhaiten.",
|
||||
"You can continue using your current browser, but some or all features may not work and the look and feel of the application may be incorrect.": "Voit käyttää edelleen nykyistä selaintasi, mutta jotkut tai kaikki ominaisuudet eivät ehkä toimi ja sovelluksen ulkoasu voi olla virheellinen.",
|
||||
"I understand the risks and wish to continue": "Ymmärrän riskit ja haluan jatkaa",
|
||||
"Failed to start": "Käynnistys ei onnistunut"
|
||||
}
|
||||
|
|
|
@ -21,5 +21,20 @@
|
|||
"Your Riot is misconfigured": "Riot está mal configurado",
|
||||
"Unexpected error preparing the app. See console for details.": "Fallo non agardado ao preparar a app. Detalles na consola.",
|
||||
"Invalid configuration: can only specify one of default_server_config, default_server_name, or default_hs_url.": "Configuración non válida: só se pode indicar un de default_server_config, default_server_name, ou default_hs_url.",
|
||||
"Invalid configuration: no default server specified.": "Configuración non válida: non se indicou servidor por omisión."
|
||||
"Invalid configuration: no default server specified.": "Configuración non válida: non se indicou servidor por omisión.",
|
||||
"Missing indexeddb worker script!": "Falta o script indexeddb!",
|
||||
"Unable to load config file: please refresh the page to try again.": "Non se cargou o ficheiro de configuración: actualiza a páxina para reintentalo.",
|
||||
"Open user settings": "Abrir axustes do usuario",
|
||||
"Previous/next recently visited room or community": "Anterior/seguinte sala ou comunidade recentes",
|
||||
"Riot Desktop (%(platformName)s)": "Riot Desktop (%(platformName)s)",
|
||||
"Go to your browser to complete Sign In": "Abre o navegador para realizar a Conexión",
|
||||
"%(appName)s (%(browserName)s, %(osName)s)": "%(appName)s (%(browserName)s, %(osName)s)",
|
||||
"Unsupported browser": "Navegador non soportado",
|
||||
"Your browser can't run Riot": "O teu navegador non pode executar Riot",
|
||||
"Riot uses advanced browser features which aren't supported by your current browser.": "Riot utiliza funcións avanzadas dos navegadores que o teu actual non soporta.",
|
||||
"Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.": "Instala <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, ou <safariLink>Safari</safariLink> para ter unha mellor experiencia.",
|
||||
"You can continue using your current browser, but some or all features may not work and the look and feel of the application may be incorrect.": "Podes continuar co teu navegador, pero algunhas características poderían non funcionar e o aspecto da aplicación podería non ser o correcto.",
|
||||
"I understand the risks and wish to continue": "Entendo os riscos e desexo continuar",
|
||||
"Go to Riot.im": "Ir a Riot.im",
|
||||
"Failed to start": "Fallou o inicio"
|
||||
}
|
||||
|
|
|
@ -21,5 +21,16 @@
|
|||
"Your Riot configuration contains invalid JSON. Please correct the problem and reload the page.": "Jūsu Riot konfigurācijā ir nederīgs JSON. Lūdzu, izlabojiet problēmu un ielādējiet lapu atkārtoti.",
|
||||
"The message from the parser is: %(message)s": "Ziņojums no parsētāja ir: %(message)s",
|
||||
"Invalid JSON": "Nederīgs JSON",
|
||||
"Your Riot is misconfigured": "Jūsu Riot ir nepareizi konfigurēts"
|
||||
"Your Riot is misconfigured": "Jūsu Riot ir nepareizi konfigurēts",
|
||||
"Unable to load config file: please refresh the page to try again.": "Neizdevās ielādēt config datni: lūdzu pārlādē lapu lai mēģinātu vēlreiz.",
|
||||
"Open user settings": "Atver lietotāja iestatījumus",
|
||||
"Go to your browser to complete Sign In": "Aizej uz savu pārlūku lai pabeigtu Piekļuvi",
|
||||
"Unsupported browser": "Neatbalstīts pārlūks",
|
||||
"Your browser can't run Riot": "Tavs pārlūks nevar palaist Riot",
|
||||
"Riot uses advanced browser features which aren't supported by your current browser.": "",
|
||||
"Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.": "Lūdzu instalē <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, vai <safariLink>Safari</safariLink> priekš labākās pieredzes.",
|
||||
"You can continue using your current browser, but some or all features may not work and the look and feel of the application may be incorrect.": "Tu vari turpināt izmantot savu pašreizējo pārlūku, bet dažas vai visas funkcijas nestrādās, un lietotnes izskats var būt nepareizs.",
|
||||
"I understand the risks and wish to continue": "Es pieņemu riskus un vēlos turpināt",
|
||||
"Go to Riot.im": "Ej uz Riot.im",
|
||||
"Failed to start": "Neizdevās palaist"
|
||||
}
|
||||
|
|
|
@ -35,5 +35,6 @@
|
|||
"You can continue using your current browser, but some or all features may not work and the look and feel of the application may be incorrect.": "Mund të vazhdoni të përdorni shfletuesin tuaj të tanishëm, por disa ose krejt veçoritë mund të mos funksionojnë dhe pamja dhe ndjesitë prej aplikacionit të mos jenë të sakta.",
|
||||
"I understand the risks and wish to continue": "I kuptoj rreziqet dhe dëshiroj të vazhdoj",
|
||||
"Go to Riot.im": "Shko te Riot.im",
|
||||
"Failed to start": "S’u arrit të nisej"
|
||||
"Failed to start": "S’u arrit të nisej",
|
||||
"Missing indexeddb worker script!": "Mungon programth worker-i indexeddb-je!"
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ import { _t } from 'matrix-react-sdk/src/languageHandler';
|
|||
import dis from 'matrix-react-sdk/src/dispatcher/dispatcher';
|
||||
import {getVectorConfig} from "../getconfig";
|
||||
|
||||
import Favico from 'favico.js';
|
||||
import Favico from '../../favicon';
|
||||
|
||||
export const updateCheckStatusEnum = {
|
||||
CHECKING: 'CHECKING',
|
||||
|
@ -85,29 +85,7 @@ export default class VectorBasePlatform extends BasePlatform {
|
|||
bgColor = "#f00";
|
||||
}
|
||||
|
||||
const doUpdate = () => {
|
||||
this.favicon.badge(notif, {
|
||||
bgColor: bgColor,
|
||||
});
|
||||
};
|
||||
|
||||
doUpdate();
|
||||
|
||||
// HACK: Workaround for Chrome 78+ and dependency incompatibility.
|
||||
// The library we use doesn't appear to work in Chrome 78, likely due to their
|
||||
// changes surrounding tab behaviour. Tabs went through a bit of a redesign and
|
||||
// restructuring in Chrome 78, so it's not terribly surprising that the library
|
||||
// doesn't work correctly. The library we use hasn't been updated in years and
|
||||
// does not look easy to fix/fork ourselves - we might as well write our own that
|
||||
// doesn't include animation/webcam/etc support. However, that's a bit difficult
|
||||
// so for now we'll just trigger the update twice.
|
||||
//
|
||||
// Note that trying to reproduce the problem in isolation doesn't seem to work:
|
||||
// see https://gist.github.com/turt2live/5ab87919918adbfd7cfb8f1ad10f2409 for
|
||||
// an example (you'll need your own web server to host that).
|
||||
if (window.chrome) {
|
||||
doUpdate();
|
||||
}
|
||||
this.favicon.badge(notif, { bgColor });
|
||||
} catch (e) {
|
||||
console.warn(`Failed to set badge count: ${e.message}`);
|
||||
}
|
||||
|
|
26
yarn.lock
26
yarn.lock
|
@ -4284,11 +4284,6 @@ fast-levenshtein@~2.0.6:
|
|||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
|
||||
|
||||
favico.js@^0.3.10:
|
||||
version "0.3.10"
|
||||
resolved "https://registry.yarnpkg.com/favico.js/-/favico.js-0.3.10.tgz#80586e27a117f24a8d51c18a99bdc714d4339301"
|
||||
integrity sha1-gFhuJ6EX8kqNUcGKmb3HFNQzkwE=
|
||||
|
||||
faye-websocket@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4"
|
||||
|
@ -6942,8 +6937,8 @@ mathml-tag-names@^2.1.1:
|
|||
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
||||
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
||||
version "6.0.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f120533fadb309ac5dc8b2bcb6882e784ba93f18"
|
||||
version "6.1.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e3c6a0e1a08a3812ba988e60eb5a2a013bb27404"
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.8.3"
|
||||
another-json "^0.2.0"
|
||||
|
@ -6964,8 +6959,8 @@ matrix-mock-request@^1.2.3:
|
|||
expect "^1.20.2"
|
||||
|
||||
"matrix-react-sdk@github:matrix-org/matrix-react-sdk#develop":
|
||||
version "2.5.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-react-sdk/tar.gz/aa0c6f1f71a6122579c74f3cba3a7925f35131ac"
|
||||
version "2.6.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-react-sdk/tar.gz/833eccf5cd901d04ddfcc06a7f95e2e2ebe6f5d5"
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.8.3"
|
||||
blueimp-canvas-to-blob "^3.5.0"
|
||||
|
@ -7000,7 +6995,6 @@ matrix-mock-request@^1.2.3:
|
|||
project-name-generator "^2.1.7"
|
||||
prop-types "^15.5.8"
|
||||
qrcode "^1.4.4"
|
||||
qrcode-react "^0.1.16"
|
||||
qs "^6.6.0"
|
||||
react "^16.9.0"
|
||||
react-beautiful-dnd "^4.0.1"
|
||||
|
@ -9196,18 +9190,6 @@ q@^1.1.2:
|
|||
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
||||
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
|
||||
|
||||
qr.js@0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f"
|
||||
integrity sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8=
|
||||
|
||||
qrcode-react@^0.1.16:
|
||||
version "0.1.16"
|
||||
resolved "https://registry.yarnpkg.com/qrcode-react/-/qrcode-react-0.1.16.tgz#d064999d510ffc3e55a9ca3ffcf6c203c69f1517"
|
||||
integrity sha512-FK+QCfFqCQMSxUE1byzglERJQkwKqXYvYMCS+/Ad2zACJOfoHkHHtRqsQQPji7lfb1y1qCXLvL+3eP1hAfg8Ng==
|
||||
dependencies:
|
||||
qr.js "0.0.0"
|
||||
|
||||
qrcode@^1.4.4:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.4.4.tgz#f0c43568a7e7510a55efc3b88d9602f71963ea83"
|
||||
|
|
Loading…
Reference in a new issue