Merge branch 'develop' into matthew/slate
This commit is contained in:
commit
02947063d3
89 changed files with 2884 additions and 1622 deletions
11
.eslintrc.js
11
.eslintrc.js
|
@ -41,7 +41,8 @@ module.exports = {
|
||||||
"react/jsx-uses-react": "error",
|
"react/jsx-uses-react": "error",
|
||||||
|
|
||||||
// bind or arrow function in props causes performance issues
|
// bind or arrow function in props causes performance issues
|
||||||
"react/jsx-no-bind": ["error", {
|
// (but we currently use them in some places)
|
||||||
|
"react/jsx-no-bind": ["warn", {
|
||||||
"ignoreRefs": true,
|
"ignoreRefs": true,
|
||||||
}],
|
}],
|
||||||
"react/jsx-key": ["error"],
|
"react/jsx-key": ["error"],
|
||||||
|
@ -50,7 +51,12 @@ module.exports = {
|
||||||
// <Element prop={ consideredError} prop={notConsideredError} />
|
// <Element prop={ consideredError} prop={notConsideredError} />
|
||||||
//
|
//
|
||||||
// https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/docs/rules/jsx-curly-spacing.md
|
// https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/docs/rules/jsx-curly-spacing.md
|
||||||
"react/jsx-curly-spacing": ["error", {"when": "never", "children": {"when": "always"}}],
|
//
|
||||||
|
// Disabled for now - if anything we'd like to *enforce* spacing in JSX
|
||||||
|
// curly brackets for legibility, but in practice it's not clear that the
|
||||||
|
// consistency particularly improves legibility here. --Matthew
|
||||||
|
//
|
||||||
|
// "react/jsx-curly-spacing": ["error", {"when": "never", "children": {"when": "always"}}],
|
||||||
|
|
||||||
// Assert spacing before self-closing JSX tags, and no spacing before or
|
// Assert spacing before self-closing JSX tags, and no spacing before or
|
||||||
// after the closing slash, and no spacing after the opening bracket of
|
// after the closing slash, and no spacing after the opening bracket of
|
||||||
|
@ -88,7 +94,6 @@ module.exports = {
|
||||||
"valid-jsdoc": ["warn"],
|
"valid-jsdoc": ["warn"],
|
||||||
"new-cap": ["warn"],
|
"new-cap": ["warn"],
|
||||||
"key-spacing": ["warn"],
|
"key-spacing": ["warn"],
|
||||||
"arrow-parens": ["warn"],
|
|
||||||
"prefer-const": ["warn"],
|
"prefer-const": ["warn"],
|
||||||
|
|
||||||
// crashes currently: https://github.com/eslint/eslint/issues/6274
|
// crashes currently: https://github.com/eslint/eslint/issues/6274
|
||||||
|
|
|
@ -9,16 +9,9 @@ set -ev
|
||||||
RIOT_WEB_DIR=riot-web
|
RIOT_WEB_DIR=riot-web
|
||||||
REACT_SDK_DIR=`pwd`
|
REACT_SDK_DIR=`pwd`
|
||||||
|
|
||||||
curbranch="${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH}"
|
scripts/fetchdep.sh vector-im riot-web
|
||||||
echo "Determined branch to be $curbranch"
|
|
||||||
|
|
||||||
git clone https://github.com/vector-im/riot-web.git \
|
|
||||||
"$RIOT_WEB_DIR"
|
|
||||||
|
|
||||||
cd "$RIOT_WEB_DIR"
|
cd "$RIOT_WEB_DIR"
|
||||||
|
|
||||||
git checkout "$curbranch" || git checkout develop
|
|
||||||
|
|
||||||
mkdir node_modules
|
mkdir node_modules
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,5 @@ addons:
|
||||||
chrome: stable
|
chrome: stable
|
||||||
install:
|
install:
|
||||||
- npm install
|
- npm install
|
||||||
- (cd node_modules/matrix-js-sdk && npm install)
|
|
||||||
script:
|
script:
|
||||||
./scripts/travis.sh
|
./scripts/travis.sh
|
||||||
|
|
|
@ -11,8 +11,10 @@ set -x
|
||||||
# install the other dependencies
|
# install the other dependencies
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# we may be using a dev branch of js-sdk in which case we need to build it
|
scripts/fetchdep.sh matrix-org matrix-js-sdk
|
||||||
(cd node_modules/matrix-js-sdk && npm install)
|
rm -r node_modules/matrix-js-sdk || true
|
||||||
|
ln -s ../matrix-js-sdk node_modules/matrix-js-sdk
|
||||||
|
(cd matrix-js-sdk && npm install)
|
||||||
|
|
||||||
# run the mocha tests
|
# run the mocha tests
|
||||||
npm run test -- --no-colors
|
npm run test -- --no-colors
|
||||||
|
|
423
package-lock.json
generated
423
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.10.7",
|
"version": "0.12.2",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -268,7 +268,7 @@
|
||||||
"glob": {
|
"glob": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||||
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
|
"integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"fs.realpath": "1.0.0",
|
"fs.realpath": "1.0.0",
|
||||||
|
@ -1203,10 +1203,7 @@
|
||||||
"boom": {
|
"boom": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz",
|
||||||
"integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=",
|
"integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE="
|
||||||
"requires": {
|
|
||||||
"hoek": "4.2.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
|
@ -1238,6 +1235,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz",
|
||||||
"integrity": "sha1-ns5bWsqJopkyJC4Yv5M975h2zBc="
|
"integrity": "sha1-ns5bWsqJopkyJC4Yv5M975h2zBc="
|
||||||
},
|
},
|
||||||
|
"browser-stdout": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"browserify-aes": {
|
"browserify-aes": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-0.4.0.tgz",
|
||||||
|
@ -1254,6 +1257,14 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"pako": "0.2.9"
|
"pako": "0.2.9"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pako": {
|
||||||
|
"version": "0.2.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
||||||
|
"integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"buffer": {
|
"buffer": {
|
||||||
|
@ -1449,9 +1460,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"commonmark": {
|
"commonmark": {
|
||||||
"version": "0.27.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.28.1.tgz",
|
||||||
"integrity": "sha1-2GwmK5YoIelIPGnFR7xYhAwEezQ=",
|
"integrity": "sha1-Buq41SM4uDn6Gi11rwCF7tGxvq4=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"entities": "1.1.1",
|
"entities": "1.1.1",
|
||||||
"mdurl": "1.0.1",
|
"mdurl": "1.0.1",
|
||||||
|
@ -1580,10 +1591,7 @@
|
||||||
"boom": {
|
"boom": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz",
|
||||||
"integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==",
|
"integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw=="
|
||||||
"requires": {
|
|
||||||
"hoek": "4.2.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1705,12 +1713,6 @@
|
||||||
"integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=",
|
"integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"diff": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz",
|
|
||||||
"integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"doctrine": {
|
"doctrine": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz",
|
||||||
|
@ -2139,7 +2141,7 @@
|
||||||
"glob": {
|
"glob": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||||
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
|
"integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"fs.realpath": "1.0.0",
|
"fs.realpath": "1.0.0",
|
||||||
|
@ -2183,15 +2185,26 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eslint-plugin-react": {
|
"eslint-plugin-react": {
|
||||||
"version": "7.4.0",
|
"version": "7.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.7.0.tgz",
|
||||||
"integrity": "sha512-tvjU9u3VqmW2vVuYnE8Qptq+6ji4JltjOjJ9u7VAOxVYkUkyBZWRvNYKbDv5fN+L6wiA+4we9+qQahZ0m63XEA==",
|
"integrity": "sha512-KC7Snr4YsWZD5flu6A5c0AcIZidzW3Exbqp7OT67OaD2AppJtlBr/GuPrW/vaQM/yfZotEvKAdrxrO+v8vwYJA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"doctrine": "2.0.0",
|
"doctrine": "2.1.0",
|
||||||
"has": "1.0.1",
|
"has": "1.0.1",
|
||||||
"jsx-ast-utils": "2.0.1",
|
"jsx-ast-utils": "2.0.1",
|
||||||
"prop-types": "15.6.0"
|
"prop-types": "15.6.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"doctrine": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"esutils": "2.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"espree": {
|
"espree": {
|
||||||
|
@ -2238,7 +2251,7 @@
|
||||||
"estree-walker": {
|
"estree-walker": {
|
||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.5.0.tgz",
|
||||||
"integrity": "sha512-/bEAy+yKAZQrEWUhGmS3H9XpGqSDBtRzX0I2PgMw9kA2n1jN22uV5B5p7MFdZdvWdXCRJztXAfx6ZeRfgkEETg==",
|
"integrity": "sha1-quO1fELeuAEONJyJJGLw5xxd0ao=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"esutils": {
|
"esutils": {
|
||||||
|
@ -2554,6 +2567,22 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"focus-trap": {
|
||||||
|
"version": "2.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-2.4.3.tgz",
|
||||||
|
"integrity": "sha512-sT5Ip9nyAIxWq8Apt1Fdv6yTci5GotaOtO5Ro1/+F3PizttNBcCYz8j/Qze54PPFK73KUbOqh++HUCiyNPqvhA==",
|
||||||
|
"requires": {
|
||||||
|
"tabbable": "1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"focus-trap-react": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-MoQmONoy9gRPyrC5DGezkcOMGgx7MtIOAQDHe098UtL2sA2vmucJwEmQisb+8LRXNYFHxuw5zJ1oLFeKu4Mteg==",
|
||||||
|
"requires": {
|
||||||
|
"focus-trap": "2.4.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"for-in": {
|
"for-in": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
||||||
|
@ -2629,7 +2658,7 @@
|
||||||
"fsevents": {
|
"fsevents": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz",
|
||||||
"integrity": "sha512-Sn44E5wQW4bTHXvQmvSHwqbuiXtduD6Rrjm2ZtUEGbyrig+nUH3t/QD4M4/ZXViY556TBpRgZkHLDx3JxPwxiw==",
|
"integrity": "sha1-MoK3E/s62A7eDp/PRhG1qm/AM/Q=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
@ -3562,6 +3591,11 @@
|
||||||
"assert-plus": "1.0.0"
|
"assert-plus": "1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"gfm.css": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/gfm.css/-/gfm.css-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-KhK3rqxMj+UTLRxWnfUA5n8XZYMWfHrrcCxtWResYR2B3hWIqBM6v9FPGZSlVuX+ScLewizOvNkjYXuPs95ThQ=="
|
||||||
|
},
|
||||||
"glob": {
|
"glob": {
|
||||||
"version": "5.0.15",
|
"version": "5.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
|
||||||
|
@ -3596,7 +3630,7 @@
|
||||||
"globals": {
|
"globals": {
|
||||||
"version": "9.18.0",
|
"version": "9.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
|
||||||
"integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==",
|
"integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"globby": {
|
"globby": {
|
||||||
|
@ -3616,7 +3650,7 @@
|
||||||
"glob": {
|
"glob": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||||
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
|
"integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"fs.realpath": "1.0.0",
|
"fs.realpath": "1.0.0",
|
||||||
|
@ -3635,12 +3669,6 @@
|
||||||
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
|
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"growl": {
|
|
||||||
"version": "1.9.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz",
|
|
||||||
"integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"har-schema": {
|
"har-schema": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
||||||
|
@ -3709,19 +3737,24 @@
|
||||||
"requires": {
|
"requires": {
|
||||||
"boom": "4.3.1",
|
"boom": "4.3.1",
|
||||||
"cryptiles": "3.1.2",
|
"cryptiles": "3.1.2",
|
||||||
"hoek": "4.2.0",
|
|
||||||
"sntp": "2.0.2"
|
"sntp": "2.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"highlight.js": {
|
"he": {
|
||||||
"version": "8.9.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-8.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
|
||||||
"integrity": "sha1-uKnFSTISqTkvAiK2SclhFJfr+4g="
|
"integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"hoek": {
|
"highlight.js": {
|
||||||
"version": "4.2.0",
|
"version": "9.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz",
|
||||||
"integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ=="
|
"integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4="
|
||||||
|
},
|
||||||
|
"hoist-non-react-statics": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-6Bl6XsDT1ntE0lHbIhr4Kp2PGcleGZ66qu5Jqk8lc0Xc/IeG6gVLmwUGs/K0Us+L8VWoKgj0uWdPMataOsm31w=="
|
||||||
},
|
},
|
||||||
"home-or-tmp": {
|
"home-or-tmp": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
@ -3862,7 +3895,6 @@
|
||||||
"version": "2.2.2",
|
"version": "2.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz",
|
||||||
"integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=",
|
"integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "1.3.1"
|
"loose-envify": "1.3.1"
|
||||||
}
|
}
|
||||||
|
@ -4137,30 +4169,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||||
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
|
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
|
||||||
},
|
},
|
||||||
"jade": {
|
|
||||||
"version": "0.26.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz",
|
|
||||||
"integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"commander": "0.6.1",
|
|
||||||
"mkdirp": "0.3.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"commander": {
|
|
||||||
"version": "0.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz",
|
|
||||||
"integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"mkdirp": {
|
|
||||||
"version": "0.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz",
|
|
||||||
"integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=",
|
|
||||||
"dev": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"jquery": {
|
"jquery": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.2.1.tgz",
|
||||||
|
@ -4303,7 +4311,7 @@
|
||||||
"glob": {
|
"glob": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||||
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
|
"integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"fs.realpath": "1.0.0",
|
"fs.realpath": "1.0.0",
|
||||||
|
@ -4472,6 +4480,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
|
||||||
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
|
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
|
||||||
},
|
},
|
||||||
|
"lodash-es": {
|
||||||
|
"version": "4.17.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.10.tgz",
|
||||||
|
"integrity": "sha512-iesFYPmxYYGTcmQK0sL8bX3TGHyM6b2qREaB4kamHfQyfPJP0xgoGxp19nsH16nsfquLdiyKyX3mQkfiSGV8Rg=="
|
||||||
|
},
|
||||||
"lodash.assign": {
|
"lodash.assign": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
|
||||||
|
@ -4547,17 +4560,28 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"matrix-js-sdk": {
|
"matrix-js-sdk": {
|
||||||
"version": "0.8.5",
|
"version": "0.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-0.8.5.tgz",
|
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-0.10.1.tgz",
|
||||||
"integrity": "sha1-1ZAVTx53ADVyZw+p28rH5APnbk8=",
|
"integrity": "sha512-BLo+Okn2o///TyWBKtjFXvhlD32vGfr10eTE51hHx/jwaXO82VyGMzMi+IDPS4SDYUbvXI7PpamECeh9TXnV2w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"another-json": "0.2.0",
|
"another-json": "0.2.0",
|
||||||
|
"babel-runtime": "6.26.0",
|
||||||
"bluebird": "3.5.1",
|
"bluebird": "3.5.1",
|
||||||
"browser-request": "0.3.3",
|
"browser-request": "0.3.3",
|
||||||
"content-type": "1.0.4",
|
"content-type": "1.0.4",
|
||||||
"request": "2.83.0"
|
"request": "2.83.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"matrix-mock-request": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/matrix-mock-request/-/matrix-mock-request-1.2.1.tgz",
|
||||||
|
"integrity": "sha1-2aWrqNPYJG6I/3YyWYuZwUE/QjI=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"bluebird": "3.5.1",
|
||||||
|
"expect": "1.20.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"matrix-react-test-utils": {
|
"matrix-react-test-utils": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/matrix-react-test-utils/-/matrix-react-test-utils-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/matrix-react-test-utils/-/matrix-react-test-utils-0.1.1.tgz",
|
||||||
|
@ -4579,6 +4603,11 @@
|
||||||
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
|
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"memoize-one": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-YqVh744GsMlZu6xkhGslPSqSurOv6P+kLN2J3ysBZfagLcL5FdRK/0UpgLoL8hwjjEvvAVkjJZyFP+1T6p1vgA=="
|
||||||
|
},
|
||||||
"memory-fs": {
|
"memory-fs": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
|
||||||
|
@ -4632,7 +4661,7 @@
|
||||||
"minimatch": {
|
"minimatch": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
"integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "1.1.8"
|
"brace-expansion": "1.1.8"
|
||||||
}
|
}
|
||||||
|
@ -4660,75 +4689,73 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mocha": {
|
"mocha": {
|
||||||
"version": "2.5.3",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-2.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/mocha/-/mocha-5.1.1.tgz",
|
||||||
"integrity": "sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg=",
|
"integrity": "sha512-kKKs/H1KrMMQIEsWNxGmb4/BGsmj0dkeyotEvbrAuQ01FcWRLssUNXCEUZk6SZtyJBi6EE7SL0zDDtItw1rGhw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"commander": "2.3.0",
|
"browser-stdout": "1.3.1",
|
||||||
"debug": "2.2.0",
|
"commander": "2.11.0",
|
||||||
"diff": "1.4.0",
|
"debug": "3.1.0",
|
||||||
"escape-string-regexp": "1.0.2",
|
"diff": "3.5.0",
|
||||||
"glob": "3.2.11",
|
"escape-string-regexp": "1.0.5",
|
||||||
"growl": "1.9.2",
|
"glob": "7.1.2",
|
||||||
"jade": "0.26.3",
|
"growl": "1.10.3",
|
||||||
|
"he": "1.1.1",
|
||||||
|
"minimatch": "3.0.4",
|
||||||
"mkdirp": "0.5.1",
|
"mkdirp": "0.5.1",
|
||||||
"supports-color": "1.2.0",
|
"supports-color": "4.4.0"
|
||||||
"to-iso-string": "0.0.2"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz",
|
|
||||||
"integrity": "sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "2.2.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||||
"integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
|
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ms": "0.7.1"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"escape-string-regexp": {
|
"diff": {
|
||||||
"version": "1.0.2",
|
"version": "3.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
|
||||||
"integrity": "sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE=",
|
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"glob": {
|
"glob": {
|
||||||
"version": "3.2.11",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||||
"integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=",
|
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
"fs.realpath": "1.0.0",
|
||||||
|
"inflight": "1.0.6",
|
||||||
"inherits": "2.0.3",
|
"inherits": "2.0.3",
|
||||||
"minimatch": "0.3.0"
|
"minimatch": "3.0.4",
|
||||||
|
"once": "1.4.0",
|
||||||
|
"path-is-absolute": "1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimatch": {
|
"growl": {
|
||||||
"version": "0.3.0",
|
"version": "1.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz",
|
||||||
"integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=",
|
"integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"requires": {
|
|
||||||
"lru-cache": "2.2.4",
|
|
||||||
"sigmund": "1.0.1"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"ms": {
|
"has-flag": {
|
||||||
"version": "0.7.1",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
|
||||||
"integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=",
|
"integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"supports-color": {
|
"supports-color": {
|
||||||
"version": "1.2.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
|
||||||
"integrity": "sha1-/x7R5hFp0Gs88tWI4YixjYhH4X4=",
|
"integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"has-flag": "2.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -4979,10 +5006,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pako": {
|
"pako": {
|
||||||
"version": "0.2.9",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz",
|
||||||
"integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=",
|
"integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"parallelshell": {
|
"parallelshell": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
|
@ -5205,10 +5231,23 @@
|
||||||
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
|
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"raf": {
|
||||||
|
"version": "3.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz",
|
||||||
|
"integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==",
|
||||||
|
"requires": {
|
||||||
|
"performance-now": "2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"raf-schd": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-ngcBQygUeE3kHlOaBSqgWKv7BT9kx5kQ6fAwFJRNRT7TD54M+hx1kpNHb8sONRskcYQedJg2RC2xKlAHRUQBig=="
|
||||||
|
},
|
||||||
"randomatic": {
|
"randomatic": {
|
||||||
"version": "1.1.7",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz",
|
||||||
"integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==",
|
"integrity": "sha1-x6vpzIuHwLqodrGf3oP9RkeX44w=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-number": "3.0.0",
|
"is-number": "3.0.0",
|
||||||
|
@ -5287,6 +5326,23 @@
|
||||||
"integrity": "sha1-wStu/cIkfBDae4dw0YUICnsEcVY=",
|
"integrity": "sha1-wStu/cIkfBDae4dw0YUICnsEcVY=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"react-beautiful-dnd": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-d73RMu4QOFCyjUELLWFyY/EuclnfqulI9pECx+2gIuJvV0ycf1uR88o+1x0RSB9ILD70inHMzCBKNkWVbbt+vA==",
|
||||||
|
"requires": {
|
||||||
|
"babel-runtime": "6.26.0",
|
||||||
|
"invariant": "2.2.2",
|
||||||
|
"memoize-one": "3.1.1",
|
||||||
|
"prop-types": "15.6.0",
|
||||||
|
"raf-schd": "2.1.1",
|
||||||
|
"react-motion": "0.5.2",
|
||||||
|
"react-redux": "5.0.7",
|
||||||
|
"redux": "3.7.2",
|
||||||
|
"redux-thunk": "2.2.0",
|
||||||
|
"reselect": "3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-dom": {
|
"react-dom": {
|
||||||
"version": "15.6.2",
|
"version": "15.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz",
|
||||||
|
@ -5304,6 +5360,43 @@
|
||||||
"gemini-scrollbar": "github:matrix-org/gemini-scrollbar#b302279810d05319ac5ff1bd34910bff32325c7b"
|
"gemini-scrollbar": "github:matrix-org/gemini-scrollbar#b302279810d05319ac5ff1bd34910bff32325c7b"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-motion": {
|
||||||
|
"version": "0.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-motion/-/react-motion-0.5.2.tgz",
|
||||||
|
"integrity": "sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ==",
|
||||||
|
"requires": {
|
||||||
|
"performance-now": "0.2.0",
|
||||||
|
"prop-types": "15.6.0",
|
||||||
|
"raf": "3.4.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"performance-now": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz",
|
||||||
|
"integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"version": "5.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.7.tgz",
|
||||||
|
"integrity": "sha512-5VI8EV5hdgNgyjfmWzBbdrqUkrVRKlyTKk1sGH3jzM2M2Mhj/seQgPXaz6gVAj2lz/nz688AdTqMO18Lr24Zhg==",
|
||||||
|
"requires": {
|
||||||
|
"hoist-non-react-statics": "2.5.0",
|
||||||
|
"invariant": "2.2.2",
|
||||||
|
"lodash": "4.17.10",
|
||||||
|
"lodash-es": "4.17.10",
|
||||||
|
"loose-envify": "1.3.1",
|
||||||
|
"prop-types": "15.6.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": {
|
||||||
|
"version": "4.17.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz",
|
||||||
|
"integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"readable-stream": {
|
"readable-stream": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
|
||||||
|
@ -5350,6 +5443,22 @@
|
||||||
"resolve": "1.4.0"
|
"resolve": "1.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"redux": {
|
||||||
|
"version": "3.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
|
||||||
|
"integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
|
||||||
|
"requires": {
|
||||||
|
"lodash": "4.17.4",
|
||||||
|
"lodash-es": "4.17.10",
|
||||||
|
"loose-envify": "1.3.1",
|
||||||
|
"symbol-observable": "1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"redux-thunk": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.2.0.tgz",
|
||||||
|
"integrity": "sha1-5hWhbha0ehmlFXZhM9Hj6Zt4UuU="
|
||||||
|
},
|
||||||
"regenerate": {
|
"regenerate": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz",
|
||||||
|
@ -5498,6 +5607,11 @@
|
||||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"reselect": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz",
|
||||||
|
"integrity": "sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc="
|
||||||
|
},
|
||||||
"resolve": {
|
"resolve": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz",
|
||||||
|
@ -5544,7 +5658,7 @@
|
||||||
"glob": {
|
"glob": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||||
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
|
"integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"fs.realpath": "1.0.0",
|
"fs.realpath": "1.0.0",
|
||||||
|
@ -5642,7 +5756,7 @@
|
||||||
"glob": {
|
"glob": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||||
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
|
"integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"fs.realpath": "1.0.0",
|
"fs.realpath": "1.0.0",
|
||||||
|
@ -5655,12 +5769,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sigmund": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
|
|
||||||
"integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"sinon": {
|
"sinon": {
|
||||||
"version": "1.17.7",
|
"version": "1.17.7",
|
||||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz",
|
"resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz",
|
||||||
|
@ -5688,10 +5796,7 @@
|
||||||
"sntp": {
|
"sntp": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz",
|
||||||
"integrity": "sha1-UGQRDwr4X3z9t9a2ekACjOUrSys=",
|
"integrity": "sha1-UGQRDwr4X3z9t9a2ekACjOUrSys="
|
||||||
"requires": {
|
|
||||||
"hoek": "4.2.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"socket.io": {
|
"socket.io": {
|
||||||
"version": "1.7.3",
|
"version": "1.7.3",
|
||||||
|
@ -5848,24 +5953,30 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"source-map-loader": {
|
"source-map-loader": {
|
||||||
"version": "0.1.6",
|
"version": "0.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-0.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-0.2.3.tgz",
|
||||||
"integrity": "sha1-wJkD2m1zueU7ftjuUkVZcFHpjpE=",
|
"integrity": "sha512-MYbFX9DYxmTQFfy2v8FC1XZwpwHKYxg3SK8Wb7VPBKuhDjz8gi9re2819MsG4p49HDyiOSUKlmZ+nQBArW5CGw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"async": "0.9.2",
|
"async": "2.6.0",
|
||||||
"loader-utils": "0.2.17",
|
"loader-utils": "0.2.17",
|
||||||
"source-map": "0.1.43"
|
"source-map": "0.6.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"source-map": {
|
"async": {
|
||||||
"version": "0.1.43",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz",
|
||||||
"integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=",
|
"integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"amdefine": "1.0.1"
|
"lodash": "4.17.4"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -5917,7 +6028,7 @@
|
||||||
"stream-http": {
|
"stream-http": {
|
||||||
"version": "2.7.2",
|
"version": "2.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz",
|
||||||
"integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==",
|
"integrity": "sha1-QKBQ7I3DtTsz2ZCUFcAsC/Gr+60=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"builtin-status-codes": "3.0.0",
|
"builtin-status-codes": "3.0.0",
|
||||||
|
@ -5983,6 +6094,16 @@
|
||||||
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
|
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"symbol-observable": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
|
||||||
|
},
|
||||||
|
"tabbable": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-77oqsKEPrxIwgRcXUwipkj9W5ItO97L6eUT1Ar7vh+El16Zm4M6V+YU1cbipHEa6q0Yjw8O3Hoh8oRgatV5s7A=="
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"version": "3.8.3",
|
"version": "3.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz",
|
||||||
|
@ -6111,12 +6232,6 @@
|
||||||
"integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=",
|
"integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"to-iso-string": {
|
|
||||||
"version": "0.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/to-iso-string/-/to-iso-string-0.0.2.tgz",
|
|
||||||
"integrity": "sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"tough-cookie": {
|
"tough-cookie": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz",
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
"start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",
|
"start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",
|
||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
"lintall": "eslint src/ test/",
|
"lintall": "eslint src/ test/",
|
||||||
"lintwithexclusions": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test",
|
"lintwithexclusions": "eslint --max-warnings 20 --ignore-path .eslintignore.errorfiles src test",
|
||||||
"clean": "rimraf lib",
|
"clean": "rimraf lib",
|
||||||
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt",
|
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt",
|
||||||
"test": "karma start --single-run=true --browsers ChromeHeadless",
|
"test": "karma start --single-run=true --browsers ChromeHeadless",
|
||||||
|
@ -71,6 +71,7 @@
|
||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"linkifyjs": "^2.1.3",
|
"linkifyjs": "^2.1.3",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
|
"lolex": "^2.3.2",
|
||||||
"matrix-js-sdk": "0.10.1",
|
"matrix-js-sdk": "0.10.1",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"pako": "^1.0.5",
|
"pako": "^1.0.5",
|
||||||
|
@ -111,7 +112,7 @@
|
||||||
"eslint-config-google": "^0.7.1",
|
"eslint-config-google": "^0.7.1",
|
||||||
"eslint-plugin-babel": "^4.0.1",
|
"eslint-plugin-babel": "^4.0.1",
|
||||||
"eslint-plugin-flowtype": "^2.30.0",
|
"eslint-plugin-flowtype": "^2.30.0",
|
||||||
"eslint-plugin-react": "^7.4.0",
|
"eslint-plugin-react": "^7.7.0",
|
||||||
"estree-walker": "^0.5.0",
|
"estree-walker": "^0.5.0",
|
||||||
"expect": "^1.16.0",
|
"expect": "^1.16.0",
|
||||||
"flow-parser": "^0.57.3",
|
"flow-parser": "^0.57.3",
|
||||||
|
@ -126,8 +127,9 @@
|
||||||
"karma-spec-reporter": "^0.0.31",
|
"karma-spec-reporter": "^0.0.31",
|
||||||
"karma-summary-reporter": "^1.3.3",
|
"karma-summary-reporter": "^1.3.3",
|
||||||
"karma-webpack": "^1.7.0",
|
"karma-webpack": "^1.7.0",
|
||||||
|
"matrix-mock-request": "^1.2.1",
|
||||||
"matrix-react-test-utils": "^0.1.1",
|
"matrix-react-test-utils": "^0.1.1",
|
||||||
"mocha": "^2.4.5",
|
"mocha": "^5.0.5",
|
||||||
"parallelshell": "^3.0.2",
|
"parallelshell": "^3.0.2",
|
||||||
"react-addons-test-utils": "^15.4.0",
|
"react-addons-test-utils": "^15.4.0",
|
||||||
"require-json": "0.0.1",
|
"require-json": "0.0.1",
|
||||||
|
|
|
@ -250,6 +250,7 @@ textarea {
|
||||||
.mx_Dialog button.danger, .mx_Dialog input[type="submit"].danger {
|
.mx_Dialog button.danger, .mx_Dialog input[type="submit"].danger {
|
||||||
background-color: $warning-color;
|
background-color: $warning-color;
|
||||||
border: solid 1px $warning-color;
|
border: solid 1px $warning-color;
|
||||||
|
color: $accent-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Dialog button:disabled, .mx_Dialog input[type="submit"]:disabled {
|
.mx_Dialog button:disabled, .mx_Dialog input[type="submit"]:disabled {
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
@import "./views/elements/_InlineSpinner.scss";
|
@import "./views/elements/_InlineSpinner.scss";
|
||||||
@import "./views/elements/_MemberEventListSummary.scss";
|
@import "./views/elements/_MemberEventListSummary.scss";
|
||||||
@import "./views/elements/_ProgressBar.scss";
|
@import "./views/elements/_ProgressBar.scss";
|
||||||
@import "./views/elements/_Quote.scss";
|
@import "./views/elements/_ReplyThread.scss";
|
||||||
@import "./views/elements/_RichText.scss";
|
@import "./views/elements/_RichText.scss";
|
||||||
@import "./views/elements/_RoleButton.scss";
|
@import "./views/elements/_RoleButton.scss";
|
||||||
@import "./views/elements/_Spinner.scss";
|
@import "./views/elements/_Spinner.scss";
|
||||||
|
@ -89,7 +89,7 @@
|
||||||
@import "./views/rooms/_PinnedEventTile.scss";
|
@import "./views/rooms/_PinnedEventTile.scss";
|
||||||
@import "./views/rooms/_PinnedEventsPanel.scss";
|
@import "./views/rooms/_PinnedEventsPanel.scss";
|
||||||
@import "./views/rooms/_PresenceLabel.scss";
|
@import "./views/rooms/_PresenceLabel.scss";
|
||||||
@import "./views/rooms/_QuotePreview.scss";
|
@import "./views/rooms/_ReplyPreview.scss";
|
||||||
@import "./views/rooms/_RoomDropTarget.scss";
|
@import "./views/rooms/_RoomDropTarget.scss";
|
||||||
@import "./views/rooms/_RoomHeader.scss";
|
@import "./views/rooms/_RoomHeader.scss";
|
||||||
@import "./views/rooms/_RoomList.scss";
|
@import "./views/rooms/_RoomList.scss";
|
||||||
|
|
|
@ -50,13 +50,13 @@ limitations under the License.
|
||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_FilePanel .mx_EventTile .mx_MImageBody_download {
|
.mx_FilePanel .mx_EventTile .mx_MFileBody_download {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: $event-timestamp-color;
|
color: $event-timestamp-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_FilePanel .mx_EventTile .mx_MImageBody_downloadLink {
|
.mx_FilePanel .mx_EventTile .mx_MFileBody_downloadLink {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
color: $light-fg-color;
|
color: $light-fg-color;
|
||||||
}
|
}
|
||||||
|
|
|
@ -251,3 +251,7 @@ input.mx_UserSettings_phoneNumberField {
|
||||||
color: $accent-color;
|
color: $accent-color;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_UserSettings_analyticsModal table {
|
||||||
|
margin: 10px 0px;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2018 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,13 +14,19 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_Quote .mx_DateSeparator {
|
.mx_ReplyThread .mx_DateSeparator {
|
||||||
font-size: 1em !important;
|
font-size: 1em !important;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
padding-bottom: 1px;
|
padding-bottom: 1px;
|
||||||
bottom: -5px;
|
bottom: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Quote_show {
|
.mx_ReplyThread_show {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blockquote.mx_ReplyThread {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 10px;
|
||||||
|
border-left: 4px solid $blockquote-bar-color;
|
||||||
|
}
|
|
@ -15,6 +15,10 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_MImageBody {
|
.mx_MImageBody {
|
||||||
display: block;
|
display: block;
|
||||||
margin-right: 34px;
|
margin-right: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MImageBody_thumbnail {
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
|
@ -84,7 +84,7 @@ limitations under the License.
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line, .mx_EventTile_reply {
|
||||||
position: relative;
|
position: relative;
|
||||||
/* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
|
/* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
|
||||||
margin-right: 110px;
|
margin-right: 110px;
|
||||||
|
@ -96,7 +96,7 @@ limitations under the License.
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_quote {
|
.mx_EventTile_reply {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ limitations under the License.
|
||||||
background-color: $event-selected-color;
|
background-color: $event-selected-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile:hover .mx_EventTile_line:not(.mx_EventTile_quote),
|
.mx_EventTile:hover .mx_EventTile_line,
|
||||||
.mx_EventTile.menu .mx_EventTile_line
|
.mx_EventTile.menu .mx_EventTile_line
|
||||||
{
|
{
|
||||||
background-color: $event-selected-color;
|
background-color: $event-selected-color;
|
||||||
|
@ -157,7 +157,8 @@ limitations under the License.
|
||||||
color: $event-notsent-color;
|
color: $event-notsent-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_redacted .mx_EventTile_line .mx_UnknownBody {
|
.mx_EventTile_redacted .mx_EventTile_line .mx_UnknownBody,
|
||||||
|
.mx_EventTile_redacted .mx_EventTile_reply .mx_UnknownBody {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
|
@ -202,10 +203,10 @@ limitations under the License.
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_last .mx_MessageTimestamp,
|
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
||||||
.mx_EventTile:hover .mx_MessageTimestamp,
|
.mx_EventTile_last > div > a > .mx_MessageTimestamp,
|
||||||
.mx_EventTile.menu .mx_MessageTimestamp
|
.mx_EventTile:hover > div > a > .mx_MessageTimestamp,
|
||||||
{
|
.mx_EventTile.menu > div > a > .mx_MessageTimestamp {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,12 +236,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile:hover .mx_EventTile_editButton,
|
.mx_EventTile:hover .mx_EventTile_editButton,
|
||||||
.mx_EventTile.menu .mx_EventTile_editButton
|
.mx_EventTile.menu .mx_EventTile_editButton {
|
||||||
{
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile.menu .mx_MessageTimestamp {
|
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -298,6 +294,16 @@ limitations under the License.
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_e2eIcon_hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* always override hidden attribute for blocked and warning */
|
||||||
|
.mx_EventTile_e2eIcon_hidden[src="img/e2e-blocked.svg"],
|
||||||
|
.mx_EventTile_e2eIcon_hidden[src="img/e2e-warning.svg"] {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_EventTile_keyRequestInfo {
|
.mx_EventTile_keyRequestInfo {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
@ -348,8 +354,9 @@ limitations under the License.
|
||||||
border-left: $e2e-unverified-color 5px solid;
|
border-left: $e2e-unverified-color 5px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_MessageTimestamp,
|
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_MessageTimestamp {
|
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
|
||||||
|
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp {
|
||||||
left: 3px;
|
left: 3px;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
@ -360,8 +367,9 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_e2eIcon,
|
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_e2eIcon {
|
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon,
|
||||||
|
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon {
|
||||||
display: block;
|
display: block;
|
||||||
left: 41px;
|
left: 41px;
|
||||||
}
|
}
|
||||||
|
@ -456,7 +464,7 @@ limitations under the License.
|
||||||
// same as the padding for non-compact .mx_EventTile.mx_EventTile_info
|
// same as the padding for non-compact .mx_EventTile.mx_EventTile_info
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line, .mx_EventTile_reply {
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
.mx_EventTile_avatar {
|
.mx_EventTile_avatar {
|
||||||
|
@ -474,7 +482,7 @@ limitations under the License.
|
||||||
.mx_EventTile_avatar {
|
.mx_EventTile_avatar {
|
||||||
top: 2px;
|
top: 2px;
|
||||||
}
|
}
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line, .mx_EventTile_reply {
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
padding-bottom: 1px;
|
padding-bottom: 1px;
|
||||||
}
|
}
|
||||||
|
@ -482,13 +490,13 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_EventTile.mx_EventTile_emote.mx_EventTile_continuation {
|
.mx_EventTile.mx_EventTile_emote.mx_EventTile_continuation {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line, .mx_EventTile_reply {
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line, .mx_EventTile_reply {
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
.mx_QuotePreview {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid $primary-hairline-color;
|
|
||||||
background: $primary-bg-color;
|
|
||||||
border-bottom: none;
|
|
||||||
border-radius: 4px 4px 0 0;
|
|
||||||
max-height: 50vh;
|
|
||||||
overflow: auto
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_QuotePreview_section {
|
|
||||||
border-bottom: 1px solid $primary-hairline-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_QuotePreview_header {
|
|
||||||
margin: 12px;
|
|
||||||
color: $primary-fg-color;
|
|
||||||
font-weight: 400;
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_QuotePreview_title {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_QuotePreview_cancel {
|
|
||||||
float: right;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_QuotePreview_clear {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
52
res/css/views/rooms/_ReplyPreview.scss
Normal file
52
res/css/views/rooms/_ReplyPreview.scss
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 Vector Creations 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_ReplyPreview {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid $primary-hairline-color;
|
||||||
|
background: $primary-bg-color;
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow: auto
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ReplyPreview_section {
|
||||||
|
border-bottom: 1px solid $primary-hairline-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ReplyPreview_header {
|
||||||
|
margin: 12px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ReplyPreview_title {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ReplyPreview_cancel {
|
||||||
|
float: right;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ReplyPreview_clear {
|
||||||
|
clear: both;
|
||||||
|
}
|
12
res/img/button-new-window.svg
Normal file
12
res/img/button-new-window.svg
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="612px" height="612px" viewBox="0 90 612 612" enable-background="new 0 90 612 612" xml:space="preserve">
|
||||||
|
<path fill="#76CFA6" d="M612,149.5v135.983c0,22.802-27.582,33.979-43.531,18.032l-37.939-37.941L271.787,524.317
|
||||||
|
c-9.959,9.958-26.104,9.958-36.062,0l-24.041-24.042c-9.959-9.958-9.959-26.104,0-36.062l258.746-258.746l-37.935-37.937
|
||||||
|
C416.48,151.519,427.822,124,450.525,124H586.5C600.583,124,612,135.417,612,149.5z M432.469,411.719l-17,17
|
||||||
|
c-4.782,4.782-7.469,11.269-7.469,18.031V600H68V260h280.5c6.763,0,13.248-2.687,18.03-7.468l17-17
|
||||||
|
C399.595,219.467,388.218,192,365.5,192H51c-28.167,0-51,22.833-51,51v374c0,28.167,22.833,51,51,51h374c28.167,0,51-22.833,51-51
|
||||||
|
V429.749C476,407.031,448.532,395.654,432.469,411.719z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
22
scripts/fetchdep.sh
Executable file
22
scripts/fetchdep.sh
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
org="$1"
|
||||||
|
repo="$2"
|
||||||
|
|
||||||
|
rm -r "$repo" || true
|
||||||
|
|
||||||
|
curbranch="$TRAVIS_PULL_REQUEST_BRANCH"
|
||||||
|
[ -z "$curbranch" ] && curbranch="$TRAVIS_BRANCH"
|
||||||
|
[ -z "$curbranch" ] && curbranch=`"echo $GIT_BRANCH" | sed -e 's/^origin\///'` # jenkins
|
||||||
|
|
||||||
|
if [ -n "$curbranch" ]
|
||||||
|
then
|
||||||
|
echo "Determined branch to be $curbranch"
|
||||||
|
|
||||||
|
git clone https://github.com/$org/$repo.git $repo --branch "$curbranch" && exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking out develop branch"
|
||||||
|
git clone https://github.com/$org/$repo.git $repo --branch develop
|
|
@ -2,6 +2,14 @@
|
||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
|
scripts/fetchdep.sh matrix-org matrix-js-sdk
|
||||||
|
rm -r node_modules/matrix-js-sdk || true
|
||||||
|
ln -s ../matrix-js-sdk node_modules/matrix-js-sdk
|
||||||
|
|
||||||
|
cd matrix-js-sdk
|
||||||
|
npm install
|
||||||
|
cd ..
|
||||||
|
|
||||||
npm run test
|
npm run test
|
||||||
./.travis-test-riot.sh
|
./.travis-test-riot.sh
|
||||||
|
|
||||||
|
|
|
@ -16,17 +16,33 @@
|
||||||
|
|
||||||
import { getCurrentLanguage, _t, _td } from './languageHandler';
|
import { getCurrentLanguage, _t, _td } from './languageHandler';
|
||||||
import PlatformPeg from './PlatformPeg';
|
import PlatformPeg from './PlatformPeg';
|
||||||
import SdkConfig, { DEFAULTS } from './SdkConfig';
|
import SdkConfig from './SdkConfig';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import sdk from './index';
|
import sdk from './index';
|
||||||
|
|
||||||
function getRedactedHash() {
|
const hashRegex = /#\/(groups?|room|user|settings|register|login|forgot_password|home|directory)/;
|
||||||
return window.location.hash.replace(/#\/(group|room|user)\/(.+)/, "#/$1/<redacted>");
|
const hashVarRegex = /#\/(group|room|user)\/.*$/;
|
||||||
|
|
||||||
|
// Remove all but the first item in the hash path. Redact unexpected hashes.
|
||||||
|
function getRedactedHash(hash) {
|
||||||
|
// Don't leak URLs we aren't expecting - they could contain tokens/PII
|
||||||
|
const match = hashRegex.exec(hash);
|
||||||
|
if (!match) {
|
||||||
|
console.warn(`Unexpected hash location "${hash}"`);
|
||||||
|
return '#/<unexpected hash location>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashVarRegex.test(hash)) {
|
||||||
|
return hash.replace(hashVarRegex, "#/$1/<redacted>");
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash.replace(hashRegex, "#/$1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return the current origin and hash separated with a `/`. This does not include query parameters.
|
||||||
function getRedactedUrl() {
|
function getRedactedUrl() {
|
||||||
// hardcoded url to make piwik happy
|
const { origin, pathname, hash } = window.location;
|
||||||
return 'https://riot.im/app/' + getRedactedHash();
|
return origin + pathname + getRedactedHash(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
const customVariables = {
|
const customVariables = {
|
||||||
|
@ -148,9 +164,6 @@ class Analytics {
|
||||||
}
|
}
|
||||||
|
|
||||||
trackPageChange(generationTimeMs) {
|
trackPageChange(generationTimeMs) {
|
||||||
if (typeof generationTimeMs !== 'number') {
|
|
||||||
throw new Error('Analytics.trackPageChange: expected generationTimeMs to be a number');
|
|
||||||
}
|
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
if (this.firstPage) {
|
if (this.firstPage) {
|
||||||
// De-duplicate first page
|
// De-duplicate first page
|
||||||
|
@ -158,8 +171,15 @@ class Analytics {
|
||||||
this.firstPage = false;
|
this.firstPage = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof generationTimeMs === 'number') {
|
||||||
|
this._paq.push(['setGenerationTimeMs', generationTimeMs]);
|
||||||
|
} else {
|
||||||
|
console.warn('Analytics.trackPageChange: expected generationTimeMs to be a number');
|
||||||
|
// But continue anyway because we still want to track the change
|
||||||
|
}
|
||||||
|
|
||||||
this._paq.push(['setCustomUrl', getRedactedUrl()]);
|
this._paq.push(['setCustomUrl', getRedactedUrl()]);
|
||||||
this._paq.push(['setGenerationTimeMs', generationTimeMs]);
|
|
||||||
this._paq.push(['trackPageView']);
|
this._paq.push(['trackPageView']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,6 +194,7 @@ class Analytics {
|
||||||
}
|
}
|
||||||
|
|
||||||
_setVisitVariable(key, value) {
|
_setVisitVariable(key, value) {
|
||||||
|
if (this.disabled) return;
|
||||||
this._paq.push(['setCustomVariable', customVariables[key].id, key, value, 'visit']);
|
this._paq.push(['setCustomVariable', customVariables[key].id, key, value, 'visit']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,8 +202,10 @@ class Analytics {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
|
|
||||||
const config = SdkConfig.get();
|
const config = SdkConfig.get();
|
||||||
const whitelistedHSUrls = config.piwik.whitelistedHSUrls || DEFAULTS.piwik.whitelistedHSUrls;
|
if (!config.piwik) return;
|
||||||
const whitelistedISUrls = config.piwik.whitelistedISUrls || DEFAULTS.piwik.whitelistedISUrls;
|
|
||||||
|
const whitelistedHSUrls = config.piwik.whitelistedHSUrls || [];
|
||||||
|
const whitelistedISUrls = config.piwik.whitelistedISUrls || [];
|
||||||
|
|
||||||
this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
|
this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
|
||||||
this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
|
this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
|
||||||
|
@ -199,11 +222,25 @@ class Analytics {
|
||||||
const rows = Object.values(customVariables).map((v) => Tracker.getCustomVariable(v.id)).filter(Boolean);
|
const rows = Object.values(customVariables).map((v) => Tracker.getCustomVariable(v.id)).filter(Boolean);
|
||||||
|
|
||||||
const resolution = `${window.screen.width}x${window.screen.height}`;
|
const resolution = `${window.screen.width}x${window.screen.height}`;
|
||||||
|
const otherVariables = [
|
||||||
|
{
|
||||||
|
expl: _td('Every page you use in the app'),
|
||||||
|
value: _t(
|
||||||
|
'e.g. <CurrentPageURL>',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
CurrentPageURL: getRedactedUrl(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ expl: _td('Your User Agent'), value: navigator.userAgent },
|
||||||
|
{ expl: _td('Your device resolution'), value: resolution },
|
||||||
|
];
|
||||||
|
|
||||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||||
Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, {
|
Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, {
|
||||||
title: _t('Analytics'),
|
title: _t('Analytics'),
|
||||||
description: <div>
|
description: <div className="mx_UserSettings_analyticsModal">
|
||||||
<div>
|
<div>
|
||||||
{ _t('The information being sent to us to help make Riot.im better includes:') }
|
{ _t('The information being sent to us to help make Riot.im better includes:') }
|
||||||
</div>
|
</div>
|
||||||
|
@ -212,19 +249,14 @@ class Analytics {
|
||||||
<td>{ _t(customVariables[row[0]].expl) }</td>
|
<td>{ _t(customVariables[row[0]].expl) }</td>
|
||||||
<td><code>{ row[1] }</code></td>
|
<td><code>{ row[1] }</code></td>
|
||||||
</tr>) }
|
</tr>) }
|
||||||
</table>
|
{ otherVariables.map((item, index) =>
|
||||||
<br />
|
<tr key={index}>
|
||||||
<div>
|
<td>{ _t(item.expl) }</td>
|
||||||
{ _t('We also record each page you use in the app (currently <CurrentPageHash>), your User Agent'
|
<td><code>{ item.value }</code></td>
|
||||||
+ ' (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).',
|
</tr>,
|
||||||
{},
|
|
||||||
{
|
|
||||||
CurrentPageHash: <code>{ getRedactedHash() }</code>,
|
|
||||||
CurrentUserAgent: <code>{ navigator.userAgent }</code>,
|
|
||||||
CurrentDeviceResolution: <code>{ resolution }</code>,
|
|
||||||
},
|
|
||||||
) }
|
) }
|
||||||
|
</table>
|
||||||
|
<div>
|
||||||
{ _t('Where this page includes identifiable information, such as a room, '
|
{ _t('Where this page includes identifiable information, such as a room, '
|
||||||
+ 'user or group ID, that data is removed before being sent to the server.') }
|
+ 'user or group ID, that data is removed before being sent to the server.') }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,7 +19,7 @@ import sdk from './';
|
||||||
import MultiInviter from './utils/MultiInviter';
|
import MultiInviter from './utils/MultiInviter';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
import GroupStoreCache from './stores/GroupStoreCache';
|
import GroupStore from './stores/GroupStore';
|
||||||
|
|
||||||
export function showGroupInviteDialog(groupId) {
|
export function showGroupInviteDialog(groupId) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -116,11 +116,10 @@ function _onGroupInviteFinished(groupId, addrs) {
|
||||||
|
|
||||||
function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
|
function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
|
||||||
const matrixClient = MatrixClientPeg.get();
|
const matrixClient = MatrixClientPeg.get();
|
||||||
const groupStore = GroupStoreCache.getGroupStore(groupId);
|
|
||||||
const errorList = [];
|
const errorList = [];
|
||||||
return Promise.all(addrs.map((addr) => {
|
return Promise.all(addrs.map((addr) => {
|
||||||
return groupStore
|
return GroupStore
|
||||||
.addRoomToGroup(addr.address, addRoomsPublicly)
|
.addRoomToGroup(groupId, addr.address, addRoomsPublicly)
|
||||||
.catch(() => { errorList.push(addr.address); })
|
.catch(() => { errorList.push(addr.address); })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const roomId = addr.address;
|
const roomId = addr.address;
|
||||||
|
|
|
@ -17,6 +17,8 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
import ReplyThread from "./components/views/elements/ReplyThread";
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const sanitizeHtml = require('sanitize-html');
|
const sanitizeHtml = require('sanitize-html');
|
||||||
const highlight = require('highlight.js');
|
const highlight = require('highlight.js');
|
||||||
|
@ -184,6 +186,7 @@ const sanitizeHtmlParams = {
|
||||||
],
|
],
|
||||||
allowedAttributes: {
|
allowedAttributes: {
|
||||||
// custom ones first:
|
// custom ones first:
|
||||||
|
blockquote: ['data-mx-reply'], // used to allow explicit removal of a reply fallback blockquote, value ignored
|
||||||
font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
||||||
span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
||||||
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
|
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
|
||||||
|
@ -408,12 +411,14 @@ class TextHighlighter extends BaseHighlighter {
|
||||||
*
|
*
|
||||||
* opts.highlightLink: optional href to add to highlighted words
|
* opts.highlightLink: optional href to add to highlighted words
|
||||||
* opts.disableBigEmoji: optional argument to disable the big emoji class.
|
* opts.disableBigEmoji: optional argument to disable the big emoji class.
|
||||||
|
* opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing
|
||||||
*/
|
*/
|
||||||
export function bodyToHtml(content, highlights, opts={}) {
|
export function bodyToHtml(content, highlights, opts={}) {
|
||||||
let isHtml = (content.format === "org.matrix.custom.html");
|
let isHtml = content.format === "org.matrix.custom.html" && content.formatted_body;
|
||||||
|
|
||||||
let bodyHasEmoji = false;
|
let bodyHasEmoji = false;
|
||||||
|
|
||||||
|
let strippedBody;
|
||||||
let safeBody;
|
let safeBody;
|
||||||
// XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
|
// XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
|
||||||
// to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which
|
// to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which
|
||||||
|
@ -431,17 +436,22 @@ export function bodyToHtml(content, highlights, opts={}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyHasEmoji = containsEmoji(isHtml ? content.formatted_body : content.body);
|
let formattedBody = content.formatted_body;
|
||||||
|
if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody);
|
||||||
|
strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(content.body) : content.body;
|
||||||
|
|
||||||
|
bodyHasEmoji = containsEmoji(isHtml ? formattedBody : content.body);
|
||||||
|
|
||||||
|
|
||||||
// Only generate safeBody if the message was sent as org.matrix.custom.html
|
// Only generate safeBody if the message was sent as org.matrix.custom.html
|
||||||
if (isHtml) {
|
if (isHtml) {
|
||||||
safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
|
safeBody = sanitizeHtml(formattedBody, sanitizeHtmlParams);
|
||||||
} else {
|
} else {
|
||||||
// ... or if there are emoji, which we insert as HTML alongside the
|
// ... or if there are emoji, which we insert as HTML alongside the
|
||||||
// escaped plaintext body.
|
// escaped plaintext body.
|
||||||
if (bodyHasEmoji) {
|
if (bodyHasEmoji) {
|
||||||
isHtml = true;
|
isHtml = true;
|
||||||
safeBody = sanitizeHtml(escape(content.body), sanitizeHtmlParams);
|
safeBody = sanitizeHtml(escape(strippedBody), sanitizeHtmlParams);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -458,7 +468,7 @@ export function bodyToHtml(content, highlights, opts={}) {
|
||||||
let emojiBody = false;
|
let emojiBody = false;
|
||||||
if (!opts.disableBigEmoji && bodyHasEmoji) {
|
if (!opts.disableBigEmoji && bodyHasEmoji) {
|
||||||
EMOJI_REGEX.lastIndex = 0;
|
EMOJI_REGEX.lastIndex = 0;
|
||||||
const contentBodyTrimmed = content.body !== undefined ? content.body.trim() : '';
|
const contentBodyTrimmed = strippedBody !== undefined ? strippedBody.trim() : '';
|
||||||
const match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
const match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
||||||
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
|
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
|
||||||
}
|
}
|
||||||
|
@ -471,7 +481,7 @@ export function bodyToHtml(content, highlights, opts={}) {
|
||||||
|
|
||||||
return isHtml ?
|
return isHtml ?
|
||||||
<span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
|
<span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
|
||||||
<span className={className} dir="auto">{ content.body }</span>;
|
<span className={className} dir="auto">{ strippedBody }</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emojifyText(text) {
|
export function emojifyText(text) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -64,33 +65,33 @@ import sdk from './index';
|
||||||
* Resolves to `true` if we ended up starting a session, or `false` if we
|
* Resolves to `true` if we ended up starting a session, or `false` if we
|
||||||
* failed.
|
* failed.
|
||||||
*/
|
*/
|
||||||
export function loadSession(opts) {
|
export async function loadSession(opts) {
|
||||||
const fragmentQueryParams = opts.fragmentQueryParams || {};
|
try {
|
||||||
let enableGuest = opts.enableGuest || false;
|
let enableGuest = opts.enableGuest || false;
|
||||||
const guestHsUrl = opts.guestHsUrl;
|
const guestHsUrl = opts.guestHsUrl;
|
||||||
const guestIsUrl = opts.guestIsUrl;
|
const guestIsUrl = opts.guestIsUrl;
|
||||||
const defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
|
const fragmentQueryParams = opts.fragmentQueryParams || {};
|
||||||
|
const defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
|
||||||
|
|
||||||
if (!guestHsUrl) {
|
if (!guestHsUrl) {
|
||||||
console.warn("Cannot enable guest access: can't determine HS URL to use");
|
console.warn("Cannot enable guest access: can't determine HS URL to use");
|
||||||
enableGuest = false;
|
enableGuest = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableGuest &&
|
if (enableGuest &&
|
||||||
fragmentQueryParams.guest_user_id &&
|
fragmentQueryParams.guest_user_id &&
|
||||||
fragmentQueryParams.guest_access_token
|
fragmentQueryParams.guest_access_token
|
||||||
) {
|
) {
|
||||||
console.log("Using guest access credentials");
|
console.log("Using guest access credentials");
|
||||||
return _doSetLoggedIn({
|
return _doSetLoggedIn({
|
||||||
userId: fragmentQueryParams.guest_user_id,
|
userId: fragmentQueryParams.guest_user_id,
|
||||||
accessToken: fragmentQueryParams.guest_access_token,
|
accessToken: fragmentQueryParams.guest_access_token,
|
||||||
homeserverUrl: guestHsUrl,
|
homeserverUrl: guestHsUrl,
|
||||||
identityServerUrl: guestIsUrl,
|
identityServerUrl: guestIsUrl,
|
||||||
guest: true,
|
guest: true,
|
||||||
}, true).then(() => true);
|
}, true).then(() => true);
|
||||||
}
|
}
|
||||||
|
const success = await _restoreFromLocalStorage();
|
||||||
return _restoreFromLocalStorage().then((success) => {
|
|
||||||
if (success) {
|
if (success) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -101,7 +102,9 @@ export function loadSession(opts) {
|
||||||
|
|
||||||
// fall back to login screen
|
// fall back to login screen
|
||||||
return false;
|
return false;
|
||||||
});
|
} catch (e) {
|
||||||
|
return _handleLoadSessionFailure(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -195,9 +198,9 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
||||||
// The plan is to gradually move the localStorage access done here into
|
// The plan is to gradually move the localStorage access done here into
|
||||||
// SessionStore to avoid bugs where the view becomes out-of-sync with
|
// SessionStore to avoid bugs where the view becomes out-of-sync with
|
||||||
// localStorage (e.g. teamToken, isGuest etc.)
|
// localStorage (e.g. teamToken, isGuest etc.)
|
||||||
function _restoreFromLocalStorage() {
|
async function _restoreFromLocalStorage() {
|
||||||
if (!localStorage) {
|
if (!localStorage) {
|
||||||
return Promise.resolve(false);
|
return false;
|
||||||
}
|
}
|
||||||
const hsUrl = localStorage.getItem("mx_hs_url");
|
const hsUrl = localStorage.getItem("mx_hs_url");
|
||||||
const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org';
|
const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org';
|
||||||
|
@ -215,26 +218,23 @@ function _restoreFromLocalStorage() {
|
||||||
|
|
||||||
if (accessToken && userId && hsUrl) {
|
if (accessToken && userId && hsUrl) {
|
||||||
console.log(`Restoring session for ${userId}`);
|
console.log(`Restoring session for ${userId}`);
|
||||||
try {
|
await _doSetLoggedIn({
|
||||||
return _doSetLoggedIn({
|
userId: userId,
|
||||||
userId: userId,
|
deviceId: deviceId,
|
||||||
deviceId: deviceId,
|
accessToken: accessToken,
|
||||||
accessToken: accessToken,
|
homeserverUrl: hsUrl,
|
||||||
homeserverUrl: hsUrl,
|
identityServerUrl: isUrl,
|
||||||
identityServerUrl: isUrl,
|
guest: isGuest,
|
||||||
guest: isGuest,
|
}, false);
|
||||||
}, false).then(() => true);
|
return true;
|
||||||
} catch (e) {
|
|
||||||
return _handleRestoreFailure(e);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.log("No previous session found.");
|
console.log("No previous session found.");
|
||||||
return Promise.resolve(false);
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _handleRestoreFailure(e) {
|
function _handleLoadSessionFailure(e) {
|
||||||
console.log("Unable to restore session", e);
|
console.log("Unable to load session", e);
|
||||||
|
|
||||||
const def = Promise.defer();
|
const def = Promise.defer();
|
||||||
const SessionRestoreErrorDialog =
|
const SessionRestoreErrorDialog =
|
||||||
|
@ -255,7 +255,7 @@ function _handleRestoreFailure(e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// try, try again
|
// try, try again
|
||||||
return _restoreFromLocalStorage();
|
return loadSession();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,13 +21,6 @@ const DEFAULTS = {
|
||||||
integrations_rest_url: "https://scalar.vector.im/api",
|
integrations_rest_url: "https://scalar.vector.im/api",
|
||||||
// Where to send bug reports. If not specified, bugs cannot be sent.
|
// Where to send bug reports. If not specified, bugs cannot be sent.
|
||||||
bug_report_endpoint_url: null,
|
bug_report_endpoint_url: null,
|
||||||
|
|
||||||
piwik: {
|
|
||||||
url: "https://piwik.riot.im/",
|
|
||||||
whitelistedHSUrls: ["https://matrix.org"],
|
|
||||||
whitelistedISUrls: ["https://vector.im", "https://matrix.org"],
|
|
||||||
siteId: 1,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class SdkConfig {
|
class SdkConfig {
|
||||||
|
@ -52,4 +45,3 @@ class SdkConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = SdkConfig;
|
module.exports = SdkConfig;
|
||||||
module.exports.DEFAULTS = DEFAULTS;
|
|
||||||
|
|
|
@ -58,7 +58,7 @@ function textForMemberEvent(ev) {
|
||||||
});
|
});
|
||||||
} else if (!prevContent.displayname && content.displayname) {
|
} else if (!prevContent.displayname && content.displayname) {
|
||||||
return _t('%(senderName)s set their display name to %(displayName)s.', {
|
return _t('%(senderName)s set their display name to %(displayName)s.', {
|
||||||
senderName,
|
senderName: ev.getSender(),
|
||||||
displayName: content.displayname,
|
displayName: content.displayname,
|
||||||
});
|
});
|
||||||
} else if (prevContent.displayname && !content.displayname) {
|
} else if (prevContent.displayname && !content.displayname) {
|
||||||
|
|
|
@ -101,8 +101,13 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
const {command, range} = this.getCurrentCommand(query, selection, force);
|
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||||
if (command) {
|
|
||||||
completions = this.matcher.match(command[0]).map((user) => {
|
if (!command) return completions;
|
||||||
|
|
||||||
|
const fullMatch = command[0];
|
||||||
|
// Don't search if the query is a single "@"
|
||||||
|
if (fullMatch && fullMatch !== '@') {
|
||||||
|
completions = this.matcher.match(fullMatch).map((user) => {
|
||||||
const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
|
const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
|
||||||
return {
|
return {
|
||||||
// Length of completion should equal length of text in decorator. draft-js
|
// Length of completion should equal length of text in decorator. draft-js
|
||||||
|
|
|
@ -27,7 +27,6 @@ import AccessibleButton from '../views/elements/AccessibleButton';
|
||||||
import Modal from '../../Modal';
|
import Modal from '../../Modal';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import GroupStoreCache from '../../stores/GroupStoreCache';
|
|
||||||
import GroupStore from '../../stores/GroupStore';
|
import GroupStore from '../../stores/GroupStore';
|
||||||
import FlairStore from '../../stores/FlairStore';
|
import FlairStore from '../../stores/FlairStore';
|
||||||
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||||
|
@ -93,8 +92,8 @@ const CategoryRoomList = React.createClass({
|
||||||
if (!success) return;
|
if (!success) return;
|
||||||
const errorList = [];
|
const errorList = [];
|
||||||
Promise.all(addrs.map((addr) => {
|
Promise.all(addrs.map((addr) => {
|
||||||
return this.context.groupStore
|
return GroupStore
|
||||||
.addRoomToGroupSummary(addr.address)
|
.addRoomToGroupSummary(this.props.groupId, addr.address)
|
||||||
.catch(() => { errorList.push(addr.address); })
|
.catch(() => { errorList.push(addr.address); })
|
||||||
.reflect();
|
.reflect();
|
||||||
})).then(() => {
|
})).then(() => {
|
||||||
|
@ -174,7 +173,8 @@ const FeaturedRoom = React.createClass({
|
||||||
onDeleteClicked: function(e) {
|
onDeleteClicked: function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.context.groupStore.removeRoomFromGroupSummary(
|
GroupStore.removeRoomFromGroupSummary(
|
||||||
|
this.props.groupId,
|
||||||
this.props.summaryInfo.room_id,
|
this.props.summaryInfo.room_id,
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
console.error('Error whilst removing room from group summary', err);
|
console.error('Error whilst removing room from group summary', err);
|
||||||
|
@ -269,7 +269,7 @@ const RoleUserList = React.createClass({
|
||||||
if (!success) return;
|
if (!success) return;
|
||||||
const errorList = [];
|
const errorList = [];
|
||||||
Promise.all(addrs.map((addr) => {
|
Promise.all(addrs.map((addr) => {
|
||||||
return this.context.groupStore
|
return GroupStore
|
||||||
.addUserToGroupSummary(addr.address)
|
.addUserToGroupSummary(addr.address)
|
||||||
.catch(() => { errorList.push(addr.address); })
|
.catch(() => { errorList.push(addr.address); })
|
||||||
.reflect();
|
.reflect();
|
||||||
|
@ -344,7 +344,8 @@ const FeaturedUser = React.createClass({
|
||||||
onDeleteClicked: function(e) {
|
onDeleteClicked: function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.context.groupStore.removeUserFromGroupSummary(
|
GroupStore.removeUserFromGroupSummary(
|
||||||
|
this.props.groupId,
|
||||||
this.props.summaryInfo.user_id,
|
this.props.summaryInfo.user_id,
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
console.error('Error whilst removing user from group summary', err);
|
console.error('Error whilst removing user from group summary', err);
|
||||||
|
@ -390,15 +391,6 @@ const FeaturedUser = React.createClass({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const GroupContext = {
|
|
||||||
groupStore: PropTypes.instanceOf(GroupStore).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
CategoryRoomList.contextTypes = GroupContext;
|
|
||||||
FeaturedRoom.contextTypes = GroupContext;
|
|
||||||
RoleUserList.contextTypes = GroupContext;
|
|
||||||
FeaturedUser.contextTypes = GroupContext;
|
|
||||||
|
|
||||||
const GROUP_JOINPOLICY_OPEN = "open";
|
const GROUP_JOINPOLICY_OPEN = "open";
|
||||||
const GROUP_JOINPOLICY_INVITE = "invite";
|
const GROUP_JOINPOLICY_INVITE = "invite";
|
||||||
|
|
||||||
|
@ -415,12 +407,6 @@ export default React.createClass({
|
||||||
groupStore: PropTypes.instanceOf(GroupStore),
|
groupStore: PropTypes.instanceOf(GroupStore),
|
||||||
},
|
},
|
||||||
|
|
||||||
getChildContext: function() {
|
|
||||||
return {
|
|
||||||
groupStore: this._groupStore,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
summary: null,
|
summary: null,
|
||||||
|
@ -440,6 +426,7 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
this._unmounted = false;
|
||||||
this._matrixClient = MatrixClientPeg.get();
|
this._matrixClient = MatrixClientPeg.get();
|
||||||
this._matrixClient.on("Group.myMembership", this._onGroupMyMembership);
|
this._matrixClient.on("Group.myMembership", this._onGroupMyMembership);
|
||||||
|
|
||||||
|
@ -448,8 +435,8 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
this._unmounted = true;
|
||||||
this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||||
this._groupStore.removeAllListeners();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(newProps) {
|
componentWillReceiveProps: function(newProps) {
|
||||||
|
@ -464,8 +451,7 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_onGroupMyMembership: function(group) {
|
_onGroupMyMembership: function(group) {
|
||||||
if (group.groupId !== this.props.groupId) return;
|
if (this._unmounted || group.groupId !== this.props.groupId) return;
|
||||||
|
|
||||||
if (group.myMembership === 'leave') {
|
if (group.myMembership === 'leave') {
|
||||||
// Leave settings - the user might have clicked the "Leave" button
|
// Leave settings - the user might have clicked the "Leave" button
|
||||||
this._closeSettings();
|
this._closeSettings();
|
||||||
|
@ -478,34 +464,11 @@ export default React.createClass({
|
||||||
if (group && group.inviter && group.inviter.userId) {
|
if (group && group.inviter && group.inviter.userId) {
|
||||||
this._fetchInviterProfile(group.inviter.userId);
|
this._fetchInviterProfile(group.inviter.userId);
|
||||||
}
|
}
|
||||||
this._groupStore = GroupStoreCache.getGroupStore(groupId);
|
GroupStore.registerListener(groupId, this.onGroupStoreUpdated.bind(this, firstInit));
|
||||||
this._groupStore.registerListener(() => {
|
|
||||||
const summary = this._groupStore.getSummary();
|
|
||||||
if (summary.profile) {
|
|
||||||
// Default profile fields should be "" for later sending to the server (which
|
|
||||||
// requires that the fields are strings, not null)
|
|
||||||
["avatar_url", "long_description", "name", "short_description"].forEach((k) => {
|
|
||||||
summary.profile[k] = summary.profile[k] || "";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
summary,
|
|
||||||
summaryLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.Summary),
|
|
||||||
isGroupPublicised: this._groupStore.getGroupPublicity(),
|
|
||||||
isUserPrivileged: this._groupStore.isUserPrivileged(),
|
|
||||||
groupRooms: this._groupStore.getGroupRooms(),
|
|
||||||
groupRoomsLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.GroupRooms),
|
|
||||||
isUserMember: this._groupStore.getGroupMembers().some(
|
|
||||||
(m) => m.userId === this._matrixClient.credentials.userId,
|
|
||||||
),
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
if (this.props.groupIsNew && firstInit) {
|
|
||||||
this._onEditClick();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let willDoOnboarding = false;
|
let willDoOnboarding = false;
|
||||||
this._groupStore.on('error', (err) => {
|
// XXX: This should be more fluxy - let's get the error from GroupStore .getError or something
|
||||||
|
GroupStore.on('error', (err, errorGroupId) => {
|
||||||
|
if (this._unmounted || groupId !== errorGroupId) return;
|
||||||
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN' && !willDoOnboarding) {
|
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN' && !willDoOnboarding) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'do_after_sync_prepared',
|
action: 'do_after_sync_prepared',
|
||||||
|
@ -520,15 +483,45 @@ export default React.createClass({
|
||||||
this.setState({
|
this.setState({
|
||||||
summary: null,
|
summary: null,
|
||||||
error: err,
|
error: err,
|
||||||
|
editing: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onGroupStoreUpdated(firstInit) {
|
||||||
|
if (this._unmounted) return;
|
||||||
|
const summary = GroupStore.getSummary(this.props.groupId);
|
||||||
|
if (summary.profile) {
|
||||||
|
// Default profile fields should be "" for later sending to the server (which
|
||||||
|
// requires that the fields are strings, not null)
|
||||||
|
["avatar_url", "long_description", "name", "short_description"].forEach((k) => {
|
||||||
|
summary.profile[k] = summary.profile[k] || "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
summary,
|
||||||
|
summaryLoading: !GroupStore.isStateReady(this.props.groupId, GroupStore.STATE_KEY.Summary),
|
||||||
|
isGroupPublicised: GroupStore.getGroupPublicity(this.props.groupId),
|
||||||
|
isUserPrivileged: GroupStore.isUserPrivileged(this.props.groupId),
|
||||||
|
groupRooms: GroupStore.getGroupRooms(this.props.groupId),
|
||||||
|
groupRoomsLoading: !GroupStore.isStateReady(this.props.groupId, GroupStore.STATE_KEY.GroupRooms),
|
||||||
|
isUserMember: GroupStore.getGroupMembers(this.props.groupId).some(
|
||||||
|
(m) => m.userId === this._matrixClient.credentials.userId,
|
||||||
|
),
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
// XXX: This might not work but this.props.groupIsNew unused anyway
|
||||||
|
if (this.props.groupIsNew && firstInit) {
|
||||||
|
this._onEditClick();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
_fetchInviterProfile(userId) {
|
_fetchInviterProfile(userId) {
|
||||||
this.setState({
|
this.setState({
|
||||||
inviterProfileBusy: true,
|
inviterProfileBusy: true,
|
||||||
});
|
});
|
||||||
this._matrixClient.getProfileInfo(userId).then((resp) => {
|
this._matrixClient.getProfileInfo(userId).then((resp) => {
|
||||||
|
if (this._unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
inviterProfile: {
|
inviterProfile: {
|
||||||
avatarUrl: resp.avatar_url,
|
avatarUrl: resp.avatar_url,
|
||||||
|
@ -538,6 +531,7 @@ export default React.createClass({
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
console.error('Error getting group inviter profile', e);
|
console.error('Error getting group inviter profile', e);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
if (this._unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
inviterProfileBusy: false,
|
inviterProfileBusy: false,
|
||||||
});
|
});
|
||||||
|
@ -677,7 +671,7 @@ export default React.createClass({
|
||||||
// spinner disappearing after we have fetched new group data.
|
// spinner disappearing after we have fetched new group data.
|
||||||
await Promise.delay(500);
|
await Promise.delay(500);
|
||||||
|
|
||||||
this._groupStore.acceptGroupInvite().then(() => {
|
GroupStore.acceptGroupInvite(this.props.groupId).then(() => {
|
||||||
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
this.setState({membershipBusy: false});
|
this.setState({membershipBusy: false});
|
||||||
|
@ -696,7 +690,7 @@ export default React.createClass({
|
||||||
// spinner disappearing after we have fetched new group data.
|
// spinner disappearing after we have fetched new group data.
|
||||||
await Promise.delay(500);
|
await Promise.delay(500);
|
||||||
|
|
||||||
this._groupStore.leaveGroup().then(() => {
|
GroupStore.leaveGroup(this.props.groupId).then(() => {
|
||||||
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
this.setState({membershipBusy: false});
|
this.setState({membershipBusy: false});
|
||||||
|
@ -715,7 +709,7 @@ export default React.createClass({
|
||||||
// spinner disappearing after we have fetched new group data.
|
// spinner disappearing after we have fetched new group data.
|
||||||
await Promise.delay(500);
|
await Promise.delay(500);
|
||||||
|
|
||||||
this._groupStore.joinGroup().then(() => {
|
GroupStore.joinGroup(this.props.groupId).then(() => {
|
||||||
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
this.setState({membershipBusy: false});
|
this.setState({membershipBusy: false});
|
||||||
|
@ -743,7 +737,7 @@ export default React.createClass({
|
||||||
// spinner disappearing after we have fetched new group data.
|
// spinner disappearing after we have fetched new group data.
|
||||||
await Promise.delay(500);
|
await Promise.delay(500);
|
||||||
|
|
||||||
this._groupStore.leaveGroup().then(() => {
|
GroupStore.leaveGroup(this.props.groupId).then(() => {
|
||||||
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
this.setState({membershipBusy: false});
|
this.setState({membershipBusy: false});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -351,16 +351,16 @@ export default React.createClass({
|
||||||
guestIsUrl: this.getCurrentIsUrl(),
|
guestIsUrl: this.getCurrentIsUrl(),
|
||||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||||
});
|
});
|
||||||
}).catch((e) => {
|
|
||||||
console.error(`Error attempting to load session: ${e}`);
|
|
||||||
return false;
|
|
||||||
}).then((loadedSession) => {
|
}).then((loadedSession) => {
|
||||||
if (!loadedSession) {
|
if (!loadedSession) {
|
||||||
// fall back to showing the login screen
|
// fall back to showing the login screen
|
||||||
dis.dispatch({action: "start_login"});
|
dis.dispatch({action: "start_login"});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).done();
|
// Note we don't catch errors from this: we catch everything within
|
||||||
|
// loadSession as there's logic there to ask the user if they want
|
||||||
|
// to try logging out.
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
@ -413,6 +413,10 @@ export default React.createClass({
|
||||||
performance.clearMarks('riot_MatrixChat_page_change_start');
|
performance.clearMarks('riot_MatrixChat_page_change_start');
|
||||||
performance.clearMarks('riot_MatrixChat_page_change_stop');
|
performance.clearMarks('riot_MatrixChat_page_change_stop');
|
||||||
const measurement = performance.getEntriesByName('riot_MatrixChat_page_change_delta').pop();
|
const measurement = performance.getEntriesByName('riot_MatrixChat_page_change_delta').pop();
|
||||||
|
|
||||||
|
// In practice, sometimes the entries list is empty, so we get no measurement
|
||||||
|
if (!measurement) return null;
|
||||||
|
|
||||||
return measurement.duration;
|
return measurement.duration;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ import Analytics from '../../Analytics';
|
||||||
import RateLimitedFunc from '../../ratelimitedfunc';
|
import RateLimitedFunc from '../../ratelimitedfunc';
|
||||||
import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
||||||
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||||
import GroupStoreCache from '../../stores/GroupStoreCache';
|
import GroupStore from '../../stores/GroupStore';
|
||||||
|
|
||||||
import { formatCount } from '../../utils/FormattingUtils';
|
import { formatCount } from '../../utils/FormattingUtils';
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ module.exports = React.createClass({
|
||||||
if (this.context.matrixClient) {
|
if (this.context.matrixClient) {
|
||||||
this.context.matrixClient.removeListener("RoomState.members", this.onRoomStateMember);
|
this.context.matrixClient.removeListener("RoomState.members", this.onRoomStateMember);
|
||||||
}
|
}
|
||||||
this._unregisterGroupStore();
|
this._unregisterGroupStore(this.props.groupId);
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -132,26 +132,23 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
componentWillReceiveProps(newProps) {
|
componentWillReceiveProps(newProps) {
|
||||||
if (newProps.groupId !== this.props.groupId) {
|
if (newProps.groupId !== this.props.groupId) {
|
||||||
this._unregisterGroupStore();
|
this._unregisterGroupStore(this.props.groupId);
|
||||||
this._initGroupStore(newProps.groupId);
|
this._initGroupStore(newProps.groupId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_initGroupStore(groupId) {
|
_initGroupStore(groupId) {
|
||||||
if (!groupId) return;
|
if (!groupId) return;
|
||||||
this._groupStore = GroupStoreCache.getGroupStore(groupId);
|
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
|
||||||
this._groupStore.registerListener(this.onGroupStoreUpdated);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_unregisterGroupStore() {
|
_unregisterGroupStore() {
|
||||||
if (this._groupStore) {
|
GroupStore.unregisterListener(this.onGroupStoreUpdated);
|
||||||
this._groupStore.unregisterListener(this.onGroupStoreUpdated);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onGroupStoreUpdated: function() {
|
onGroupStoreUpdated: function() {
|
||||||
this.setState({
|
this.setState({
|
||||||
isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(),
|
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -908,17 +908,17 @@ module.exports = React.createClass({
|
||||||
this.setState({ draggingFile: false });
|
this.setState({ draggingFile: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadFile: function(file) {
|
uploadFile: async function(file) {
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
dis.dispatch({action: 'view_set_mxid'});
|
dis.dispatch({action: 'view_set_mxid'});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentMessages.sendContentToRoom(
|
try {
|
||||||
file, this.state.room.roomId, MatrixClientPeg.get(),
|
await ContentMessages.sendContentToRoom(file, this.state.room.roomId, MatrixClientPeg.get());
|
||||||
).catch((error) => {
|
} catch (error) {
|
||||||
if (error.name === "UnknownDeviceError") {
|
if (error.name === "UnknownDeviceError") {
|
||||||
// Let the staus bar handle this
|
// Let the status bar handle this
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
@ -928,6 +928,14 @@ module.exports = React.createClass({
|
||||||
description: ((error && error.message)
|
description: ((error && error.message)
|
||||||
? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
|
? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// bail early to avoid calling the dispatch below
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send message_sent callback, for things like _checkIfAlone because after all a file is still a message.
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'message_sent',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -63,6 +63,7 @@ const gHVersionLabel = function(repo, token='') {
|
||||||
const SIMPLE_SETTINGS = [
|
const SIMPLE_SETTINGS = [
|
||||||
{ id: "urlPreviewsEnabled" },
|
{ id: "urlPreviewsEnabled" },
|
||||||
{ id: "autoplayGifsAndVideos" },
|
{ id: "autoplayGifsAndVideos" },
|
||||||
|
{ id: "alwaysShowEncryptionIcons" },
|
||||||
{ id: "hideReadReceipts" },
|
{ id: "hideReadReceipts" },
|
||||||
{ id: "dontSendTypingNotifications" },
|
{ id: "dontSendTypingNotifications" },
|
||||||
{ id: "alwaysShowTimestamps" },
|
{ id: "alwaysShowTimestamps" },
|
||||||
|
@ -801,10 +802,10 @@ module.exports = React.createClass({
|
||||||
"us track down the problem. Debug logs contain application " +
|
"us track down the problem. Debug logs contain application " +
|
||||||
"usage data including your username, the IDs or aliases of " +
|
"usage data including your username, the IDs or aliases of " +
|
||||||
"the rooms or groups you have visited and the usernames of " +
|
"the rooms or groups you have visited and the usernames of " +
|
||||||
"other users. They do not contian messages.",
|
"other users. They do not contain messages.",
|
||||||
)
|
)
|
||||||
}</p>
|
}</p>
|
||||||
<button className="mx_UserSettings_button danger"
|
<button className="mx_UserSettings_button"
|
||||||
onClick={this._onBugReportClicked}>{ _t('Submit debug logs') }
|
onClick={this._onBugReportClicked}>{ _t('Submit debug logs') }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
|
@ -34,13 +35,16 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
/* the MatrixEvent associated with the context menu */
|
/* the MatrixEvent associated with the context menu */
|
||||||
mxEvent: React.PropTypes.object.isRequired,
|
mxEvent: PropTypes.object.isRequired,
|
||||||
|
|
||||||
/* an optional EventTileOps implementation that can be used to unhide preview widgets */
|
/* an optional EventTileOps implementation that can be used to unhide preview widgets */
|
||||||
eventTileOps: React.PropTypes.object,
|
eventTileOps: PropTypes.object,
|
||||||
|
|
||||||
|
/* an optional function to be called when the user clicks collapse thread, if not provided hide button */
|
||||||
|
collapseReplyThread: PropTypes.func,
|
||||||
|
|
||||||
/* callback called when the menu is dismissed */
|
/* callback called when the menu is dismissed */
|
||||||
onFinished: React.PropTypes.func,
|
onFinished: PropTypes.func,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -182,12 +186,17 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onReplyClick: function() {
|
onReplyClick: function() {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'quote_event',
|
action: 'reply_to_event',
|
||||||
event: this.props.mxEvent,
|
event: this.props.mxEvent,
|
||||||
});
|
});
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onCollapseReplyThreadClick: function() {
|
||||||
|
this.props.collapseReplyThread();
|
||||||
|
this.closeMenu();
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const eventStatus = this.props.mxEvent.status;
|
const eventStatus = this.props.mxEvent.status;
|
||||||
let resendButton;
|
let resendButton;
|
||||||
|
@ -200,6 +209,7 @@ module.exports = React.createClass({
|
||||||
let externalURLButton;
|
let externalURLButton;
|
||||||
let quoteButton;
|
let quoteButton;
|
||||||
let replyButton;
|
let replyButton;
|
||||||
|
let collapseReplyThread;
|
||||||
|
|
||||||
if (eventStatus === 'not_sent') {
|
if (eventStatus === 'not_sent') {
|
||||||
resendButton = (
|
resendButton = (
|
||||||
|
@ -305,6 +315,13 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.collapseReplyThread) {
|
||||||
|
collapseReplyThread = (
|
||||||
|
<div className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
|
||||||
|
{ _t('Collapse Reply Thread') }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -320,6 +337,7 @@ module.exports = React.createClass({
|
||||||
{ quoteButton }
|
{ quoteButton }
|
||||||
{ replyButton }
|
{ replyButton }
|
||||||
{ externalURLButton }
|
{ externalURLButton }
|
||||||
|
{ collapseReplyThread }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,7 +22,7 @@ import sdk from '../../../index';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
||||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
import GroupStore from '../../../stores/GroupStore';
|
||||||
|
|
||||||
const TRUNCATE_QUERY_LIST = 40;
|
const TRUNCATE_QUERY_LIST = 40;
|
||||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||||
|
@ -243,9 +243,8 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_doNaiveGroupRoomSearch: function(query) {
|
_doNaiveGroupRoomSearch: function(query) {
|
||||||
const lowerCaseQuery = query.toLowerCase();
|
const lowerCaseQuery = query.toLowerCase();
|
||||||
const groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
|
|
||||||
const results = [];
|
const results = [];
|
||||||
groupStore.getGroupRooms().forEach((r) => {
|
GroupStore.getGroupRooms(this.props.groupId).forEach((r) => {
|
||||||
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
|
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery);
|
const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery);
|
const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -36,8 +37,18 @@ export default React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
// onFinished callback to call when Escape is pressed
|
// onFinished callback to call when Escape is pressed
|
||||||
|
// Take a boolean which is true if the dialog was dismissed
|
||||||
|
// with a positive / confirm action or false if it was
|
||||||
|
// cancelled (BaseDialog itself only calls this with false).
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
// Whether the dialog should have a 'close' button that will
|
||||||
|
// cause the dialog to be cancelled. This should only be set
|
||||||
|
// to false if there is nothing the app can sensibly do if the
|
||||||
|
// dialog is cancelled, eg. "We can't restore your session and
|
||||||
|
// the app cannot work". Default: true.
|
||||||
|
hasCancel: PropTypes.bool,
|
||||||
|
|
||||||
// called when a key is pressed
|
// called when a key is pressed
|
||||||
onKeyDown: PropTypes.func,
|
onKeyDown: PropTypes.func,
|
||||||
|
|
||||||
|
@ -56,6 +67,12 @@ export default React.createClass({
|
||||||
contentId: React.PropTypes.string,
|
contentId: React.PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
hasCancel: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
childContextTypes: {
|
childContextTypes: {
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||||
},
|
},
|
||||||
|
@ -74,15 +91,15 @@ export default React.createClass({
|
||||||
if (this.props.onKeyDown) {
|
if (this.props.onKeyDown) {
|
||||||
this.props.onKeyDown(e);
|
this.props.onKeyDown(e);
|
||||||
}
|
}
|
||||||
if (e.keyCode === KeyCode.ESCAPE) {
|
if (this.props.hasCancel && e.keyCode === KeyCode.ESCAPE) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onFinished();
|
this.props.onFinished(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_onCancelClick: function(e) {
|
_onCancelClick: function(e) {
|
||||||
this.props.onFinished();
|
this.props.onFinished(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
@ -101,11 +118,11 @@ export default React.createClass({
|
||||||
// AT users can skip its presentation.
|
// AT users can skip its presentation.
|
||||||
aria-describedby={this.props.contentId}
|
aria-describedby={this.props.contentId}
|
||||||
>
|
>
|
||||||
<AccessibleButton onClick={this._onCancelClick}
|
{ this.props.hasCancel ? <AccessibleButton onClick={this._onCancelClick}
|
||||||
className="mx_Dialog_cancelButton"
|
className="mx_Dialog_cancelButton"
|
||||||
>
|
>
|
||||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||||
</AccessibleButton>
|
</AccessibleButton> : null }
|
||||||
<div className={'mx_Dialog_title ' + this.props.titleClass} id='mx_BaseDialog_title'>
|
<div className={'mx_Dialog_title ' + this.props.titleClass} id='mx_BaseDialog_title'>
|
||||||
{ this.props.title }
|
{ this.props.title }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 OpenMarket Ltd
|
Copyright 2017 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -105,6 +106,8 @@ export default class BugReportDialog extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
|
||||||
let error = null;
|
let error = null;
|
||||||
if (this.state.err) {
|
if (this.state.err) {
|
||||||
|
@ -113,13 +116,6 @@ export default class BugReportDialog extends React.Component {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cancelButton = null;
|
|
||||||
if (!this.state.busy) {
|
|
||||||
cancelButton = <button onClick={this._onCancel}>
|
|
||||||
{ _t("Cancel") }
|
|
||||||
</button>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let progress = null;
|
let progress = null;
|
||||||
if (this.state.busy) {
|
if (this.state.busy) {
|
||||||
progress = (
|
progress = (
|
||||||
|
@ -131,11 +127,11 @@ export default class BugReportDialog extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_BugReportDialog">
|
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel}
|
||||||
<div className="mx_Dialog_title">
|
title={_t('Submit debug logs')}
|
||||||
{ _t("Submit debug logs") }
|
contentId='mx_Dialog_content'
|
||||||
</div>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||||
<p>
|
<p>
|
||||||
{ _t(
|
{ _t(
|
||||||
"Debug logs contain application usage data including your " +
|
"Debug logs contain application usage data including your " +
|
||||||
|
@ -146,7 +142,7 @@ export default class BugReportDialog extends React.Component {
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{ _t(
|
{ _t(
|
||||||
"<a>Click here</a> to create a GitHub issue.",
|
"Riot bugs are tracked on GitHub: <a>create a GitHub issue</a>.",
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
a: (sub) => <a
|
a: (sub) => <a
|
||||||
|
@ -191,19 +187,13 @@ export default class BugReportDialog extends React.Component {
|
||||||
{progress}
|
{progress}
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<DialogButtons primaryButton={_t("Send logs")}
|
||||||
<button
|
onPrimaryButtonClick={this._onSubmit}
|
||||||
className="mx_Dialog_primary danger"
|
focus={true}
|
||||||
onClick={this._onSubmit}
|
onCancel={this._onCancel}
|
||||||
autoFocus={true}
|
disabled={this.state.busy}
|
||||||
disabled={this.state.busy}
|
/>
|
||||||
>
|
</BaseDialog>
|
||||||
{ _t("Send logs") }
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{cancelButton}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -30,59 +31,79 @@ export default React.createClass({
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
if (this.refs.bugreportLink) {
|
|
||||||
this.refs.bugreportLink.focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_sendBugReport: function() {
|
_sendBugReport: function() {
|
||||||
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
|
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
|
||||||
Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {});
|
Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {});
|
||||||
},
|
},
|
||||||
|
|
||||||
_continueClicked: function() {
|
_onClearStorageClick: function() {
|
||||||
this.props.onFinished(true);
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
Modal.createTrackedDialog('Session Restore Confirm Logout', '', QuestionDialog, {
|
||||||
|
title: _t("Sign out"),
|
||||||
|
description:
|
||||||
|
<div>{ _t("Log out and remove encryption keys?") }</div>,
|
||||||
|
button: _t("Sign out"),
|
||||||
|
danger: true,
|
||||||
|
onFinished: this.props.onFinished,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onRefreshClick: function() {
|
||||||
|
// Is this likely to help? Probably not, but giving only one button
|
||||||
|
// that clears your storage seems awful.
|
||||||
|
window.location.reload(true);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
let bugreport;
|
|
||||||
|
|
||||||
|
const clearStorageButton = (
|
||||||
|
<button onClick={this._onClearStorageClick} className="danger">
|
||||||
|
{ _t("Clear Storage and Sign Out") }
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
let dialogButtons;
|
||||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||||
bugreport = (
|
dialogButtons = <DialogButtons primaryButton={_t("Send Logs")}
|
||||||
<p>
|
onPrimaryButtonClick={this._sendBugReport}
|
||||||
{ _t(
|
focus={true}
|
||||||
"Otherwise, <a>click here</a> to send a bug report.",
|
hasCancel={false}
|
||||||
{},
|
>
|
||||||
{ 'a': (sub) => <a ref="bugreportLink" onClick={this._sendBugReport}
|
{ clearStorageButton }
|
||||||
key="bugreport" href='#'>{ sub }</a> },
|
</DialogButtons>;
|
||||||
) }
|
} else {
|
||||||
</p>
|
dialogButtons = <DialogButtons primaryButton={_t("Refresh")}
|
||||||
);
|
onPrimaryButtonClick={this._onRefreshClick}
|
||||||
|
focus={true}
|
||||||
|
hasCancel={false}
|
||||||
|
>
|
||||||
|
{ clearStorageButton }
|
||||||
|
</DialogButtons>;
|
||||||
}
|
}
|
||||||
const shouldFocusContinueButton =!(bugreport==true);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||||
title={_t('Unable to restore session')}
|
title={_t('Unable to restore session')}
|
||||||
contentId='mx_Dialog_content'
|
contentId='mx_Dialog_content'
|
||||||
|
hasCancel={false}
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||||
<p>{ _t("We encountered an error trying to restore your previous session. If " +
|
<p>{ _t("We encountered an error trying to restore your previous session.") }</p>
|
||||||
"you continue, you will need to log in again, and encrypted chat " +
|
|
||||||
"history will be unreadable.") }</p>
|
|
||||||
|
|
||||||
<p>{ _t("If you have previously used a more recent version of Riot, your session " +
|
<p>{ _t(
|
||||||
"may be incompatible with this version. Close this window and return " +
|
"If you have previously used a more recent version of Riot, your session " +
|
||||||
"to the more recent version.") }</p>
|
"may be incompatible with this version. Close this window and return " +
|
||||||
|
"to the more recent version.",
|
||||||
|
) }</p>
|
||||||
|
|
||||||
{ bugreport }
|
<p>{ _t(
|
||||||
|
"Clearing your browser's storage may fix the problem, but will sign you " +
|
||||||
|
"out and cause any encrypted chat history to become unreadable.",
|
||||||
|
) }</p>
|
||||||
</div>
|
</div>
|
||||||
<DialogButtons primaryButton={_t("Continue anyway")}
|
{ dialogButtons }
|
||||||
onPrimaryButtonClick={this._continueClicked} focus={shouldFocusContinueButton}
|
|
||||||
onCancel={this.props.onFinished} />
|
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -54,6 +54,7 @@ export default class AppTile extends React.Component {
|
||||||
this._onInitialLoad = this._onInitialLoad.bind(this);
|
this._onInitialLoad = this._onInitialLoad.bind(this);
|
||||||
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
|
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
|
||||||
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
|
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
|
||||||
|
this._onPopoutWidgetClick = this._onPopoutWidgetClick.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -499,6 +500,13 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onPopoutWidgetClick(e) {
|
||||||
|
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
|
||||||
|
// window.open(this._getSafeUrl(), '_blank', 'noopener=yes,noreferrer=yes');
|
||||||
|
Object.assign(document.createElement('a'),
|
||||||
|
{ target: '_blank', href: this._getSafeUrl(), rel: 'noopener noreferrer'}).click();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let appTileBody;
|
let appTileBody;
|
||||||
|
|
||||||
|
@ -581,6 +589,7 @@ export default class AppTile extends React.Component {
|
||||||
// Picture snapshot - only show button when apps are maximised.
|
// Picture snapshot - only show button when apps are maximised.
|
||||||
const showPictureSnapshotButton = this._hasCapability('screenshot') && this.props.show;
|
const showPictureSnapshotButton = this._hasCapability('screenshot') && this.props.show;
|
||||||
const showPictureSnapshotIcon = 'img/camera_green.svg';
|
const showPictureSnapshotIcon = 'img/camera_green.svg';
|
||||||
|
const popoutWidgetIcon = 'img/button-new-window.svg';
|
||||||
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
|
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -599,15 +608,25 @@ export default class AppTile extends React.Component {
|
||||||
{ this.props.showTitle && this._getTileTitle() }
|
{ this.props.showTitle && this._getTileTitle() }
|
||||||
</span>
|
</span>
|
||||||
<span className="mx_AppTileMenuBarWidgets">
|
<span className="mx_AppTileMenuBarWidgets">
|
||||||
{ /* Snapshot widget */ }
|
{ /* Popout widget */ }
|
||||||
{ showPictureSnapshotButton && <TintableSvgButton
|
{ this.props.showPopout && <TintableSvgButton
|
||||||
src={showPictureSnapshotIcon}
|
src={popoutWidgetIcon}
|
||||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||||
title={_t('Picture')}
|
title={_t('Popout widget')}
|
||||||
onClick={this._onSnapshotClick}
|
onClick={this._onPopoutWidgetClick}
|
||||||
width="10"
|
width="10"
|
||||||
height="10"
|
height="10"
|
||||||
/> }
|
/> }
|
||||||
|
|
||||||
|
{ /* Snapshot widget */ }
|
||||||
|
{ showPictureSnapshotButton && <TintableSvgButton
|
||||||
|
src={showPictureSnapshotIcon}
|
||||||
|
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||||
|
title={_t('Picture')}
|
||||||
|
onClick={this._onSnapshotClick}
|
||||||
|
width="10"
|
||||||
|
height="10"
|
||||||
|
/> }
|
||||||
|
|
||||||
{ /* Edit widget */ }
|
{ /* Edit widget */ }
|
||||||
{ showEditButton && <TintableSvgButton
|
{ showEditButton && <TintableSvgButton
|
||||||
|
@ -670,6 +689,8 @@ AppTile.propTypes = {
|
||||||
handleMinimisePointerEvents: PropTypes.bool,
|
handleMinimisePointerEvents: PropTypes.bool,
|
||||||
// Optionally hide the delete icon
|
// Optionally hide the delete icon
|
||||||
showDelete: PropTypes.bool,
|
showDelete: PropTypes.bool,
|
||||||
|
// Optionally hide the popout widget icon
|
||||||
|
showPopout: PropTypes.bool,
|
||||||
// Widget apabilities to allow by default (without user confirmation)
|
// Widget apabilities to allow by default (without user confirmation)
|
||||||
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
||||||
// basic widget capabilities, e.g. injecting sticker message events.
|
// basic widget capabilities, e.g. injecting sticker message events.
|
||||||
|
@ -686,6 +707,7 @@ AppTile.defaultProps = {
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
showMinimise: true,
|
showMinimise: true,
|
||||||
showDelete: true,
|
showDelete: true,
|
||||||
|
showPopout: true,
|
||||||
handleMinimisePointerEvents: false,
|
handleMinimisePointerEvents: false,
|
||||||
whitelistCapabilities: [],
|
whitelistCapabilities: [],
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Aidan Gauland
|
Copyright 2017 Aidan Gauland
|
||||||
|
Copyright 2018 New Vector Ltd.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,8 +15,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -33,10 +32,26 @@ module.exports = React.createClass({
|
||||||
// onClick handler for the primary button.
|
// onClick handler for the primary button.
|
||||||
onPrimaryButtonClick: PropTypes.func.isRequired,
|
onPrimaryButtonClick: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
// should there be a cancel button? default: true
|
||||||
|
hasCancel: PropTypes.bool,
|
||||||
|
|
||||||
// onClick handler for the cancel button.
|
// onClick handler for the cancel button.
|
||||||
onCancel: PropTypes.func.isRequired,
|
onCancel: PropTypes.func,
|
||||||
|
|
||||||
focus: PropTypes.bool,
|
focus: PropTypes.bool,
|
||||||
|
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
hasCancel: true,
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
_onCancelClick: function() {
|
||||||
|
this.props.onCancel();
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
@ -44,18 +59,23 @@ module.exports = React.createClass({
|
||||||
if (this.props.primaryButtonClass) {
|
if (this.props.primaryButtonClass) {
|
||||||
primaryButtonClassName += " " + this.props.primaryButtonClass;
|
primaryButtonClassName += " " + this.props.primaryButtonClass;
|
||||||
}
|
}
|
||||||
|
let cancelButton;
|
||||||
|
if (this.props.hasCancel) {
|
||||||
|
cancelButton = <button onClick={this._onCancelClick} disabled={this.props.disabled}>
|
||||||
|
{ _t("Cancel") }
|
||||||
|
</button>;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button className={primaryButtonClassName}
|
<button className={primaryButtonClassName}
|
||||||
onClick={this.props.onPrimaryButtonClick}
|
onClick={this.props.onPrimaryButtonClick}
|
||||||
autoFocus={this.props.focus}
|
autoFocus={this.props.focus}
|
||||||
|
disabled={this.props.disabled}
|
||||||
>
|
>
|
||||||
{ this.props.primaryButton }
|
{ this.props.primaryButton }
|
||||||
</button>
|
</button>
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
<button onClick={this.props.onCancel}>
|
{ cancelButton }
|
||||||
{ _t("Cancel") }
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,188 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2017 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.
|
|
||||||
*/
|
|
||||||
import React from 'react';
|
|
||||||
import sdk from '../../../index';
|
|
||||||
import {_t} from '../../../languageHandler';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
|
||||||
import {wantsDateSeparator} from '../../../DateUtils';
|
|
||||||
import {MatrixEvent} from 'matrix-js-sdk';
|
|
||||||
import {makeUserPermalink} from "../../../matrix-to";
|
|
||||||
|
|
||||||
// For URLs of matrix.to links in the timeline which have been reformatted by
|
|
||||||
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
|
||||||
const REGEX_LOCAL_MATRIXTO = /^#\/room\/([\#\!][^\/]*)\/(\$[^\/]*)$/;
|
|
||||||
|
|
||||||
export default class Quote extends React.Component {
|
|
||||||
static isMessageUrl(url) {
|
|
||||||
return !!REGEX_LOCAL_MATRIXTO.exec(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
static childContextTypes = {
|
|
||||||
matrixClient: PropTypes.object,
|
|
||||||
addRichQuote: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
// The matrix.to url of the event
|
|
||||||
url: PropTypes.string,
|
|
||||||
// The original node that was rendered
|
|
||||||
node: PropTypes.instanceOf(Element),
|
|
||||||
// The parent event
|
|
||||||
parentEv: PropTypes.instanceOf(MatrixEvent),
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
// The event related to this quote and their nested rich quotes
|
|
||||||
events: [],
|
|
||||||
// Whether the top (oldest) event should be shown or spoilered
|
|
||||||
show: true,
|
|
||||||
// Whether an error was encountered fetching nested older event, show node if it does
|
|
||||||
err: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.onQuoteClick = this.onQuoteClick.bind(this);
|
|
||||||
this.addRichQuote = this.addRichQuote.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
getChildContext() {
|
|
||||||
return {
|
|
||||||
matrixClient: MatrixClientPeg.get(),
|
|
||||||
addRichQuote: this.addRichQuote,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
parseUrl(url) {
|
|
||||||
if (!url) return;
|
|
||||||
|
|
||||||
// Default to the empty array if no match for simplicity
|
|
||||||
// resource and prefix will be undefined instead of throwing
|
|
||||||
const matrixToMatch = REGEX_LOCAL_MATRIXTO.exec(url) || [];
|
|
||||||
|
|
||||||
const [, roomIdentifier, eventId] = matrixToMatch;
|
|
||||||
return {roomIdentifier, eventId};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
const {roomIdentifier, eventId} = this.parseUrl(nextProps.url);
|
|
||||||
if (!roomIdentifier || !eventId) return;
|
|
||||||
|
|
||||||
const room = this.getRoom(roomIdentifier);
|
|
||||||
if (!room) return;
|
|
||||||
|
|
||||||
// Only try and load the event if we know about the room
|
|
||||||
// otherwise we just leave a `Quote` anchor which can be used to navigate/join the room manually.
|
|
||||||
this.setState({ events: [] });
|
|
||||||
if (room) this.getEvent(room, eventId, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
this.componentWillReceiveProps(this.props);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRoom(id) {
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
if (id[0] === '!') return cli.getRoom(id);
|
|
||||||
|
|
||||||
return cli.getRooms().find((r) => {
|
|
||||||
return r.getAliases().includes(id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEvent(room, eventId, show) {
|
|
||||||
const event = room.findEventById(eventId);
|
|
||||||
if (event) {
|
|
||||||
this.addEvent(event, show);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await MatrixClientPeg.get().getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
|
|
||||||
this.addEvent(room.findEventById(eventId), show);
|
|
||||||
}
|
|
||||||
|
|
||||||
addEvent(event, show) {
|
|
||||||
const events = [event].concat(this.state.events);
|
|
||||||
this.setState({events, show});
|
|
||||||
}
|
|
||||||
|
|
||||||
// addRichQuote(roomId, eventId) {
|
|
||||||
addRichQuote(href) {
|
|
||||||
const {roomIdentifier, eventId} = this.parseUrl(href);
|
|
||||||
if (!roomIdentifier || !eventId) {
|
|
||||||
this.setState({ err: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const room = this.getRoom(roomIdentifier);
|
|
||||||
if (!room) {
|
|
||||||
this.setState({ err: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getEvent(room, eventId, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
onQuoteClick() {
|
|
||||||
this.setState({ show: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const events = this.state.events.slice();
|
|
||||||
if (events.length) {
|
|
||||||
const evTiles = [];
|
|
||||||
|
|
||||||
if (!this.state.show) {
|
|
||||||
const oldestEv = events.shift();
|
|
||||||
const Pill = sdk.getComponent('elements.Pill');
|
|
||||||
const room = MatrixClientPeg.get().getRoom(oldestEv.getRoomId());
|
|
||||||
|
|
||||||
evTiles.push(<blockquote className="mx_Quote" key="load">
|
|
||||||
{
|
|
||||||
_t('<a>In reply to</a> <pill>', {}, {
|
|
||||||
'a': (sub) => <a onClick={this.onQuoteClick} className="mx_Quote_show">{ sub }</a>,
|
|
||||||
'pill': <Pill type={Pill.TYPE_USER_MENTION} room={room}
|
|
||||||
url={makeUserPermalink(oldestEv.getSender())} shouldShowPillAvatar={true} />,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</blockquote>);
|
|
||||||
}
|
|
||||||
|
|
||||||
const EventTile = sdk.getComponent('views.rooms.EventTile');
|
|
||||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
|
||||||
events.forEach((ev) => {
|
|
||||||
let dateSep = null;
|
|
||||||
|
|
||||||
if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
|
|
||||||
dateSep = <a href={this.props.url}><DateSeparator ts={ev.getTs()} /></a>;
|
|
||||||
}
|
|
||||||
|
|
||||||
evTiles.push(<blockquote className="mx_Quote" key={ev.getId()}>
|
|
||||||
{ dateSep }
|
|
||||||
<EventTile mxEvent={ev} tileShape="quote" />
|
|
||||||
</blockquote>);
|
|
||||||
});
|
|
||||||
|
|
||||||
return <div>{ evTiles }</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deliberately render nothing if the URL isn't recognised
|
|
||||||
// in case we get an undefined/falsey node, replace it with null to make React happy
|
|
||||||
return this.props.node || null;
|
|
||||||
}
|
|
||||||
}
|
|
306
src/components/views/elements/ReplyThread.js
Normal file
306
src/components/views/elements/ReplyThread.js
Normal file
|
@ -0,0 +1,306 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 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.
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import {_t} from '../../../languageHandler';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import dis from '../../../dispatcher';
|
||||||
|
import {wantsDateSeparator} from '../../../DateUtils';
|
||||||
|
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
|
||||||
|
import {makeEventPermalink, makeUserPermalink} from "../../../matrix-to";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
|
// This component does no cycle detection, simply because the only way to make such a cycle would be to
|
||||||
|
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
|
||||||
|
// be low as each event being loaded (after the first) is triggered by an explicit user action.
|
||||||
|
export default class ReplyThread extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
// the latest event in this chain of replies
|
||||||
|
parentEv: PropTypes.instanceOf(MatrixEvent),
|
||||||
|
// called when the ReplyThread contents has changed, including EventTiles thereof
|
||||||
|
onWidgetLoad: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
// The loaded events to be rendered as linear-replies
|
||||||
|
events: [],
|
||||||
|
|
||||||
|
// The latest loaded event which has not yet been shown
|
||||||
|
loadedEv: null,
|
||||||
|
// Whether the component is still loading more events
|
||||||
|
loading: true,
|
||||||
|
|
||||||
|
// Whether as error was encountered fetching a replied to event.
|
||||||
|
err: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.onQuoteClick = this.onQuoteClick.bind(this);
|
||||||
|
this.canCollapse = this.canCollapse.bind(this);
|
||||||
|
this.collapse = this.collapse.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getParentEventId(ev) {
|
||||||
|
if (!ev || ev.isRedacted()) return;
|
||||||
|
|
||||||
|
const mRelatesTo = ev.getWireContent()['m.relates_to'];
|
||||||
|
if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
|
||||||
|
const mInReplyTo = mRelatesTo['m.in_reply_to'];
|
||||||
|
if (mInReplyTo && mInReplyTo['event_id']) return mInReplyTo['event_id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part of Replies fallback support
|
||||||
|
static stripPlainReply(body) {
|
||||||
|
// Removes lines beginning with `> ` until you reach one that doesn't.
|
||||||
|
const lines = body.split('\n');
|
||||||
|
while (lines.length && lines[0].startsWith('> ')) lines.shift();
|
||||||
|
// Reply fallback has a blank line after it, so remove it to prevent leading newline
|
||||||
|
if (lines[0] === '') lines.shift();
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part of Replies fallback support
|
||||||
|
static stripHTMLReply(html) {
|
||||||
|
return html.replace(/^<blockquote data-mx-reply>[\s\S]+?<!--end-mx-reply--><\/blockquote>/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part of Replies fallback support
|
||||||
|
static getNestedReplyText(ev) {
|
||||||
|
if (!ev) return null;
|
||||||
|
|
||||||
|
let {body, formatted_body: html} = ev.getContent();
|
||||||
|
if (this.getParentEventId(ev)) {
|
||||||
|
if (body) body = this.stripPlainReply(body);
|
||||||
|
if (html) html = this.stripHTMLReply(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
const evLink = makeEventPermalink(ev.getRoomId(), ev.getId());
|
||||||
|
const userLink = makeUserPermalink(ev.getSender());
|
||||||
|
const mxid = ev.getSender();
|
||||||
|
|
||||||
|
// This fallback contains text that is explicitly EN.
|
||||||
|
switch (ev.getContent().msgtype) {
|
||||||
|
case 'm.text':
|
||||||
|
case 'm.notice': {
|
||||||
|
html = `<blockquote data-mx-reply><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
|
||||||
|
+ `<br>${html || body}<!--end-mx-reply--></blockquote>`;
|
||||||
|
const lines = body.trim().split('\n');
|
||||||
|
if (lines.length > 0) {
|
||||||
|
lines[0] = `<${mxid}> ${lines[0]}`;
|
||||||
|
body = lines.map((line) => `> ${line}`).join('\n') + '\n\n';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'm.image':
|
||||||
|
html = `<blockquote data-mx-reply><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
|
||||||
|
+ `<br>sent an image.<!--end-mx-reply--></blockquote>`;
|
||||||
|
body = `> <${mxid}> sent an image.\n\n`;
|
||||||
|
break;
|
||||||
|
case 'm.video':
|
||||||
|
html = `<blockquote data-mx-reply><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
|
||||||
|
+ `<br>sent a video.<!--end-mx-reply--></blockquote>`;
|
||||||
|
body = `> <${mxid}> sent a video.\n\n`;
|
||||||
|
break;
|
||||||
|
case 'm.audio':
|
||||||
|
html = `<blockquote data-mx-reply><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
|
||||||
|
+ `<br>sent an audio file.<!--end-mx-reply--></blockquote>`;
|
||||||
|
body = `> <${mxid}> sent an audio file.\n\n`;
|
||||||
|
break;
|
||||||
|
case 'm.file':
|
||||||
|
html = `<blockquote data-mx-reply><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
|
||||||
|
+ `<br>sent a file.<!--end-mx-reply--></blockquote>`;
|
||||||
|
body = `> <${mxid}> sent a file.\n\n`;
|
||||||
|
break;
|
||||||
|
case 'm.emote': {
|
||||||
|
html = `<blockquote data-mx-reply><a href="${evLink}">In reply to</a> * `
|
||||||
|
+ `<a href="${userLink}">${mxid}</a><br>${html || body}<!--end-mx-reply--></blockquote>`;
|
||||||
|
const lines = body.trim().split('\n');
|
||||||
|
if (lines.length > 0) {
|
||||||
|
lines[0] = `* <${mxid}> ${lines[0]}`;
|
||||||
|
body = lines.map((line) => `> ${line}`).join('\n') + '\n\n';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {body, html};
|
||||||
|
}
|
||||||
|
|
||||||
|
static makeReplyMixIn(ev) {
|
||||||
|
if (!ev) return {};
|
||||||
|
return {
|
||||||
|
'm.relates_to': {
|
||||||
|
'm.in_reply_to': {
|
||||||
|
'event_id': ev.getId(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static makeThread(parentEv, onWidgetLoad, ref) {
|
||||||
|
if (!SettingsStore.isFeatureEnabled("feature_rich_quoting") || !ReplyThread.getParentEventId(parentEv)) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
return <ReplyThread parentEv={parentEv} onWidgetLoad={onWidgetLoad} ref={ref} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.unmounted = false;
|
||||||
|
this.room = this.context.matrixClient.getRoom(this.props.parentEv.getRoomId());
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.props.onWidgetLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unmounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
const {parentEv} = this.props;
|
||||||
|
// at time of making this component we checked that props.parentEv has a parentEventId
|
||||||
|
const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv));
|
||||||
|
if (this.unmounted) return;
|
||||||
|
|
||||||
|
if (ev) {
|
||||||
|
this.setState({
|
||||||
|
events: [ev],
|
||||||
|
}, this.loadNextEvent);
|
||||||
|
} else {
|
||||||
|
this.setState({err: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadNextEvent() {
|
||||||
|
if (this.unmounted) return;
|
||||||
|
const ev = this.state.events[0];
|
||||||
|
const inReplyToEventId = ReplyThread.getParentEventId(ev);
|
||||||
|
|
||||||
|
if (!inReplyToEventId) {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadedEv = await this.getEvent(inReplyToEventId);
|
||||||
|
if (this.unmounted) return;
|
||||||
|
|
||||||
|
if (loadedEv) {
|
||||||
|
this.setState({loadedEv});
|
||||||
|
} else {
|
||||||
|
this.setState({err: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEvent(eventId) {
|
||||||
|
const event = this.room.findEventById(eventId);
|
||||||
|
if (event) return event;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ask the client to fetch the event we want using the context API, only interface to do so is to ask
|
||||||
|
// for a timeline with that event, but once it is loaded we can use findEventById to look up the ev map
|
||||||
|
await this.context.matrixClient.getEventTimeline(this.room.getUnfilteredTimelineSet(), eventId);
|
||||||
|
} catch (e) {
|
||||||
|
// if it fails catch the error and return early, there's no point trying to find the event in this case.
|
||||||
|
// Return null as it is falsey and thus should be treated as an error (as the event cannot be resolved).
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.room.findEventById(eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
canCollapse() {
|
||||||
|
return this.state.events.length > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
collapse() {
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
onQuoteClick() {
|
||||||
|
const events = [this.state.loadedEv, ...this.state.events];
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
loadedEv: null,
|
||||||
|
events,
|
||||||
|
}, this.loadNextEvent);
|
||||||
|
|
||||||
|
dis.dispatch({action: 'focus_composer'});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let header = null;
|
||||||
|
|
||||||
|
if (this.state.err) {
|
||||||
|
header = <blockquote className="mx_ReplyThread mx_ReplyThread_error">
|
||||||
|
{
|
||||||
|
_t('Unable to load event that was replied to, ' +
|
||||||
|
'it either does not exist or you do not have permission to view it.')
|
||||||
|
}
|
||||||
|
</blockquote>;
|
||||||
|
} else if (this.state.loadedEv) {
|
||||||
|
const ev = this.state.loadedEv;
|
||||||
|
const Pill = sdk.getComponent('elements.Pill');
|
||||||
|
const room = this.context.matrixClient.getRoom(ev.getRoomId());
|
||||||
|
header = <blockquote className="mx_ReplyThread">
|
||||||
|
{
|
||||||
|
_t('<a>In reply to</a> <pill>', {}, {
|
||||||
|
'a': (sub) => <a onClick={this.onQuoteClick} className="mx_ReplyThread_show">{ sub }</a>,
|
||||||
|
'pill': <Pill type={Pill.TYPE_USER_MENTION} room={room}
|
||||||
|
url={makeUserPermalink(ev.getSender())} shouldShowPillAvatar={true} />,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</blockquote>;
|
||||||
|
} else if (this.state.loading) {
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
header = <Spinner w={16} h={16} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EventTile = sdk.getComponent('views.rooms.EventTile');
|
||||||
|
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
|
const evTiles = this.state.events.map((ev) => {
|
||||||
|
let dateSep = null;
|
||||||
|
|
||||||
|
if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
|
||||||
|
dateSep = <a href={this.props.url}><DateSeparator ts={ev.getTs()} /></a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <blockquote className="mx_ReplyThread" key={ev.getId()}>
|
||||||
|
{ dateSep }
|
||||||
|
<EventTile mxEvent={ev}
|
||||||
|
tileShape="reply"
|
||||||
|
onWidgetLoad={this.props.onWidgetLoad}
|
||||||
|
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
|
||||||
|
</blockquote>;
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<div>{ header }</div>
|
||||||
|
<div>{ evTiles }</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
|
||||||
import ContextualMenu from '../../structures/ContextualMenu';
|
import ContextualMenu from '../../structures/ContextualMenu';
|
||||||
|
|
||||||
import FlairStore from '../../../stores/FlairStore';
|
import FlairStore from '../../../stores/FlairStore';
|
||||||
|
import GroupStore from '../../../stores/GroupStore';
|
||||||
|
|
||||||
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
|
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
|
||||||
// a thing to click on for the user to filter the visible rooms in the RoomList to:
|
// a thing to click on for the user to filter the visible rooms in the RoomList to:
|
||||||
|
@ -57,6 +58,8 @@ export default React.createClass({
|
||||||
if (this.props.tag[0] === '+') {
|
if (this.props.tag[0] === '+') {
|
||||||
FlairStore.addListener('updateGroupProfile', this._onFlairStoreUpdated);
|
FlairStore.addListener('updateGroupProfile', this._onFlairStoreUpdated);
|
||||||
this._onFlairStoreUpdated();
|
this._onFlairStoreUpdated();
|
||||||
|
// New rooms or members may have been added to the group, fetch async
|
||||||
|
this._refreshGroup(this.props.tag);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -80,6 +83,11 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_refreshGroup(groupId) {
|
||||||
|
GroupStore.refreshGroupRooms(groupId);
|
||||||
|
GroupStore.refreshGroupMembers(groupId);
|
||||||
|
},
|
||||||
|
|
||||||
onClick: function(e) {
|
onClick: function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -89,6 +97,10 @@ export default React.createClass({
|
||||||
ctrlOrCmdKey: isOnlyCtrlOrCmdIgnoreShiftKeyEvent(e),
|
ctrlOrCmdKey: isOnlyCtrlOrCmdIgnoreShiftKeyEvent(e),
|
||||||
shiftKey: e.shiftKey,
|
shiftKey: e.shiftKey,
|
||||||
});
|
});
|
||||||
|
if (this.props.tag[0] === '+') {
|
||||||
|
// New rooms or members may have been added to the group, fetch async
|
||||||
|
this._refreshGroup(this.props.tag);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onContextButtonClick: function(e) {
|
onContextButtonClick: function(e) {
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TintableSvg from './TintableSvg';
|
import TintableSvg from './TintableSvg';
|
||||||
|
import AccessibleButton from './AccessibleButton';
|
||||||
|
|
||||||
export default class TintableSvgButton extends React.Component {
|
export default class TintableSvgButton extends React.Component {
|
||||||
|
|
||||||
|
@ -39,9 +40,11 @@ export default class TintableSvgButton extends React.Component {
|
||||||
width={this.props.width}
|
width={this.props.width}
|
||||||
height={this.props.height}
|
height={this.props.height}
|
||||||
></TintableSvg>
|
></TintableSvg>
|
||||||
<span
|
<AccessibleButton
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
element='span'
|
||||||
title={this.props.title}
|
title={this.props.title}
|
||||||
onClick={this.props.onClick} />
|
/>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ import Modal from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { GroupMemberType } from '../../../groups';
|
import { GroupMemberType } from '../../../groups';
|
||||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
import GroupStore from '../../../stores/GroupStore';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -47,33 +47,37 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
this._unmounted = false;
|
||||||
this._initGroupStore(this.props.groupId);
|
this._initGroupStore(this.props.groupId);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps(newProps) {
|
componentWillReceiveProps(newProps) {
|
||||||
if (newProps.groupId !== this.props.groupId) {
|
if (newProps.groupId !== this.props.groupId) {
|
||||||
this._unregisterGroupStore();
|
this._unregisterGroupStore(this.props.groupId);
|
||||||
this._initGroupStore(newProps.groupId);
|
this._initGroupStore(newProps.groupId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_initGroupStore(groupId) {
|
componentWillUnmount() {
|
||||||
this._groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
|
this._unmounted = true;
|
||||||
this._groupStore.registerListener(this.onGroupStoreUpdated);
|
this._unregisterGroupStore(this.props.groupId);
|
||||||
},
|
},
|
||||||
|
|
||||||
_unregisterGroupStore() {
|
_initGroupStore(groupId) {
|
||||||
if (this._groupStore) {
|
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
|
||||||
this._groupStore.unregisterListener(this.onGroupStoreUpdated);
|
},
|
||||||
}
|
|
||||||
|
_unregisterGroupStore(groupId) {
|
||||||
|
GroupStore.unregisterListener(this.onGroupStoreUpdated);
|
||||||
},
|
},
|
||||||
|
|
||||||
onGroupStoreUpdated: function() {
|
onGroupStoreUpdated: function() {
|
||||||
|
if (this._unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
isUserInvited: this._groupStore.getGroupInvitedMembers().some(
|
isUserInvited: GroupStore.getGroupInvitedMembers(this.props.groupId).some(
|
||||||
(m) => m.userId === this.props.groupMember.userId,
|
(m) => m.userId === this.props.groupMember.userId,
|
||||||
),
|
),
|
||||||
isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(),
|
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
import GroupStore from '../../../stores/GroupStore';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||||
|
@ -42,9 +42,12 @@ export default React.createClass({
|
||||||
this._initGroupStore(this.props.groupId);
|
this._initGroupStore(this.props.groupId);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
this._unmounted = true;
|
||||||
|
},
|
||||||
|
|
||||||
_initGroupStore: function(groupId) {
|
_initGroupStore: function(groupId) {
|
||||||
this._groupStore = GroupStoreCache.getGroupStore(groupId);
|
GroupStore.registerListener(groupId, () => {
|
||||||
this._groupStore.registerListener(() => {
|
|
||||||
this._fetchMembers();
|
this._fetchMembers();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -52,8 +55,8 @@ export default React.createClass({
|
||||||
_fetchMembers: function() {
|
_fetchMembers: function() {
|
||||||
if (this._unmounted) return;
|
if (this._unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
members: this._groupStore.getGroupMembers(),
|
members: GroupStore.getGroupMembers(this.props.groupId),
|
||||||
invitedMembers: this._groupStore.getGroupInvitedMembers(),
|
invitedMembers: GroupStore.getGroupInvitedMembers(this.props.groupId),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
|
||||||
import GroupStore from '../../../stores/GroupStore';
|
import GroupStore from '../../../stores/GroupStore';
|
||||||
import { _t } from '../../../languageHandler.js';
|
import { _t } from '../../../languageHandler.js';
|
||||||
|
|
||||||
|
@ -41,15 +40,18 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_initGroupStore: function(groupId) {
|
_initGroupStore: function(groupId) {
|
||||||
this._groupStore = GroupStoreCache.getGroupStore(groupId);
|
this._groupStoreToken = GroupStore.registerListener(groupId, () => {
|
||||||
this._groupStore.registerListener(() => {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isGroupPublicised: this._groupStore.getGroupPublicity(),
|
isGroupPublicised: GroupStore.getGroupPublicity(groupId),
|
||||||
ready: this._groupStore.isStateReady(GroupStore.STATE_KEY.Summary),
|
ready: GroupStore.isStateReady(groupId, GroupStore.STATE_KEY.Summary),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this._groupStoreToken) this._groupStoreToken.unregister();
|
||||||
|
},
|
||||||
|
|
||||||
_onPublicityToggle: function(e) {
|
_onPublicityToggle: function(e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -57,7 +59,7 @@ export default React.createClass({
|
||||||
// Optimistic early update
|
// Optimistic early update
|
||||||
isGroupPublicised: !this.state.isGroupPublicised,
|
isGroupPublicised: !this.state.isGroupPublicised,
|
||||||
});
|
});
|
||||||
this._groupStore.setGroupPublicity(!this.state.isGroupPublicised).then(() => {
|
GroupStore.setGroupPublicity(this.props.groupId, !this.state.isGroupPublicised).then(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: false,
|
busy: false,
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,7 +21,7 @@ import dis from '../../../dispatcher';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
import GroupStore from '../../../stores/GroupStore';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'GroupRoomInfo',
|
displayName: 'GroupRoomInfo',
|
||||||
|
@ -50,29 +50,26 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
componentWillReceiveProps(newProps) {
|
componentWillReceiveProps(newProps) {
|
||||||
if (newProps.groupId !== this.props.groupId) {
|
if (newProps.groupId !== this.props.groupId) {
|
||||||
this._unregisterGroupStore();
|
this._unregisterGroupStore(this.props.groupId);
|
||||||
this._initGroupStore(newProps.groupId);
|
this._initGroupStore(newProps.groupId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._unregisterGroupStore();
|
this._unregisterGroupStore(this.props.groupId);
|
||||||
},
|
},
|
||||||
|
|
||||||
_initGroupStore(groupId) {
|
_initGroupStore(groupId) {
|
||||||
this._groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
|
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
|
||||||
this._groupStore.registerListener(this.onGroupStoreUpdated);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_unregisterGroupStore() {
|
_unregisterGroupStore(groupId) {
|
||||||
if (this._groupStore) {
|
GroupStore.unregisterListener(this.onGroupStoreUpdated);
|
||||||
this._groupStore.unregisterListener(this.onGroupStoreUpdated);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_updateGroupRoom() {
|
_updateGroupRoom() {
|
||||||
this.setState({
|
this.setState({
|
||||||
groupRoom: this._groupStore.getGroupRooms().find(
|
groupRoom: GroupStore.getGroupRooms(this.props.groupId).find(
|
||||||
(r) => r.roomId === this.props.groupRoomId,
|
(r) => r.roomId === this.props.groupRoomId,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -80,7 +77,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onGroupStoreUpdated: function() {
|
onGroupStoreUpdated: function() {
|
||||||
this.setState({
|
this.setState({
|
||||||
isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(),
|
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
|
||||||
});
|
});
|
||||||
this._updateGroupRoom();
|
this._updateGroupRoom();
|
||||||
},
|
},
|
||||||
|
@ -100,7 +97,7 @@ module.exports = React.createClass({
|
||||||
this.setState({groupRoomRemoveLoading: true});
|
this.setState({groupRoomRemoveLoading: true});
|
||||||
const groupId = this.props.groupId;
|
const groupId = this.props.groupId;
|
||||||
const roomId = this.props.groupRoomId;
|
const roomId = this.props.groupRoomId;
|
||||||
this._groupStore.removeRoomFromGroup(roomId).then(() => {
|
GroupStore.removeRoomFromGroup(this.props.groupId, roomId).then(() => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "view_group_room_list",
|
action: "view_group_room_list",
|
||||||
});
|
});
|
||||||
|
@ -134,7 +131,7 @@ module.exports = React.createClass({
|
||||||
const groupId = this.props.groupId;
|
const groupId = this.props.groupId;
|
||||||
const roomId = this.props.groupRoomId;
|
const roomId = this.props.groupRoomId;
|
||||||
const roomName = this.state.groupRoom.displayname;
|
const roomName = this.state.groupRoom.displayname;
|
||||||
this._groupStore.updateGroupRoomVisibility(roomId, isPublic).catch((err) => {
|
GroupStore.updateGroupRoomVisibility(this.props.groupId, roomId, isPublic).catch((err) => {
|
||||||
console.error(`Error whilst changing visibility of ${roomId} in ${groupId} to ${isPublic}`, err);
|
console.error(`Error whilst changing visibility of ${roomId} in ${groupId} to ${isPublic}`, err);
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, {
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
import GroupStore from '../../../stores/GroupStore';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const INITIAL_LOAD_NUM_ROOMS = 30;
|
const INITIAL_LOAD_NUM_ROOMS = 30;
|
||||||
|
@ -39,22 +39,31 @@ export default React.createClass({
|
||||||
this._initGroupStore(this.props.groupId);
|
this._initGroupStore(this.props.groupId);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._unmounted = true;
|
||||||
|
this._unregisterGroupStore();
|
||||||
|
},
|
||||||
|
|
||||||
|
_unregisterGroupStore() {
|
||||||
|
GroupStore.unregisterListener(this.onGroupStoreUpdated);
|
||||||
|
},
|
||||||
|
|
||||||
_initGroupStore: function(groupId) {
|
_initGroupStore: function(groupId) {
|
||||||
this._groupStore = GroupStoreCache.getGroupStore(groupId);
|
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
|
||||||
this._groupStore.registerListener(() => {
|
// XXX: This should be more fluxy - let's get the error from GroupStore .getError or something
|
||||||
this._fetchRooms();
|
// XXX: This is also leaked - we should remove it when unmounting
|
||||||
});
|
GroupStore.on('error', (err, errorGroupId) => {
|
||||||
this._groupStore.on('error', (err) => {
|
if (errorGroupId !== groupId) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
rooms: null,
|
rooms: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_fetchRooms: function() {
|
onGroupStoreUpdated: function() {
|
||||||
if (this._unmounted) return;
|
if (this._unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
rooms: this._groupStore.getGroupRooms(),
|
rooms: GroupStore.getGroupRooms(this.props.groupId),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ import React from 'react';
|
||||||
import MFileBody from './MFileBody';
|
import MFileBody from './MFileBody';
|
||||||
|
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
import { decryptFile } from '../../../utils/DecryptFile';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
export default class MAudioBody extends React.Component {
|
export default class MAudioBody extends React.Component {
|
||||||
|
@ -54,7 +54,7 @@ export default class MAudioBody extends React.Component {
|
||||||
let decryptedBlob;
|
let decryptedBlob;
|
||||||
decryptFile(content.file).then(function(blob) {
|
decryptFile(content.file).then(function(blob) {
|
||||||
decryptedBlob = blob;
|
decryptedBlob = blob;
|
||||||
return readBlobAsDataUri(decryptedBlob);
|
return URL.createObjectURL(decryptedBlob);
|
||||||
}).done((url) => {
|
}).done((url) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
decryptedUrl: url,
|
decryptedUrl: url,
|
||||||
|
@ -69,6 +69,12 @@ export default class MAudioBody extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.state.decryptedUrl) {
|
||||||
|
URL.revokeObjectURL(this.state.decryptedUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -99,16 +100,27 @@ Tinter.registerTintable(updateTintedDownloadImage);
|
||||||
// overridable so that people running their own version of the client can
|
// overridable so that people running their own version of the client can
|
||||||
// choose a different renderer.
|
// choose a different renderer.
|
||||||
//
|
//
|
||||||
// To that end the first version of the blob generation will be the following
|
// To that end the current version of the blob generation is the following
|
||||||
// html:
|
// html:
|
||||||
//
|
//
|
||||||
// <html><head><script>
|
// <html><head><script>
|
||||||
// window.onmessage=function(e){eval("("+e.data.code+")")(e)}
|
// var params = window.location.search.substring(1).split('&');
|
||||||
|
// var lockOrigin;
|
||||||
|
// for (var i = 0; i < params.length; ++i) {
|
||||||
|
// var parts = params[i].split('=');
|
||||||
|
// if (parts[0] == 'origin') lockOrigin = decodeURIComponent(parts[1]);
|
||||||
|
// }
|
||||||
|
// window.onmessage=function(e){
|
||||||
|
// if (lockOrigin === undefined || e.origin === lockOrigin) eval("("+e.data.code+")")(e);
|
||||||
|
// }
|
||||||
// </script></head><body></body></html>
|
// </script></head><body></body></html>
|
||||||
//
|
//
|
||||||
// This waits to receive a message event sent using the window.postMessage API.
|
// This waits to receive a message event sent using the window.postMessage API.
|
||||||
// When it receives the event it evals a javascript function in data.code and
|
// When it receives the event it evals a javascript function in data.code and
|
||||||
// runs the function passing the event as an argument.
|
// runs the function passing the event as an argument. This version adds
|
||||||
|
// support for a query parameter controlling the origin from which messages
|
||||||
|
// will be processed as an extra layer of security (note that the default URL
|
||||||
|
// is still 'v1' since it is backwards compatible).
|
||||||
//
|
//
|
||||||
// In particular it means that the rendering function can be written as a
|
// In particular it means that the rendering function can be written as a
|
||||||
// ordinary javascript function which then is turned into a string using
|
// ordinary javascript function which then is turned into a string using
|
||||||
|
@ -325,6 +337,7 @@ module.exports = React.createClass({
|
||||||
if (this.context.appConfig && this.context.appConfig.cross_origin_renderer_url) {
|
if (this.context.appConfig && this.context.appConfig.cross_origin_renderer_url) {
|
||||||
renderer_url = this.context.appConfig.cross_origin_renderer_url;
|
renderer_url = this.context.appConfig.cross_origin_renderer_url;
|
||||||
}
|
}
|
||||||
|
renderer_url += "?origin=" + encodeURIComponent(window.location.origin);
|
||||||
return (
|
return (
|
||||||
<span className="mx_MFileBody">
|
<span className="mx_MFileBody">
|
||||||
<div className="mx_MFileBody_download">
|
<div className="mx_MFileBody_download">
|
||||||
|
@ -348,7 +361,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<span className="mx_MFileBody">
|
<span className="mx_MFileBody">
|
||||||
<div className="mx_MFileBody_download">
|
<div className="mx_MFileBody_download">
|
||||||
<a className="mx_ImageBody_downloadLink" href={contentUrl} download={fileName} target="_blank">
|
<a className="mx_MFileBody_downloadLink" href={contentUrl} download={fileName} target="_blank">
|
||||||
{ fileName }
|
{ fileName }
|
||||||
</a>
|
</a>
|
||||||
<div className="mx_MImageBody_size">
|
<div className="mx_MImageBody_size">
|
||||||
|
|
|
@ -25,7 +25,7 @@ import ImageUtils from '../../../ImageUtils';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
import { decryptFile } from '../../../utils/DecryptFile';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
@ -49,6 +49,8 @@ export default class extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.onAction = this.onAction.bind(this);
|
this.onAction = this.onAction.bind(this);
|
||||||
|
this.onImageError = this.onImageError.bind(this);
|
||||||
|
this.onImageLoad = this.onImageLoad.bind(this);
|
||||||
this.onImageEnter = this.onImageEnter.bind(this);
|
this.onImageEnter = this.onImageEnter.bind(this);
|
||||||
this.onImageLeave = this.onImageLeave.bind(this);
|
this.onImageLeave = this.onImageLeave.bind(this);
|
||||||
this.onClientSync = this.onClientSync.bind(this);
|
this.onClientSync = this.onClientSync.bind(this);
|
||||||
|
@ -70,6 +72,7 @@ export default class extends React.Component {
|
||||||
this.context.matrixClient.on('sync', this.onClientSync);
|
this.context.matrixClient.on('sync', this.onClientSync);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: factor this out and aplpy it to MVideoBody and MAudioBody too!
|
||||||
onClientSync(syncState, prevState) {
|
onClientSync(syncState, prevState) {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
// Consider the client reconnected if there is no error with syncing.
|
// Consider the client reconnected if there is no error with syncing.
|
||||||
|
@ -136,6 +139,11 @@ export default class extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onImageLoad() {
|
||||||
|
this.fixupHeight();
|
||||||
|
this.props.onWidgetLoad();
|
||||||
|
}
|
||||||
|
|
||||||
_getContentUrl() {
|
_getContentUrl() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined) {
|
if (content.file !== undefined) {
|
||||||
|
@ -153,6 +161,15 @@ export default class extends React.Component {
|
||||||
return this.state.decryptedThumbnailUrl;
|
return this.state.decryptedThumbnailUrl;
|
||||||
}
|
}
|
||||||
return this.state.decryptedUrl;
|
return this.state.decryptedUrl;
|
||||||
|
} else if (content.info &&
|
||||||
|
content.info.mimetype == "image/svg+xml" &&
|
||||||
|
content.info.thumbnail_url) {
|
||||||
|
// special case to return client-generated thumbnails for SVGs, if any,
|
||||||
|
// given we deliberately don't thumbnail them serverside to prevent
|
||||||
|
// billion lol attacks and similar
|
||||||
|
return this.context.matrixClient.mxcUrlToHttp(
|
||||||
|
content.info.thumbnail_url, 800, 600,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return this.context.matrixClient.mxcUrlToHttp(content.url, 800, 600);
|
return this.context.matrixClient.mxcUrlToHttp(content.url, 800, 600);
|
||||||
}
|
}
|
||||||
|
@ -160,7 +177,6 @@ export default class extends React.Component {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
this.fixupHeight();
|
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||||
let thumbnailPromise = Promise.resolve(null);
|
let thumbnailPromise = Promise.resolve(null);
|
||||||
|
@ -168,21 +184,20 @@ export default class extends React.Component {
|
||||||
thumbnailPromise = decryptFile(
|
thumbnailPromise = decryptFile(
|
||||||
content.info.thumbnail_file,
|
content.info.thumbnail_file,
|
||||||
).then(function(blob) {
|
).then(function(blob) {
|
||||||
return readBlobAsDataUri(blob);
|
return URL.createObjectURL(blob);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let decryptedBlob;
|
let decryptedBlob;
|
||||||
thumbnailPromise.then((thumbnailUrl) => {
|
thumbnailPromise.then((thumbnailUrl) => {
|
||||||
return decryptFile(content.file).then(function(blob) {
|
return decryptFile(content.file).then(function(blob) {
|
||||||
decryptedBlob = blob;
|
decryptedBlob = blob;
|
||||||
return readBlobAsDataUri(blob);
|
return URL.createObjectURL(blob);
|
||||||
}).then((contentUrl) => {
|
}).then((contentUrl) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
decryptedUrl: contentUrl,
|
decryptedUrl: contentUrl,
|
||||||
decryptedThumbnailUrl: thumbnailUrl,
|
decryptedThumbnailUrl: thumbnailUrl,
|
||||||
decryptedBlob: decryptedBlob,
|
decryptedBlob: decryptedBlob,
|
||||||
});
|
});
|
||||||
this.props.onWidgetLoad();
|
|
||||||
});
|
});
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.warn("Unable to decrypt attachment: ", err);
|
console.warn("Unable to decrypt attachment: ", err);
|
||||||
|
@ -205,6 +220,13 @@ export default class extends React.Component {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
||||||
this._afterComponentWillUnmount();
|
this._afterComponentWillUnmount();
|
||||||
|
|
||||||
|
if (this.state.decryptedUrl) {
|
||||||
|
URL.revokeObjectURL(this.state.decryptedUrl);
|
||||||
|
}
|
||||||
|
if (this.state.decryptedThumbnailUrl) {
|
||||||
|
URL.revokeObjectURL(this.state.decryptedThumbnailUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// To be overridden by subclasses (e.g. MStickerBody) for further
|
// To be overridden by subclasses (e.g. MStickerBody) for further
|
||||||
|
@ -229,7 +251,16 @@ export default class extends React.Component {
|
||||||
const maxHeight = 600; // let images take up as much width as they can so long as the height doesn't exceed 600px.
|
const maxHeight = 600; // let images take up as much width as they can so long as the height doesn't exceed 600px.
|
||||||
// the alternative here would be 600*timelineWidth/800; to scale them down to fit inside a 4:3 bounding box
|
// the alternative here would be 600*timelineWidth/800; to scale them down to fit inside a 4:3 bounding box
|
||||||
|
|
||||||
//console.log("trying to fit image into timelineWidth of " + this.refs.body.offsetWidth + " or " + this.refs.body.clientWidth);
|
// FIXME: this will break on clientside generated thumbnails (as per e2e rooms)
|
||||||
|
// which may well be much smaller than the 800x600 bounding box.
|
||||||
|
|
||||||
|
// FIXME: It will also break really badly for images with broken or missing thumbnails
|
||||||
|
|
||||||
|
// FIXME: Because we don't know what size of thumbnail the server's actually going to send
|
||||||
|
// us, we can't even really layout the page nicely for it. Instead we have to assume
|
||||||
|
// it'll target 800x600 and we'll downsize if needed to make things fit.
|
||||||
|
|
||||||
|
// console.log("trying to fit image into timelineWidth of " + this.refs.body.offsetWidth + " or " + this.refs.body.clientWidth);
|
||||||
let thumbHeight = null;
|
let thumbHeight = null;
|
||||||
if (content.info) {
|
if (content.info) {
|
||||||
thumbHeight = ImageUtils.thumbHeight(content.info.w, content.info.h, timelineWidth, maxHeight);
|
thumbHeight = ImageUtils.thumbHeight(content.info.w, content.info.h, timelineWidth, maxHeight);
|
||||||
|
@ -239,18 +270,22 @@ export default class extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_messageContent(contentUrl, thumbUrl, content) {
|
_messageContent(contentUrl, thumbUrl, content) {
|
||||||
|
const thumbnail = (
|
||||||
|
<a href={contentUrl} onClick={this.onClick}>
|
||||||
|
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
||||||
|
alt={content.body}
|
||||||
|
onError={this.onImageError}
|
||||||
|
onLoad={this.onImageLoad}
|
||||||
|
onMouseEnter={this.onImageEnter}
|
||||||
|
onMouseLeave={this.onImageLeave} />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="mx_MImageBody" ref="body">
|
<span className="mx_MImageBody" ref="body">
|
||||||
<a href={contentUrl} onClick={this.onClick}>
|
{ thumbUrl && !this.state.imgError ? thumbnail : '' }
|
||||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
|
||||||
alt={content.body}
|
|
||||||
onError={this.onImageError}
|
|
||||||
onLoad={this.props.onWidgetLoad}
|
|
||||||
onMouseEnter={this.onImageEnter}
|
|
||||||
onMouseLeave={this.onImageLeave} />
|
|
||||||
</a>
|
|
||||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,14 +320,6 @@ export default class extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.imgError) {
|
|
||||||
return (
|
|
||||||
<span className="mx_MImageBody">
|
|
||||||
{ _t("This image cannot be displayed.") }
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentUrl = this._getContentUrl();
|
const contentUrl = this._getContentUrl();
|
||||||
let thumbUrl;
|
let thumbUrl;
|
||||||
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||||
|
@ -301,20 +328,6 @@ export default class extends React.Component {
|
||||||
thumbUrl = this._getThumbUrl();
|
thumbUrl = this._getThumbUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (thumbUrl) {
|
return this._messageContent(contentUrl, thumbUrl, content);
|
||||||
return this._messageContent(contentUrl, thumbUrl, content);
|
|
||||||
} else if (content.body) {
|
|
||||||
return (
|
|
||||||
<span className="mx_MImageBody">
|
|
||||||
{ _t("Image '%(Body)s' cannot be displayed.", {Body: content.body}) }
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<span className="mx_MImageBody">
|
|
||||||
{ _t("This image cannot be displayed.") }
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import MFileBody from './MFileBody';
|
import MFileBody from './MFileBody';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
import { decryptFile } from '../../../utils/DecryptFile';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
@ -94,14 +94,14 @@ module.exports = React.createClass({
|
||||||
thumbnailPromise = decryptFile(
|
thumbnailPromise = decryptFile(
|
||||||
content.info.thumbnail_file,
|
content.info.thumbnail_file,
|
||||||
).then(function(blob) {
|
).then(function(blob) {
|
||||||
return readBlobAsDataUri(blob);
|
return URL.createObjectURL(blob);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let decryptedBlob;
|
let decryptedBlob;
|
||||||
thumbnailPromise.then((thumbnailUrl) => {
|
thumbnailPromise.then((thumbnailUrl) => {
|
||||||
return decryptFile(content.file).then(function(blob) {
|
return decryptFile(content.file).then(function(blob) {
|
||||||
decryptedBlob = blob;
|
decryptedBlob = blob;
|
||||||
return readBlobAsDataUri(blob);
|
return URL.createObjectURL(blob);
|
||||||
}).then((contentUrl) => {
|
}).then((contentUrl) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
decryptedUrl: contentUrl,
|
decryptedUrl: contentUrl,
|
||||||
|
@ -120,6 +120,15 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
if (this.state.decryptedUrl) {
|
||||||
|
URL.revokeObjectURL(this.state.decryptedUrl);
|
||||||
|
}
|
||||||
|
if (this.state.decryptedThumbnailUrl) {
|
||||||
|
URL.revokeObjectURL(this.state.decryptedThumbnailUrl);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import ContextualMenu from '../../structures/ContextualMenu';
|
import ContextualMenu from '../../structures/ContextualMenu';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
||||||
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
|
@ -61,10 +62,6 @@ module.exports = React.createClass({
|
||||||
tileShape: PropTypes.string,
|
tileShape: PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
contextTypes: {
|
|
||||||
addRichQuote: PropTypes.func,
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
// the URLs (if any) to be previewed with a LinkPreviewWidget
|
// the URLs (if any) to be previewed with a LinkPreviewWidget
|
||||||
|
@ -186,7 +183,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// If the link is a (localised) matrix.to link, replace it with a pill
|
// If the link is a (localised) matrix.to link, replace it with a pill
|
||||||
const Pill = sdk.getComponent('elements.Pill');
|
const Pill = sdk.getComponent('elements.Pill');
|
||||||
const Quote = sdk.getComponent('elements.Quote');
|
|
||||||
if (Pill.isMessagePillUrl(href)) {
|
if (Pill.isMessagePillUrl(href)) {
|
||||||
const pillContainer = document.createElement('span');
|
const pillContainer = document.createElement('span');
|
||||||
|
|
||||||
|
@ -205,21 +201,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// update the current node with one that's now taken its place
|
// update the current node with one that's now taken its place
|
||||||
node = pillContainer;
|
node = pillContainer;
|
||||||
} else if (SettingsStore.isFeatureEnabled("feature_rich_quoting") && Quote.isMessageUrl(href)) {
|
|
||||||
if (this.context.addRichQuote) { // We're already a Rich Quote so just append the next one above
|
|
||||||
this.context.addRichQuote(href);
|
|
||||||
node.remove();
|
|
||||||
} else { // We're the first in the chain
|
|
||||||
const quoteContainer = document.createElement('span');
|
|
||||||
|
|
||||||
const quote =
|
|
||||||
<Quote url={href} parentEv={this.props.mxEvent} node={node} />;
|
|
||||||
|
|
||||||
ReactDOM.render(quote, quoteContainer);
|
|
||||||
node.parentNode.replaceChild(quoteContainer, node);
|
|
||||||
node = quoteContainer;
|
|
||||||
}
|
|
||||||
pillified = true;
|
|
||||||
}
|
}
|
||||||
} else if (node.nodeType == Node.TEXT_NODE) {
|
} else if (node.nodeType == Node.TEXT_NODE) {
|
||||||
const Pill = sdk.getComponent('elements.Pill');
|
const Pill = sdk.getComponent('elements.Pill');
|
||||||
|
@ -441,8 +422,12 @@ module.exports = React.createClass({
|
||||||
const mxEvent = this.props.mxEvent;
|
const mxEvent = this.props.mxEvent;
|
||||||
const content = mxEvent.getContent();
|
const content = mxEvent.getContent();
|
||||||
|
|
||||||
|
const stripReply = SettingsStore.isFeatureEnabled("feature_rich_quoting") &&
|
||||||
|
ReplyThread.getParentEventId(mxEvent);
|
||||||
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
|
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
|
||||||
disableBigEmoji: SettingsStore.getValue('TextualBody.disableBigEmoji'),
|
disableBigEmoji: SettingsStore.getValue('TextualBody.disableBigEmoji'),
|
||||||
|
// Part of Replies fallback support
|
||||||
|
stripReplyFallback: stripReply,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.props.highlightLink) {
|
if (this.props.highlightLink) {
|
||||||
|
|
|
@ -18,6 +18,8 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
const classNames = require("classnames");
|
const classNames = require("classnames");
|
||||||
|
@ -31,6 +33,7 @@ import withMatrixClient from '../../../wrappers/withMatrixClient';
|
||||||
const ContextualMenu = require('../../structures/ContextualMenu');
|
const ContextualMenu = require('../../structures/ContextualMenu');
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import {makeEventPermalink} from "../../../matrix-to";
|
import {makeEventPermalink} from "../../../matrix-to";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
const ObjectUtils = require('../../../ObjectUtils');
|
const ObjectUtils = require('../../../ObjectUtils');
|
||||||
|
|
||||||
|
@ -152,6 +155,13 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
isTwelveHour: PropTypes.bool,
|
isTwelveHour: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
// no-op function because onWidgetLoad is optional yet some sub-components assume its existence
|
||||||
|
onWidgetLoad: function() {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
// Whether the context menu is being displayed.
|
// Whether the context menu is being displayed.
|
||||||
|
@ -300,12 +310,16 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
const x = buttonRect.right + window.pageXOffset;
|
const x = buttonRect.right + window.pageXOffset;
|
||||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
|
const {tile, replyThread} = this.refs;
|
||||||
|
|
||||||
ContextualMenu.createMenu(MessageContextMenu, {
|
ContextualMenu.createMenu(MessageContextMenu, {
|
||||||
chevronOffset: 10,
|
chevronOffset: 10,
|
||||||
mxEvent: this.props.mxEvent,
|
mxEvent: this.props.mxEvent,
|
||||||
left: x,
|
left: x,
|
||||||
top: y,
|
top: y,
|
||||||
eventTileOps: this.refs.tile && this.refs.tile.getEventTileOps ? this.refs.tile.getEventTileOps() : undefined,
|
eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
|
||||||
|
collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
|
||||||
onFinished: function() {
|
onFinished: function() {
|
||||||
self.setState({menu: false});
|
self.setState({menu: false});
|
||||||
},
|
},
|
||||||
|
@ -542,7 +556,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
|
|
||||||
if (needsSenderProfile) {
|
if (needsSenderProfile) {
|
||||||
let text = null;
|
let text = null;
|
||||||
if (!this.props.tileShape || this.props.tileShape === 'quote') {
|
if (!this.props.tileShape) {
|
||||||
if (msgtype === 'm.image') text = _td('%(senderName)s sent an image');
|
if (msgtype === 'm.image') text = _td('%(senderName)s sent an image');
|
||||||
else if (msgtype === 'm.video') text = _td('%(senderName)s sent a video');
|
else if (msgtype === 'm.video') text = _td('%(senderName)s sent a video');
|
||||||
else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file');
|
else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file');
|
||||||
|
@ -646,18 +660,23 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case 'quote': {
|
|
||||||
|
case 'reply':
|
||||||
|
case 'reply_preview': {
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
{ avatar }
|
{ avatar }
|
||||||
{ sender }
|
{ sender }
|
||||||
<div className="mx_EventTile_line mx_EventTile_quote">
|
<div className="mx_EventTile_reply">
|
||||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||||
{ timestamp }
|
{ timestamp }
|
||||||
</a>
|
</a>
|
||||||
{ this._renderE2EPadlock() }
|
{ this._renderE2EPadlock() }
|
||||||
|
{
|
||||||
|
this.props.tileShape === 'reply_preview'
|
||||||
|
&& ReplyThread.makeThread(this.props.mxEvent, this.props.onWidgetLoad, 'replyThread')
|
||||||
|
}
|
||||||
<EventTileType ref="tile"
|
<EventTileType ref="tile"
|
||||||
tileShape="quote"
|
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
highlights={this.props.highlights}
|
highlights={this.props.highlights}
|
||||||
highlightLink={this.props.highlightLink}
|
highlightLink={this.props.highlightLink}
|
||||||
|
@ -680,6 +699,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
{ timestamp }
|
{ timestamp }
|
||||||
</a>
|
</a>
|
||||||
{ this._renderE2EPadlock() }
|
{ this._renderE2EPadlock() }
|
||||||
|
{ ReplyThread.makeThread(this.props.mxEvent, this.props.onWidgetLoad, 'replyThread') }
|
||||||
<EventTileType ref="tile"
|
<EventTileType ref="tile"
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
highlights={this.props.highlights}
|
highlights={this.props.highlights}
|
||||||
|
@ -742,7 +762,11 @@ function E2ePadlockUnencrypted(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function E2ePadlock(props) {
|
function E2ePadlock(props) {
|
||||||
return <img className="mx_EventTile_e2eIcon" {...props} />;
|
if (SettingsStore.getValue("alwaysShowEncryptionIcons")) {
|
||||||
|
return <img className="mx_EventTile_e2eIcon" {...props} />;
|
||||||
|
} else {
|
||||||
|
return <img className="mx_EventTile_e2eIcon mx_EventTile_e2eIcon_hidden" {...props} />;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.getHandlerTile = getHandlerTile;
|
module.exports.getHandlerTile = getHandlerTile;
|
||||||
|
|
|
@ -111,6 +111,14 @@ export default class MessageComposer extends React.Component {
|
||||||
</li>);
|
</li>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
|
||||||
|
let replyToWarning = null;
|
||||||
|
if (isQuoting) {
|
||||||
|
replyToWarning = <p>{
|
||||||
|
_t('At this time it is not possible to reply with a file so this will be sent without being a reply.')
|
||||||
|
}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
Modal.createTrackedDialog('Upload Files confirmation', '', QuestionDialog, {
|
Modal.createTrackedDialog('Upload Files confirmation', '', QuestionDialog, {
|
||||||
title: _t('Upload Files'),
|
title: _t('Upload Files'),
|
||||||
description: (
|
description: (
|
||||||
|
@ -119,6 +127,7 @@ export default class MessageComposer extends React.Component {
|
||||||
<ul style={{listStyle: 'none', textAlign: 'left'}}>
|
<ul style={{listStyle: 'none', textAlign: 'left'}}>
|
||||||
{ fileList }
|
{ fileList }
|
||||||
</ul>
|
</ul>
|
||||||
|
{ replyToWarning }
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
onFinished: (shouldUpload) => {
|
onFinished: (shouldUpload) => {
|
||||||
|
|
|
@ -58,9 +58,11 @@ const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g')
|
||||||
|
|
||||||
import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
|
import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
|
||||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||||
import {makeEventPermalink, makeUserPermalink} from "../../../matrix-to";
|
import {makeUserPermalink} from "../../../matrix-to";
|
||||||
import QuotePreview from "./QuotePreview";
|
import ReplyPreview from "./ReplyPreview";
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
|
import {ContentHelpers} from 'matrix-js-sdk';
|
||||||
|
|
||||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
||||||
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
||||||
|
@ -263,7 +265,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
let contentState = this.state.editorState.getCurrentContent();
|
let contentState = this.state.editorState.getCurrentContent();
|
||||||
|
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'quote_event':
|
case 'reply_to_event':
|
||||||
case 'focus_composer':
|
case 'focus_composer':
|
||||||
editor.focus();
|
editor.focus();
|
||||||
break;
|
break;
|
||||||
|
@ -760,17 +762,15 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const quotingEv = RoomViewStore.getQuotingEvent();
|
const replyingToEv = RoomViewStore.getQuotingEvent();
|
||||||
|
const mustSendHTML = Boolean(replyingToEv);
|
||||||
|
|
||||||
if (this.state.isRichtextEnabled) {
|
if (this.state.isRichtextEnabled) {
|
||||||
/*
|
/*
|
||||||
// We should only send HTML if any block is styled or contains inline style
|
// We should only send HTML if any block is styled or contains inline style
|
||||||
let shouldSendHTML = false;
|
let shouldSendHTML = false;
|
||||||
|
|
||||||
// If we are quoting we need HTML Content
|
if (mustSendHTML) shouldSendHTML = true;
|
||||||
if (quotingEv) {
|
|
||||||
shouldSendHTML = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blocks = contentState.getBlocksAsArray();
|
const blocks = contentState.getBlocksAsArray();
|
||||||
if (blocks.some((block) => block.getType() !== 'unstyled')) {
|
if (blocks.some((block) => block.getType() !== 'unstyled')) {
|
||||||
|
@ -833,15 +833,15 @@ export default class MessageComposerInput extends React.Component {
|
||||||
*/
|
*/
|
||||||
const md = new Markdown(pt);
|
const md = new Markdown(pt);
|
||||||
// if contains no HTML and we're not quoting (needing HTML)
|
// if contains no HTML and we're not quoting (needing HTML)
|
||||||
if (md.isPlainText() && !quotingEv) {
|
if (md.isPlainText() && !mustSendHTML) {
|
||||||
contentText = md.toPlaintext();
|
contentText = md.toPlaintext();
|
||||||
} else {
|
} else {
|
||||||
contentHTML = md.toHTML();
|
contentHTML = md.toHTML();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let sendHtmlFn = this.client.sendHtmlMessage;
|
let sendHtmlFn = ContentHelpers.makeHtmlMessage;
|
||||||
let sendTextFn = this.client.sendTextMessage;
|
let sendTextFn = ContentHelpers.makeTextMessage;
|
||||||
|
|
||||||
this.historyManager.save(
|
this.historyManager.save(
|
||||||
contentState,
|
contentState,
|
||||||
|
@ -849,45 +849,54 @@ export default class MessageComposerInput extends React.Component {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (contentText.startsWith('/me')) {
|
if (contentText.startsWith('/me')) {
|
||||||
|
if (replyingToEv) {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Emote Reply Fail', '', ErrorDialog, {
|
||||||
|
title: _t("Unable to reply"),
|
||||||
|
description: _t("At this time it is not possible to reply with an emote."),
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
contentText = contentText.substring(4);
|
contentText = contentText.substring(4);
|
||||||
// bit of a hack, but the alternative would be quite complicated
|
// bit of a hack, but the alternative would be quite complicated
|
||||||
if (contentHTML) contentHTML = contentHTML.replace(/\/me ?/, '');
|
if (contentHTML) contentHTML = contentHTML.replace(/\/me ?/, '');
|
||||||
sendHtmlFn = this.client.sendHtmlEmote;
|
sendHtmlFn = ContentHelpers.makeHtmlEmote;
|
||||||
sendTextFn = this.client.sendEmoteMessage;
|
sendTextFn = ContentHelpers.makeEmoteMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (quotingEv) {
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
const room = cli.getRoom(quotingEv.getRoomId());
|
|
||||||
const sender = room.currentState.getMember(quotingEv.getSender());
|
|
||||||
|
|
||||||
const {body/*, formatted_body*/} = quotingEv.getContent();
|
let content = contentHTML ? sendHtmlFn(contentText, contentHTML) : sendTextFn(contentText);
|
||||||
|
|
||||||
const perma = makeEventPermalink(quotingEv.getRoomId(), quotingEv.getId());
|
if (replyingToEv) {
|
||||||
contentText = `${sender.name}:\n> ${body}\n\n${contentText}`;
|
const replyContent = ReplyThread.makeReplyMixIn(replyingToEv);
|
||||||
contentHTML = `<a href="${perma}">Quote<br></a>${contentHTML}`;
|
content = Object.assign(replyContent, content);
|
||||||
|
|
||||||
// we have finished quoting, clear the quotingEvent
|
// Part of Replies fallback support - prepend the text we're sending with the text we're replying to
|
||||||
|
const nestedReply = ReplyThread.getNestedReplyText(replyingToEv);
|
||||||
|
if (nestedReply) {
|
||||||
|
if (content.formatted_body) {
|
||||||
|
content.formatted_body = nestedReply.html + content.formatted_body;
|
||||||
|
}
|
||||||
|
content.body = nestedReply.body + content.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear reply_to_event as we put the message into the queue
|
||||||
|
// if the send fails, retry will handle resending.
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'quote_event',
|
action: 'reply_to_event',
|
||||||
event: null,
|
event: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let sendMessagePromise;
|
|
||||||
if (contentHTML) {
|
|
||||||
sendMessagePromise = sendHtmlFn.call(
|
|
||||||
this.client, this.props.room.roomId, contentText, contentHTML,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
sendMessagePromise = sendTextFn.call(this.client, this.props.room.roomId, contentText);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendMessagePromise.done((res) => {
|
this.client.sendMessage(this.props.room.roomId, content).then((res) => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_sent',
|
action: 'message_sent',
|
||||||
});
|
});
|
||||||
}, (e) => onSendMessageFailed(e, this.props.room));
|
}).catch((e) => {
|
||||||
|
onSendMessageFailed(e, this.props.room);
|
||||||
|
});
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
editorState: this.createEditorState(),
|
editorState: this.createEditorState(),
|
||||||
|
@ -1192,7 +1201,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className="mx_MessageComposer_input_wrapper">
|
<div className="mx_MessageComposer_input_wrapper">
|
||||||
<div className="mx_MessageComposer_autocomplete_wrapper">
|
<div className="mx_MessageComposer_autocomplete_wrapper">
|
||||||
{ SettingsStore.isFeatureEnabled("feature_rich_quoting") && <QuotePreview /> }
|
{ SettingsStore.isFeatureEnabled("feature_rich_quoting") && <ReplyPreview /> }
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
ref={(e) => this.autocomplete = e}
|
ref={(e) => this.autocomplete = e}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
|
|
|
@ -19,15 +19,16 @@ import dis from '../../../dispatcher';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
function cancelQuoting() {
|
function cancelQuoting() {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'quote_event',
|
action: 'reply_to_event',
|
||||||
event: null,
|
event: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class QuotePreview extends React.Component {
|
export default class ReplyPreview extends React.Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
@ -61,17 +62,20 @@ export default class QuotePreview extends React.Component {
|
||||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||||
|
|
||||||
return <div className="mx_QuotePreview">
|
return <div className="mx_ReplyPreview">
|
||||||
<div className="mx_QuotePreview_section">
|
<div className="mx_ReplyPreview_section">
|
||||||
<EmojiText element="div" className="mx_QuotePreview_header mx_QuotePreview_title">
|
<EmojiText element="div" className="mx_ReplyPreview_header mx_ReplyPreview_title">
|
||||||
{ '💬 ' + _t('Replying') }
|
{ '💬 ' + _t('Replying') }
|
||||||
</EmojiText>
|
</EmojiText>
|
||||||
<div className="mx_QuotePreview_header mx_QuotePreview_cancel">
|
<div className="mx_ReplyPreview_header mx_ReplyPreview_cancel">
|
||||||
<img className="mx_filterFlipColor" src="img/cancel.svg" width="18" height="18"
|
<img className="mx_filterFlipColor" src="img/cancel.svg" width="18" height="18"
|
||||||
onClick={cancelQuoting} />
|
onClick={cancelQuoting} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_QuotePreview_clear" />
|
<div className="mx_ReplyPreview_clear" />
|
||||||
<EventTile mxEvent={this.state.event} last={true} tileShape="quote" />
|
<EventTile last={true}
|
||||||
|
tileShape="reply_preview"
|
||||||
|
mxEvent={this.state.event}
|
||||||
|
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
|
@ -30,7 +30,7 @@ import DMRoomMap from '../../../utils/DMRoomMap';
|
||||||
const Receipt = require('../../../utils/Receipt');
|
const Receipt = require('../../../utils/Receipt');
|
||||||
import TagOrderStore from '../../../stores/TagOrderStore';
|
import TagOrderStore from '../../../stores/TagOrderStore';
|
||||||
import RoomListStore from '../../../stores/RoomListStore';
|
import RoomListStore from '../../../stores/RoomListStore';
|
||||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
import GroupStore from '../../../stores/GroupStore';
|
||||||
|
|
||||||
const HIDE_CONFERENCE_CHANS = true;
|
const HIDE_CONFERENCE_CHANS = true;
|
||||||
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
|
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
|
||||||
|
@ -83,8 +83,6 @@ module.exports = React.createClass({
|
||||||
cli.on("Group.myMembership", this._onGroupMyMembership);
|
cli.on("Group.myMembership", this._onGroupMyMembership);
|
||||||
|
|
||||||
const dmRoomMap = DMRoomMap.shared();
|
const dmRoomMap = DMRoomMap.shared();
|
||||||
this._groupStores = {};
|
|
||||||
this._groupStoreTokens = [];
|
|
||||||
// A map between tags which are group IDs and the room IDs of rooms that should be kept
|
// A map between tags which are group IDs and the room IDs of rooms that should be kept
|
||||||
// in the room list when filtering by that tag.
|
// in the room list when filtering by that tag.
|
||||||
this._visibleRoomsForGroup = {
|
this._visibleRoomsForGroup = {
|
||||||
|
@ -93,22 +91,22 @@ module.exports = React.createClass({
|
||||||
// All rooms that should be kept in the room list when filtering.
|
// All rooms that should be kept in the room list when filtering.
|
||||||
// By default, show all rooms.
|
// By default, show all rooms.
|
||||||
this._visibleRooms = MatrixClientPeg.get().getRooms();
|
this._visibleRooms = MatrixClientPeg.get().getRooms();
|
||||||
// When the selected tags are changed, initialise a group store if necessary
|
|
||||||
this._tagStoreToken = TagOrderStore.addListener(() => {
|
// Listen to updates to group data. RoomList cares about members and rooms in order
|
||||||
|
// to filter the room list when group tags are selected.
|
||||||
|
this._groupStoreToken = GroupStore.registerListener(null, () => {
|
||||||
(TagOrderStore.getOrderedTags() || []).forEach((tag) => {
|
(TagOrderStore.getOrderedTags() || []).forEach((tag) => {
|
||||||
if (tag[0] !== '+' || this._groupStores[tag]) {
|
if (tag[0] !== '+') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._groupStores[tag] = GroupStoreCache.getGroupStore(tag);
|
// This group's rooms or members may have updated, update rooms for its tag
|
||||||
this._groupStoreTokens.push(
|
this.updateVisibleRoomsForTag(dmRoomMap, tag);
|
||||||
this._groupStores[tag].registerListener(() => {
|
this.updateVisibleRooms();
|
||||||
// This group's rooms or members may have updated, update rooms for its tag
|
|
||||||
this.updateVisibleRoomsForTag(dmRoomMap, tag);
|
|
||||||
this.updateVisibleRooms();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
// Filters themselves have changed, refresh the selected tags
|
});
|
||||||
|
|
||||||
|
this._tagStoreToken = TagOrderStore.addListener(() => {
|
||||||
|
// Filters themselves have changed
|
||||||
this.updateVisibleRooms();
|
this.updateVisibleRooms();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -183,9 +181,9 @@ module.exports = React.createClass({
|
||||||
this._roomListStoreToken.remove();
|
this._roomListStoreToken.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._groupStoreTokens.length > 0) {
|
// NB: GroupStore is not a Flux.Store
|
||||||
// NB: GroupStore is not a Flux.Store
|
if (this._groupStoreToken) {
|
||||||
this._groupStoreTokens.forEach((token) => token.unregister());
|
this._groupStoreToken.unregister();
|
||||||
}
|
}
|
||||||
|
|
||||||
// cancel any pending calls to the rate_limited_funcs
|
// cancel any pending calls to the rate_limited_funcs
|
||||||
|
@ -259,12 +257,11 @@ module.exports = React.createClass({
|
||||||
updateVisibleRoomsForTag: function(dmRoomMap, tag) {
|
updateVisibleRoomsForTag: function(dmRoomMap, tag) {
|
||||||
if (!this.mounted) return;
|
if (!this.mounted) return;
|
||||||
// For now, only handle group tags
|
// For now, only handle group tags
|
||||||
const store = this._groupStores[tag];
|
if (tag[0] !== '+') return;
|
||||||
if (!store) return;
|
|
||||||
|
|
||||||
this._visibleRoomsForGroup[tag] = [];
|
this._visibleRoomsForGroup[tag] = [];
|
||||||
store.getGroupRooms().forEach((room) => this._visibleRoomsForGroup[tag].push(room.roomId));
|
GroupStore.getGroupRooms(tag).forEach((room) => this._visibleRoomsForGroup[tag].push(room.roomId));
|
||||||
store.getGroupMembers().forEach((member) => {
|
GroupStore.getGroupMembers(tag).forEach((member) => {
|
||||||
if (member.userId === MatrixClientPeg.get().credentials.userId) return;
|
if (member.userId === MatrixClientPeg.get().credentials.userId) return;
|
||||||
dmRoomMap.getDMRoomsForUserId(member.userId).forEach(
|
dmRoomMap.getDMRoomsForUserId(member.userId).forEach(
|
||||||
(roomId) => this._visibleRoomsForGroup[tag].push(roomId),
|
(roomId) => this._visibleRoomsForGroup[tag].push(roomId),
|
||||||
|
|
|
@ -174,6 +174,7 @@ export default class Stickerpicker extends React.Component {
|
||||||
showTitle={false}
|
showTitle={false}
|
||||||
showMinimise={true}
|
showMinimise={true}
|
||||||
showDelete={false}
|
showDelete={false}
|
||||||
|
showPopout={false}
|
||||||
onMinimiseClick={this._onHideStickersClick}
|
onMinimiseClick={this._onHideStickersClick}
|
||||||
handleMinimisePointerEvents={true}
|
handleMinimisePointerEvents={true}
|
||||||
whitelistCapabilities={['m.sticker']}
|
whitelistCapabilities={['m.sticker']}
|
||||||
|
|
|
@ -950,7 +950,6 @@
|
||||||
"Import room keys": "Импортиране на ключове за стая",
|
"Import room keys": "Импортиране на ключове за стая",
|
||||||
"File to import": "Файл за импортиране",
|
"File to import": "Файл за импортиране",
|
||||||
"Import": "Импортирай",
|
"Import": "Импортирай",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Също така записваме всяка страница, която използвате в приложението (в момента <CurrentPageHash>), браузъра, който използвате (<CurrentUserAgent>) и резолюцията на устройството (<CurrentDeviceResolution>).",
|
|
||||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Когато тази страница съдържа информация идентифицираща Вас (като например стая, потребител или идентификатор на група), тези данни биват премахнати преди да бъдат изпратени до сървъра.",
|
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Когато тази страница съдържа информация идентифицираща Вас (като например стая, потребител или идентификатор на група), тези данни биват премахнати преди да бъдат изпратени до сървъра.",
|
||||||
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Има непознати устройства в тази стая. Ако продължите без да ги потвърдите, ще бъде възможно за някого да подслушва Вашия разговор.",
|
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Има непознати устройства в тази стая. Ако продължите без да ги потвърдите, ще бъде възможно за някого да подслушва Вашия разговор.",
|
||||||
"WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "ВНИМАНИЕ: НЕУСПЕШНО ПОТВЪРЖДАВАНЕ НА КЛЮЧА! Ключът за подписване за %(userId)s и устройството %(deviceId)s е \"%(fprint)s\", което не съвпада с предоставения ключ \"%(fingerprint)s\". Това може да означава, че Вашата комуникация е прихваната!",
|
"WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "ВНИМАНИЕ: НЕУСПЕШНО ПОТВЪРЖДАВАНЕ НА КЛЮЧА! Ключът за подписване за %(userId)s и устройството %(deviceId)s е \"%(fprint)s\", което не съвпада с предоставения ключ \"%(fingerprint)s\". Това може да означава, че Вашата комуникация е прихваната!",
|
||||||
|
@ -987,7 +986,7 @@
|
||||||
"%(user)s is a %(userRole)s": "%(user)s е %(userRole)s",
|
"%(user)s is a %(userRole)s": "%(user)s е %(userRole)s",
|
||||||
"Code": "Код",
|
"Code": "Код",
|
||||||
"Debug Logs Submission": "Изпращане на логове за дебъгване",
|
"Debug Logs Submission": "Изпращане на логове за дебъгване",
|
||||||
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Ако сте изпратили грешка чрез GitHub, логовете за дебъгване могат да ни помогнат да проследим проблема. Логовете за дебъгване съдържат данни за използване на приложението, включващи потребителското Ви име, идентификаторите или псевдонимите на стаите или групите, които сте посетили, и потребителските имена на други потребители. Те не съдържат съобщения.",
|
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Ако сте изпратили грешка чрез GitHub, логовете за дебъгване могат да ни помогнат да проследим проблема. Логовете за дебъгване съдържат данни за използване на приложението, включващи потребителското Ви име, идентификаторите или псевдонимите на стаите или групите, които сте посетили, и потребителските имена на други потребители. Те не съдържат съобщения.",
|
||||||
"Submit debug logs": "Изпрати логове за дебъгване",
|
"Submit debug logs": "Изпрати логове за дебъгване",
|
||||||
"Opens the Developer Tools dialog": "Отваря прозорец с инструменти на разработчика",
|
"Opens the Developer Tools dialog": "Отваря прозорец с инструменти на разработчика",
|
||||||
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Видяно от %(displayName)s (%(userName)s) в %(dateTime)s",
|
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Видяно от %(displayName)s (%(userName)s) в %(dateTime)s",
|
||||||
|
@ -1157,5 +1156,7 @@
|
||||||
"Collapse panel": "Свий панела",
|
"Collapse panel": "Свий панела",
|
||||||
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "С текущия Ви браузър, изглеждането и усещането на приложението може да бъде неточно, и някои или всички от функциите може да не функционират,работят......... Ако искате може да продължите така или иначе, но сте сами по отношение на евентуалните проблеми, които може да срещнете!",
|
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "С текущия Ви браузър, изглеждането и усещането на приложението може да бъде неточно, и някои или всички от функциите може да не функционират,работят......... Ако искате може да продължите така или иначе, но сте сами по отношение на евентуалните проблеми, които може да срещнете!",
|
||||||
"Checking for an update...": "Проверяване за нова версия...",
|
"Checking for an update...": "Проверяване за нова версия...",
|
||||||
"There are advanced notifications which are not shown here": "Съществуват разширени настройки за известия, които не са показани тук"
|
"There are advanced notifications which are not shown here": "Съществуват разширени настройки за известия, които не са показани тук",
|
||||||
|
"Missing roomId.": "Липсва идентификатор на стая.",
|
||||||
|
"Picture": "Изображение"
|
||||||
}
|
}
|
||||||
|
|
|
@ -952,7 +952,6 @@
|
||||||
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Nachricht jetzt erneut senden</resendText> oder <cancelText>senden abbrechen</cancelText> now.",
|
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Nachricht jetzt erneut senden</resendText> oder <cancelText>senden abbrechen</cancelText> now.",
|
||||||
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privatsphäre ist uns wichtig, deshalb sammeln wir keine persönlichen oder identifizierbaren Daten für unsere Analysen.",
|
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privatsphäre ist uns wichtig, deshalb sammeln wir keine persönlichen oder identifizierbaren Daten für unsere Analysen.",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "Die Informationen, die an uns gesendet werden um Riot.im zu verbessern enthalten:",
|
"The information being sent to us to help make Riot.im better includes:": "Die Informationen, die an uns gesendet werden um Riot.im zu verbessern enthalten:",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Wir speichern auch jede Seite, die du in der App benutzt (currently <CurrentPageHash>), deinen User Agent (<CurrentUserAgent>) und die Bildschirmauflösung deines Gerätes (<CurrentDeviceResolution>).",
|
|
||||||
"The platform you're on": "Benutzte Plattform",
|
"The platform you're on": "Benutzte Plattform",
|
||||||
"The version of Riot.im": "Riot.im Version",
|
"The version of Riot.im": "Riot.im Version",
|
||||||
"Your language of choice": "Deine ausgewählte Sprache",
|
"Your language of choice": "Deine ausgewählte Sprache",
|
||||||
|
@ -986,7 +985,7 @@
|
||||||
"<requestLink>Re-request encryption keys</requestLink> from your other devices.": "Verschlüsselungs-Schlüssel von deinen anderen Geräten <requestLink>erneut anfragen</requestLink>.",
|
"<requestLink>Re-request encryption keys</requestLink> from your other devices.": "Verschlüsselungs-Schlüssel von deinen anderen Geräten <requestLink>erneut anfragen</requestLink>.",
|
||||||
"%(user)s is a %(userRole)s": "%(user)s ist ein %(userRole)s",
|
"%(user)s is a %(userRole)s": "%(user)s ist ein %(userRole)s",
|
||||||
"Debug Logs Submission": "Einsenden des Fehlerprotokolls",
|
"Debug Logs Submission": "Einsenden des Fehlerprotokolls",
|
||||||
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Wenn du einen Fehler via GitHub gemeldet hast, können Fehlerberichte uns helfen um das Problem zu finden. Sie enthalten Anwendungsdaten wie deinen Nutzernamen, Raum- und Gruppen-ID's und Aliase die du besucht hast und Nutzernamen anderer Nutzer. Sie enthalten keine Nachrichten.",
|
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Wenn du einen Fehler via GitHub gemeldet hast, können Fehlerberichte uns helfen um das Problem zu finden. Sie enthalten Anwendungsdaten wie deinen Nutzernamen, Raum- und Gruppen-ID's und Aliase die du besucht hast und Nutzernamen anderer Nutzer. Sie enthalten keine Nachrichten.",
|
||||||
"Submit debug logs": "Fehlerberichte einreichen",
|
"Submit debug logs": "Fehlerberichte einreichen",
|
||||||
"Code": "Code",
|
"Code": "Code",
|
||||||
"Opens the Developer Tools dialog": "Öffnet den Entwicklerwerkzeugkasten",
|
"Opens the Developer Tools dialog": "Öffnet den Entwicklerwerkzeugkasten",
|
||||||
|
@ -1157,5 +1156,7 @@
|
||||||
"Collapse panel": "Panel einklappen",
|
"Collapse panel": "Panel einklappen",
|
||||||
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "In deinem aktuell verwendeten Browser können Aussehen und Handhabung der Anwendung unter Umständen noch komplett fehlerhaft sein, so dass einige bzw. im Extremfall alle Funktionen nicht zur Verfügung stehen. Du kannst es trotzdem versuchen und fortfahren, bist dabei aber bezüglich aller auftretenden Probleme auf dich allein gestellt!",
|
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "In deinem aktuell verwendeten Browser können Aussehen und Handhabung der Anwendung unter Umständen noch komplett fehlerhaft sein, so dass einige bzw. im Extremfall alle Funktionen nicht zur Verfügung stehen. Du kannst es trotzdem versuchen und fortfahren, bist dabei aber bezüglich aller auftretenden Probleme auf dich allein gestellt!",
|
||||||
"Checking for an update...": "Nach Updates suchen...",
|
"Checking for an update...": "Nach Updates suchen...",
|
||||||
"There are advanced notifications which are not shown here": "Es existieren erweiterte Benachrichtigungen, welche hier nicht angezeigt werden"
|
"There are advanced notifications which are not shown here": "Es existieren erweiterte Benachrichtigungen, welche hier nicht angezeigt werden",
|
||||||
|
"Missing roomId.": "Fehlende Raum-ID.",
|
||||||
|
"Picture": "Bild"
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,12 @@
|
||||||
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Whether or not you're using the Richtext mode of the Rich Text Editor",
|
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Whether or not you're using the Richtext mode of the Rich Text Editor",
|
||||||
"Your homeserver's URL": "Your homeserver's URL",
|
"Your homeserver's URL": "Your homeserver's URL",
|
||||||
"Your identity server's URL": "Your identity server's URL",
|
"Your identity server's URL": "Your identity server's URL",
|
||||||
|
"Every page you use in the app": "Every page you use in the app",
|
||||||
|
"e.g. <CurrentPageURL>": "e.g. <CurrentPageURL>",
|
||||||
|
"Your User Agent": "Your User Agent",
|
||||||
|
"Your device resolution": "Your device resolution",
|
||||||
"Analytics": "Analytics",
|
"Analytics": "Analytics",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "The information being sent to us to help make Riot.im better includes:",
|
"The information being sent to us to help make Riot.im better includes:": "The information being sent to us to help make Riot.im better includes:",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).",
|
|
||||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.",
|
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.",
|
||||||
"Call Failed": "Call Failed",
|
"Call Failed": "Call Failed",
|
||||||
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.",
|
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.",
|
||||||
|
@ -198,6 +201,7 @@
|
||||||
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
|
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
|
||||||
"Always show message timestamps": "Always show message timestamps",
|
"Always show message timestamps": "Always show message timestamps",
|
||||||
"Autoplay GIFs and videos": "Autoplay GIFs and videos",
|
"Autoplay GIFs and videos": "Autoplay GIFs and videos",
|
||||||
|
"Always show encryption icons": "Always show encryption icons",
|
||||||
"Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting",
|
"Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting",
|
||||||
"Hide avatars in user and room mentions": "Hide avatars in user and room mentions",
|
"Hide avatars in user and room mentions": "Hide avatars in user and room mentions",
|
||||||
"Disable big emoji in chat": "Disable big emoji in chat",
|
"Disable big emoji in chat": "Disable big emoji in chat",
|
||||||
|
@ -355,6 +359,7 @@
|
||||||
"Filter room members": "Filter room members",
|
"Filter room members": "Filter room members",
|
||||||
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)",
|
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)",
|
||||||
"Attachment": "Attachment",
|
"Attachment": "Attachment",
|
||||||
|
"At this time it is not possible to reply with a file so this will be sent without being a reply.": "At this time it is not possible to reply with a file so this will be sent without being a reply.",
|
||||||
"Upload Files": "Upload Files",
|
"Upload Files": "Upload Files",
|
||||||
"Are you sure you want to upload the following files?": "Are you sure you want to upload the following files?",
|
"Are you sure you want to upload the following files?": "Are you sure you want to upload the following files?",
|
||||||
"Encrypted room": "Encrypted room",
|
"Encrypted room": "Encrypted room",
|
||||||
|
@ -375,6 +380,8 @@
|
||||||
"Server error": "Server error",
|
"Server error": "Server error",
|
||||||
"Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",
|
"Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",
|
||||||
"Command error": "Command error",
|
"Command error": "Command error",
|
||||||
|
"Unable to reply": "Unable to reply",
|
||||||
|
"At this time it is not possible to reply with an emote.": "At this time it is not possible to reply with an emote.",
|
||||||
"bold": "bold",
|
"bold": "bold",
|
||||||
"italic": "italic",
|
"italic": "italic",
|
||||||
"strike": "strike",
|
"strike": "strike",
|
||||||
|
@ -402,9 +409,9 @@
|
||||||
"Idle": "Idle",
|
"Idle": "Idle",
|
||||||
"Offline": "Offline",
|
"Offline": "Offline",
|
||||||
"Unknown": "Unknown",
|
"Unknown": "Unknown",
|
||||||
"Replying": "Replying",
|
|
||||||
"Seen by %(userName)s at %(dateTime)s": "Seen by %(userName)s at %(dateTime)s",
|
"Seen by %(userName)s at %(dateTime)s": "Seen by %(userName)s at %(dateTime)s",
|
||||||
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
|
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
|
||||||
|
"Replying": "Replying",
|
||||||
"No rooms to show": "No rooms to show",
|
"No rooms to show": "No rooms to show",
|
||||||
"Unnamed room": "Unnamed room",
|
"Unnamed room": "Unnamed room",
|
||||||
"World readable": "World readable",
|
"World readable": "World readable",
|
||||||
|
@ -651,6 +658,7 @@
|
||||||
"Delete widget": "Delete widget",
|
"Delete widget": "Delete widget",
|
||||||
"Revoke widget access": "Revoke widget access",
|
"Revoke widget access": "Revoke widget access",
|
||||||
"Minimize apps": "Minimize apps",
|
"Minimize apps": "Minimize apps",
|
||||||
|
"Popout widget": "Popout widget",
|
||||||
"Picture": "Picture",
|
"Picture": "Picture",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
"Create new room": "Create new room",
|
"Create new room": "Create new room",
|
||||||
|
@ -724,6 +732,7 @@
|
||||||
"expand": "expand",
|
"expand": "expand",
|
||||||
"Custom of %(powerLevel)s": "Custom of %(powerLevel)s",
|
"Custom of %(powerLevel)s": "Custom of %(powerLevel)s",
|
||||||
"Custom level": "Custom level",
|
"Custom level": "Custom level",
|
||||||
|
"Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.",
|
||||||
"<a>In reply to</a> <pill>": "<a>In reply to</a> <pill>",
|
"<a>In reply to</a> <pill>": "<a>In reply to</a> <pill>",
|
||||||
"Room directory": "Room directory",
|
"Room directory": "Room directory",
|
||||||
"Start chat": "Start chat",
|
"Start chat": "Start chat",
|
||||||
|
@ -741,7 +750,7 @@
|
||||||
"Failed to send logs: ": "Failed to send logs: ",
|
"Failed to send logs: ": "Failed to send logs: ",
|
||||||
"Submit debug logs": "Submit debug logs",
|
"Submit debug logs": "Submit debug logs",
|
||||||
"Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.",
|
"Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.",
|
||||||
"<a>Click here</a> to create a GitHub issue.": "<a>Click here</a> to create a GitHub issue.",
|
"Riot bugs are tracked on GitHub: <a>create a GitHub issue</a>.": "Riot bugs are tracked on GitHub: <a>create a GitHub issue</a>.",
|
||||||
"GitHub issue link:": "GitHub issue link:",
|
"GitHub issue link:": "GitHub issue link:",
|
||||||
"Notes:": "Notes:",
|
"Notes:": "Notes:",
|
||||||
"Send logs": "Send logs",
|
"Send logs": "Send logs",
|
||||||
|
@ -803,11 +812,15 @@
|
||||||
"Ignore request": "Ignore request",
|
"Ignore request": "Ignore request",
|
||||||
"Loading device info...": "Loading device info...",
|
"Loading device info...": "Loading device info...",
|
||||||
"Encryption key request": "Encryption key request",
|
"Encryption key request": "Encryption key request",
|
||||||
"Otherwise, <a>click here</a> to send a bug report.": "Otherwise, <a>click here</a> to send a bug report.",
|
"Sign out": "Sign out",
|
||||||
|
"Log out and remove encryption keys?": "Log out and remove encryption keys?",
|
||||||
|
"Send Logs": "Send Logs",
|
||||||
|
"Clear Storage and Sign Out": "Clear Storage and Sign Out",
|
||||||
|
"Refresh": "Refresh",
|
||||||
"Unable to restore session": "Unable to restore session",
|
"Unable to restore session": "Unable to restore session",
|
||||||
"We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.": "We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.",
|
"We encountered an error trying to restore your previous session.": "We encountered an error trying to restore your previous session.",
|
||||||
"If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.",
|
"If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.",
|
||||||
"Continue anyway": "Continue anyway",
|
"Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.",
|
||||||
"Invalid Email Address": "Invalid Email Address",
|
"Invalid Email Address": "Invalid Email Address",
|
||||||
"This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address",
|
"This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address",
|
||||||
"Verification Pending": "Verification Pending",
|
"Verification Pending": "Verification Pending",
|
||||||
|
@ -853,6 +866,7 @@
|
||||||
"Permalink": "Permalink",
|
"Permalink": "Permalink",
|
||||||
"Quote": "Quote",
|
"Quote": "Quote",
|
||||||
"Source URL": "Source URL",
|
"Source URL": "Source URL",
|
||||||
|
"Collapse Reply Thread": "Collapse Reply Thread",
|
||||||
"Failed to set Direct Message status of room": "Failed to set Direct Message status of room",
|
"Failed to set Direct Message status of room": "Failed to set Direct Message status of room",
|
||||||
"All messages (noisy)": "All messages (noisy)",
|
"All messages (noisy)": "All messages (noisy)",
|
||||||
"All messages": "All messages",
|
"All messages": "All messages",
|
||||||
|
@ -1011,7 +1025,6 @@
|
||||||
"Status.im theme": "Status.im theme",
|
"Status.im theme": "Status.im theme",
|
||||||
"Can't load user settings": "Can't load user settings",
|
"Can't load user settings": "Can't load user settings",
|
||||||
"Server may be unavailable or overloaded": "Server may be unavailable or overloaded",
|
"Server may be unavailable or overloaded": "Server may be unavailable or overloaded",
|
||||||
"Sign out": "Sign out",
|
|
||||||
"For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.",
|
"For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.",
|
||||||
"Success": "Success",
|
"Success": "Success",
|
||||||
"Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them",
|
"Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them",
|
||||||
|
@ -1029,7 +1042,7 @@
|
||||||
"Device key:": "Device key:",
|
"Device key:": "Device key:",
|
||||||
"Ignored Users": "Ignored Users",
|
"Ignored Users": "Ignored Users",
|
||||||
"Debug Logs Submission": "Debug Logs Submission",
|
"Debug Logs Submission": "Debug Logs Submission",
|
||||||
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.",
|
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.",
|
||||||
"Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.",
|
"Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.",
|
||||||
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.",
|
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.",
|
||||||
"Learn more about how we use analytics.": "Learn more about how we use analytics.",
|
"Learn more about how we use analytics.": "Learn more about how we use analytics.",
|
||||||
|
|
|
@ -952,7 +952,6 @@
|
||||||
"The platform you're on": "Via sistemtipo",
|
"The platform you're on": "Via sistemtipo",
|
||||||
"Which officially provided instance you are using, if any": "Kiun oficiale disponeblan aperon vi uzas, se iun ajn",
|
"Which officially provided instance you are using, if any": "Kiun oficiale disponeblan aperon vi uzas, se iun ajn",
|
||||||
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Ĉu vi uzas la riĉtekstan reĝimon de la riĉteksta redaktilo aŭ ne",
|
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Ĉu vi uzas la riĉtekstan reĝimon de la riĉteksta redaktilo aŭ ne",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Ni ankaŭ registras ĉiun paĝon, kiun vi uzas en la programo (nun <CurrentPageHash>), vian klientan aplikaĵon (<CurrentUserAgent>) kaj vian aparatan distingon (<CurrentDeviceResolution>).",
|
|
||||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Kiam ĉi tiu paĝo enhavas identigeblajn informojn, ekzemple ĉambron, uzantan aŭ grupan identigilon, ĝi sendiĝas al la servilo sen tiuj.",
|
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Kiam ĉi tiu paĝo enhavas identigeblajn informojn, ekzemple ĉambron, uzantan aŭ grupan identigilon, ĝi sendiĝas al la servilo sen tiuj.",
|
||||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s",
|
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s",
|
||||||
"Disable Community Filter Panel": "Malŝalti komunuman filtran breton",
|
"Disable Community Filter Panel": "Malŝalti komunuman filtran breton",
|
||||||
|
|
|
@ -959,7 +959,6 @@
|
||||||
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Pribatutasuna garrantzitsua da guretzat, beraz ez dugu datu pertsonalik edo identifikagarririk jasotzen gure estatistiketan.",
|
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Pribatutasuna garrantzitsua da guretzat, beraz ez dugu datu pertsonalik edo identifikagarririk jasotzen gure estatistiketan.",
|
||||||
"Learn more about how we use analytics.": "Ikasi gehiago estatistikei ematen diegun erabileraz.",
|
"Learn more about how we use analytics.": "Ikasi gehiago estatistikei ematen diegun erabileraz.",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "Riot.im hobetzeko bidaltzen zaigun informazioan hau dago:",
|
"The information being sent to us to help make Riot.im better includes:": "Riot.im hobetzeko bidaltzen zaigun informazioan hau dago:",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Aplikazioan erabiltzen duzun orri bakoitza jasotzen dugu (orain <CurrentPageHash>), erabiltzaile-agentea (<CurrentUserAgent>) eta gailuaren bereizmena (<CurrentDeviceResolution>).",
|
|
||||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Orri honek informazio identifikagarria badu ere, esaterako gela, erabiltzailea edo talde ID-a, datu hauek ezabatu egiten dira zerbitzarira bidali aurretik.",
|
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Orri honek informazio identifikagarria badu ere, esaterako gela, erabiltzailea edo talde ID-a, datu hauek ezabatu egiten dira zerbitzarira bidali aurretik.",
|
||||||
"Whether or not you're logged in (we don't record your user name)": "Saioa hasita dagoen ala ez (ez dugu erabiltzaile-izena gordetzen)",
|
"Whether or not you're logged in (we don't record your user name)": "Saioa hasita dagoen ala ez (ez dugu erabiltzaile-izena gordetzen)",
|
||||||
"Which officially provided instance you are using, if any": "Erabiltzen ari zaren instantzia ofiziala, balego",
|
"Which officially provided instance you are using, if any": "Erabiltzen ari zaren instantzia ofiziala, balego",
|
||||||
|
@ -987,7 +986,7 @@
|
||||||
"%(user)s is a %(userRole)s": "%(user)s %(userRole)s da",
|
"%(user)s is a %(userRole)s": "%(user)s %(userRole)s da",
|
||||||
"Code": "Kodea",
|
"Code": "Kodea",
|
||||||
"Debug Logs Submission": "Arazte-egunkarien bidalketak",
|
"Debug Logs Submission": "Arazte-egunkarien bidalketak",
|
||||||
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Akats bat bidali baduzu GitHub bidez, arazte-egunkariek arazoa aurkitzen lagundu gaitzakete. Arazte-egunkariek aplikazioak darabilen datuak dauzkate, zure erabiltzaile izena barne, bisitatu dituzun gelen ID-ak edo ezizenak eta beste erabiltzaileen izenak. Ez dute mezurik.",
|
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Akats bat bidali baduzu GitHub bidez, arazte-egunkariek arazoa aurkitzen lagundu gaitzakete. Arazte-egunkariek aplikazioak darabilen datuak dauzkate, zure erabiltzaile izena barne, bisitatu dituzun gelen ID-ak edo ezizenak eta beste erabiltzaileen izenak. Ez dute mezurik.",
|
||||||
"Submit debug logs": "Bidali arazte-txostenak",
|
"Submit debug logs": "Bidali arazte-txostenak",
|
||||||
"Opens the Developer Tools dialog": "Garatzailearen tresnen elkarrizketa-koadroa irekitzen du",
|
"Opens the Developer Tools dialog": "Garatzailearen tresnen elkarrizketa-koadroa irekitzen du",
|
||||||
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "%(displayName)s (%(userName)s)(e)k ikusita %(dateTime)s(e)tan",
|
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "%(displayName)s (%(userName)s)(e)k ikusita %(dateTime)s(e)tan",
|
||||||
|
@ -1157,5 +1156,7 @@
|
||||||
"Collapse panel": "Tolestu panela",
|
"Collapse panel": "Tolestu panela",
|
||||||
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Zure oraingo nabigatzailearekin aplikazioaren itxura eta portaera guztiz okerra izan daiteke, eta funtzio batzuk ez dira ibiliko. Hala ere aurrera jarraitu dezakezu saiatu nahi baduzu, baina zure erantzukizunaren menpe geratzen dira aurkitu ditzakezun arazoak!",
|
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Zure oraingo nabigatzailearekin aplikazioaren itxura eta portaera guztiz okerra izan daiteke, eta funtzio batzuk ez dira ibiliko. Hala ere aurrera jarraitu dezakezu saiatu nahi baduzu, baina zure erantzukizunaren menpe geratzen dira aurkitu ditzakezun arazoak!",
|
||||||
"Checking for an update...": "Eguneraketarik dagoen egiaztatzen...",
|
"Checking for an update...": "Eguneraketarik dagoen egiaztatzen...",
|
||||||
"There are advanced notifications which are not shown here": "Hemen erakusten ez diren jakinarazpen aurreratuak daude"
|
"There are advanced notifications which are not shown here": "Hemen erakusten ez diren jakinarazpen aurreratuak daude",
|
||||||
|
"Missing roomId.": "Gelaren ID-a falta da.",
|
||||||
|
"Picture": "Irudia"
|
||||||
}
|
}
|
||||||
|
|
|
@ -956,7 +956,6 @@
|
||||||
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Le respect de votre vie privée est important pour nous, donc nous ne collectons aucune donnée personnelle ou permettant de vous identifier pour nos statistiques.",
|
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Le respect de votre vie privée est important pour nous, donc nous ne collectons aucune donnée personnelle ou permettant de vous identifier pour nos statistiques.",
|
||||||
"Learn more about how we use analytics.": "En savoir plus sur notre utilisation des statistiques.",
|
"Learn more about how we use analytics.": "En savoir plus sur notre utilisation des statistiques.",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "Les informations qui nous sont envoyées pour nous aider à améliorer Riot.im comprennent :",
|
"The information being sent to us to help make Riot.im better includes:": "Les informations qui nous sont envoyées pour nous aider à améliorer Riot.im comprennent :",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Nous enregistrons aussi chaque page que vous utilisez dans l'application (en ce moment <CurrentPageHash>), votre User Agent (<CurrentUserAgent>) et la résolution de votre appareil (<CurrentDeviceResolution>).",
|
|
||||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Si la page contient des informations permettant de vous identifier, comme un salon, un identifiant d'utilisateur ou de groupe, ces données sont enlevées avant qu'elle ne soit envoyée au serveur.",
|
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Si la page contient des informations permettant de vous identifier, comme un salon, un identifiant d'utilisateur ou de groupe, ces données sont enlevées avant qu'elle ne soit envoyée au serveur.",
|
||||||
"The platform you're on": "La plateforme que vous utilisez",
|
"The platform you're on": "La plateforme que vous utilisez",
|
||||||
"The version of Riot.im": "La version de Riot.im",
|
"The version of Riot.im": "La version de Riot.im",
|
||||||
|
@ -988,7 +987,7 @@
|
||||||
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Vu par %(displayName)s (%(userName)s) à %(dateTime)s",
|
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Vu par %(displayName)s (%(userName)s) à %(dateTime)s",
|
||||||
"Code": "Code",
|
"Code": "Code",
|
||||||
"Debug Logs Submission": "Envoi des journaux de débogage",
|
"Debug Logs Submission": "Envoi des journaux de débogage",
|
||||||
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Si vous avez signalé un bug via GitHub, les journaux de débogage peuvent nous aider à identifier le problème. Les journaux de débogage contiennent des données d'utilisation de l'application dont votre nom d'utilisateur, les identifiants ou alias des salons ou groupes que vous avez visité et les noms d'utilisateur des autres participants. Ils ne contiennent pas les messages.",
|
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Si vous avez signalé un bug via GitHub, les journaux de débogage peuvent nous aider à identifier le problème. Les journaux de débogage contiennent des données d'utilisation de l'application dont votre nom d'utilisateur, les identifiants ou alias des salons ou groupes que vous avez visité et les noms d'utilisateur des autres participants. Ils ne contiennent pas les messages.",
|
||||||
"Submit debug logs": "Envoyer les journaux de débogage",
|
"Submit debug logs": "Envoyer les journaux de débogage",
|
||||||
"Opens the Developer Tools dialog": "Ouvre la fenêtre des Outils de développeur",
|
"Opens the Developer Tools dialog": "Ouvre la fenêtre des Outils de développeur",
|
||||||
"Unable to join community": "Impossible de rejoindre la communauté",
|
"Unable to join community": "Impossible de rejoindre la communauté",
|
||||||
|
@ -1156,5 +1155,8 @@
|
||||||
"Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Les rapports de débogage contiennent des données d'usage de l'application qui incluent votre nom d'utilisateur, les identifiants ou alias des salons ou groupes auxquels vous avez rendu visite ainsi que les noms des autres utilisateurs. Ils ne contiennent aucun message.",
|
"Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Les rapports de débogage contiennent des données d'usage de l'application qui incluent votre nom d'utilisateur, les identifiants ou alias des salons ou groupes auxquels vous avez rendu visite ainsi que les noms des autres utilisateurs. Ils ne contiennent aucun message.",
|
||||||
"Failed to send logs: ": "Échec lors de l'envoi des rapports : ",
|
"Failed to send logs: ": "Échec lors de l'envoi des rapports : ",
|
||||||
"Notes:": "Notes :",
|
"Notes:": "Notes :",
|
||||||
"Preparing to send logs": "Préparation d'envoi des rapports"
|
"Preparing to send logs": "Préparation d'envoi des rapports",
|
||||||
|
"Missing roomId.": "Identifiant de salon manquant.",
|
||||||
|
"Picture": "Image",
|
||||||
|
"<a>Click here</a> to create a GitHub issue.": "<a>Cliquez ici</a> pour créer un signalement sur GitHub."
|
||||||
}
|
}
|
||||||
|
|
|
@ -951,7 +951,6 @@
|
||||||
"File to import": "Ficheiro a importar",
|
"File to import": "Ficheiro a importar",
|
||||||
"Import": "Importar",
|
"Import": "Importar",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "A información enviada a Riot.im para axudarnos a mellorar inclúe:",
|
"The information being sent to us to help make Riot.im better includes:": "A información enviada a Riot.im para axudarnos a mellorar inclúe:",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Tamén rexistramos cada páxina que vostede utiliza no aplicativo (actualmente <CurrentPageHash>), o User Agent (<CurrentUserAgent>) e a resolución do dispositivo (<CurrentDeviceResolution>).",
|
|
||||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Si esta páxina inclúe información identificable como ID de grupo, usuario ou sala, estes datos son eliminados antes de ser enviados ao servidor.",
|
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Si esta páxina inclúe información identificable como ID de grupo, usuario ou sala, estes datos son eliminados antes de ser enviados ao servidor.",
|
||||||
"The platform you're on": "A plataforma na que está",
|
"The platform you're on": "A plataforma na que está",
|
||||||
"The version of Riot.im": "A versión de Riot.im",
|
"The version of Riot.im": "A versión de Riot.im",
|
||||||
|
@ -993,7 +992,7 @@
|
||||||
"Join this community": "Únase a esta comunidade",
|
"Join this community": "Únase a esta comunidade",
|
||||||
"Leave this community": "Deixar esta comunidade",
|
"Leave this community": "Deixar esta comunidade",
|
||||||
"Debug Logs Submission": "Envío de rexistro de depuración",
|
"Debug Logs Submission": "Envío de rexistro de depuración",
|
||||||
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Si enviou un reporte de fallo a través de GitHub, os informes poden axudarnos a examinar o problema. Os informes de fallo conteñen datos do uso do aplicativo incluíndo o seu nome de usuaria, os IDs ou alcumes das salas e grupos que visitou e os nomes de usuaria de outras personas. Non conteñen mensaxes.",
|
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Si enviou un reporte de fallo a través de GitHub, os informes poden axudarnos a examinar o problema. Os informes de fallo conteñen datos do uso do aplicativo incluíndo o seu nome de usuaria, os IDs ou alcumes das salas e grupos que visitou e os nomes de usuaria de outras personas. Non conteñen mensaxes.",
|
||||||
"Submit debug logs": "Enviar informes de depuración",
|
"Submit debug logs": "Enviar informes de depuración",
|
||||||
"Opens the Developer Tools dialog": "Abre o cadro de Ferramentas de Desenvolvedoras",
|
"Opens the Developer Tools dialog": "Abre o cadro de Ferramentas de Desenvolvedoras",
|
||||||
"Stickerpack": "Peganitas",
|
"Stickerpack": "Peganitas",
|
||||||
|
|
|
@ -362,7 +362,7 @@
|
||||||
"This email address was not found": "Az e-mail cím nem található",
|
"This email address was not found": "Az e-mail cím nem található",
|
||||||
"The email address linked to your account must be entered.": "A fiókodhoz kötött e-mail címet add meg.",
|
"The email address linked to your account must be entered.": "A fiókodhoz kötött e-mail címet add meg.",
|
||||||
"Press <StartChatButton> to start a chat with someone": "Nyomd meg a <StartChatButton> gombot ha szeretnél csevegni valakivel",
|
"Press <StartChatButton> to start a chat with someone": "Nyomd meg a <StartChatButton> gombot ha szeretnél csevegni valakivel",
|
||||||
"Privacy warning": "Magánéleti figyelmeztetés",
|
"Privacy warning": "Adatvédelmi figyelmeztetés",
|
||||||
"The file '%(fileName)s' exceeds this home server's size limit for uploads": "'%(fileName)s' fájl túllépte a Saját szerverben beállított feltöltési méret határt",
|
"The file '%(fileName)s' exceeds this home server's size limit for uploads": "'%(fileName)s' fájl túllépte a Saját szerverben beállított feltöltési méret határt",
|
||||||
"The file '%(fileName)s' failed to upload": "'%(fileName)s' fájl feltöltése sikertelen",
|
"The file '%(fileName)s' failed to upload": "'%(fileName)s' fájl feltöltése sikertelen",
|
||||||
"The remote side failed to pick up": "A hívott fél nem vette fel",
|
"The remote side failed to pick up": "A hívott fél nem vette fel",
|
||||||
|
@ -956,7 +956,6 @@
|
||||||
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "A személyes adatok védelme fontos számunkra, így mi nem gyűjtünk személyes és személyhez köthető adatokat az analitikánkhoz.",
|
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "A személyes adatok védelme fontos számunkra, így mi nem gyűjtünk személyes és személyhez köthető adatokat az analitikánkhoz.",
|
||||||
"Learn more about how we use analytics.": "Tudj meg többet arról hogyan használjuk az analitikai adatokat.",
|
"Learn more about how we use analytics.": "Tudj meg többet arról hogyan használjuk az analitikai adatokat.",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "Az adatok amiket a Riot.im javításához felhasználunk az alábbiak:",
|
"The information being sent to us to help make Riot.im better includes:": "Az adatok amiket a Riot.im javításához felhasználunk az alábbiak:",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Felvesszük az összes oldalt amit az alkalmazásban használsz (jelenleg <CurrentPageHash>), a \"User Agent\"-et (<CurrentUserAgent>) és az eszköz felbontását (<CurrentDeviceResolution>).",
|
|
||||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Minden azonosításra alkalmas adatot mint a szoba, felhasználó vagy csoport azonosítót mielőtt az adatokat elküldenénk eltávolításra kerülnek.",
|
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Minden azonosításra alkalmas adatot mint a szoba, felhasználó vagy csoport azonosítót mielőtt az adatokat elküldenénk eltávolításra kerülnek.",
|
||||||
"The platform you're on": "A platform amit használsz",
|
"The platform you're on": "A platform amit használsz",
|
||||||
"The version of Riot.im": "Riot.im verziója",
|
"The version of Riot.im": "Riot.im verziója",
|
||||||
|
@ -987,7 +986,7 @@
|
||||||
"%(user)s is a %(userRole)s": "%(user)s egy %(userRole)s",
|
"%(user)s is a %(userRole)s": "%(user)s egy %(userRole)s",
|
||||||
"Code": "Kód",
|
"Code": "Kód",
|
||||||
"Debug Logs Submission": "Hibakeresési napló elküldése",
|
"Debug Logs Submission": "Hibakeresési napló elküldése",
|
||||||
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Ha a GitHubon keresztül küldted be a hibát, a hibakeresési napló segíthet nekünk a javításban. A napló felhasználási adatokat tartalmaz mint a felhasználói neved, az általad meglátogatott szobák vagy csoportok azonosítóját vagy alternatív nevét és mások felhasználói nevét. De nem tartalmazzák az üzeneteket.",
|
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Ha a GitHubon keresztül küldted be a hibát, a hibakeresési napló segíthet nekünk a javításban. A napló felhasználási adatokat tartalmaz mint a felhasználói neved, az általad meglátogatott szobák vagy csoportok azonosítóját vagy alternatív nevét és mások felhasználói nevét. De nem tartalmazzák az üzeneteket.",
|
||||||
"Submit debug logs": "Hibakeresési napló küldése",
|
"Submit debug logs": "Hibakeresési napló küldése",
|
||||||
"Opens the Developer Tools dialog": "Megnyitja a fejlesztői eszközök ablakát",
|
"Opens the Developer Tools dialog": "Megnyitja a fejlesztői eszközök ablakát",
|
||||||
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "%(displayName)s (%(userName)s) az alábbi időpontban látta: %(dateTime)s",
|
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "%(displayName)s (%(userName)s) az alábbi időpontban látta: %(dateTime)s",
|
||||||
|
@ -1157,5 +1156,7 @@
|
||||||
"Collapse panel": "Panel becsukása",
|
"Collapse panel": "Panel becsukása",
|
||||||
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Ebben a böngészőben az alkalmazás felülete tele lehet hibával, és az is lehet, hogy egyáltalán nem működik. Ha így is ki szeretnéd próbálni, megteheted, de ha valami gondod van, nem tudunk segíteni!",
|
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Ebben a böngészőben az alkalmazás felülete tele lehet hibával, és az is lehet, hogy egyáltalán nem működik. Ha így is ki szeretnéd próbálni, megteheted, de ha valami gondod van, nem tudunk segíteni!",
|
||||||
"Checking for an update...": "Frissítés keresése...",
|
"Checking for an update...": "Frissítés keresése...",
|
||||||
"There are advanced notifications which are not shown here": "Vannak itt nem látható, haladó értesítések"
|
"There are advanced notifications which are not shown here": "Vannak itt nem látható, haladó értesítések",
|
||||||
|
"Missing roomId.": "Hiányzó szoba azonosító.",
|
||||||
|
"Picture": "Kép"
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,7 +101,6 @@
|
||||||
"Your identity server's URL": "L'URL del tuo server di identità",
|
"Your identity server's URL": "L'URL del tuo server di identità",
|
||||||
"Analytics": "Statistiche",
|
"Analytics": "Statistiche",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "Le informazioni inviate per aiutarci a migliorare Riot.im includono:",
|
"The information being sent to us to help make Riot.im better includes:": "Le informazioni inviate per aiutarci a migliorare Riot.im includono:",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Registriamo anche ogni pagina che usi nell'app (attualmente <CurrentPageHash>), il tuo User Agent (<CurrentUserAgent>) e la risoluzione del dispositivo (<CurrentDeviceResolution>).",
|
|
||||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Se questa pagina include informazioni identificabili, come una stanza, utente o ID di gruppo, questi dati sono rimossi prima che vengano inviati al server.",
|
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Se questa pagina include informazioni identificabili, come una stanza, utente o ID di gruppo, questi dati sono rimossi prima che vengano inviati al server.",
|
||||||
"Call Failed": "Chiamata fallita",
|
"Call Failed": "Chiamata fallita",
|
||||||
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Ci sono dispositivi sconosciuti in questa stanza: se procedi senza verificarli, qualcuno avrà la possibilità di intercettare la tua chiamata.",
|
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Ci sono dispositivi sconosciuti in questa stanza: se procedi senza verificarli, qualcuno avrà la possibilità di intercettare la tua chiamata.",
|
||||||
|
@ -865,7 +864,7 @@
|
||||||
"Device key:": "Chiave dispositivo:",
|
"Device key:": "Chiave dispositivo:",
|
||||||
"Ignored Users": "Utenti ignorati",
|
"Ignored Users": "Utenti ignorati",
|
||||||
"Debug Logs Submission": "Invio dei log di debug",
|
"Debug Logs Submission": "Invio dei log di debug",
|
||||||
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Se hai segnalato un errore via Github, i log di debug possono aiutarci a identificare il problema. I log di debug contengono dati di utilizzo dell'applicazione inclusi il nome utente, gli ID o alias delle stanze o gruppi visitati e i nomi degli altri utenti. Non contengono messaggi.",
|
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Se hai segnalato un errore via Github, i log di debug possono aiutarci a identificare il problema. I log di debug contengono dati di utilizzo dell'applicazione inclusi il nome utente, gli ID o alias delle stanze o gruppi visitati e i nomi degli altri utenti. Non contengono messaggi.",
|
||||||
"Submit debug logs": "Invia log di debug",
|
"Submit debug logs": "Invia log di debug",
|
||||||
"Riot collects anonymous analytics to allow us to improve the application.": "Riot raccoglie statistiche anonime per permetterci di migliorare l'applicazione.",
|
"Riot collects anonymous analytics to allow us to improve the application.": "Riot raccoglie statistiche anonime per permetterci di migliorare l'applicazione.",
|
||||||
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Diamo importanza alla privacy, perciò non raccogliamo dati personali o identificabili per le nostre statistiche.",
|
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Diamo importanza alla privacy, perciò non raccogliamo dati personali o identificabili per le nostre statistiche.",
|
||||||
|
|
|
@ -87,7 +87,6 @@
|
||||||
"Your identity server's URL": "あなたのアイデンティティサーバのURL",
|
"Your identity server's URL": "あなたのアイデンティティサーバのURL",
|
||||||
"Analytics": "分析",
|
"Analytics": "分析",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "Riot.imをよりよくするために私達に送信される情報は以下を含みます:",
|
"The information being sent to us to help make Riot.im better includes:": "Riot.imをよりよくするために私達に送信される情報は以下を含みます:",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "私達はこのアプリであなたが利用したページ(現在は<CurrentPageHash>)、あなたのユーザエージェント(現在は<CurrentUserAgent>)、並びにあなたの端末の解像度(現在の端末では<CurrentDeviceResolution>)も記録します。",
|
|
||||||
"Thursday": "木曜日",
|
"Thursday": "木曜日",
|
||||||
"Messages in one-to-one chats": "一対一のチャットでのメッセージ",
|
"Messages in one-to-one chats": "一対一のチャットでのメッセージ",
|
||||||
"A new version of Riot is available.": "新しいバージョンのRiotが利用可能です。",
|
"A new version of Riot is available.": "新しいバージョンのRiotが利用可能です。",
|
||||||
|
@ -242,5 +241,7 @@
|
||||||
"Collapse panel": "パネルを折りたたむ",
|
"Collapse panel": "パネルを折りたたむ",
|
||||||
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "現在ご使用のブラウザでは、アプリの外見や使い心地が正常でない可能性があります。また、一部または全部の機能がご使用いただけない可能性があります。このままご使用いただけますが、問題が発生した場合は対応しかねます!",
|
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "現在ご使用のブラウザでは、アプリの外見や使い心地が正常でない可能性があります。また、一部または全部の機能がご使用いただけない可能性があります。このままご使用いただけますが、問題が発生した場合は対応しかねます!",
|
||||||
"Checking for an update...": "アップデートを確認しています…",
|
"Checking for an update...": "アップデートを確認しています…",
|
||||||
"There are advanced notifications which are not shown here": "ここに表示されない詳細な通知があります"
|
"There are advanced notifications which are not shown here": "ここに表示されない詳細な通知があります",
|
||||||
|
"Call": "通話",
|
||||||
|
"Answer": "応答"
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"Your identity server's URL": "Jūsų identifikavimo serverio URL adresas",
|
"Your identity server's URL": "Jūsų identifikavimo serverio URL adresas",
|
||||||
"Analytics": "Statistika",
|
"Analytics": "Statistika",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "Informacijoje, kuri yra siunčiama Riot.im tobulinimui yra:",
|
"The information being sent to us to help make Riot.im better includes:": "Informacijoje, kuri yra siunčiama Riot.im tobulinimui yra:",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Mes taip pat saugome kiekvieną puslapį, kurį jūs naudojate programėlėje (dabartinis <CurrentPageHash>), jūsų paskyros agentas (<CurrentUserAgent>) ir jūsų įrenginio rezoliucija (<CurrentDeviceResolution>).",
|
|
||||||
"Fetching third party location failed": "Nepavyko gauti trečios šalies vietos",
|
"Fetching third party location failed": "Nepavyko gauti trečios šalies vietos",
|
||||||
"A new version of Riot is available.": "Yra nauja Riot versija.",
|
"A new version of Riot is available.": "Yra nauja Riot versija.",
|
||||||
"I understand the risks and wish to continue": "Aš suprantu riziką ir noriu tęsti",
|
"I understand the risks and wish to continue": "Aš suprantu riziką ir noriu tęsti",
|
||||||
|
|
|
@ -695,7 +695,6 @@
|
||||||
"Your homeserver's URL": "Bāzes servera URL adrese",
|
"Your homeserver's URL": "Bāzes servera URL adrese",
|
||||||
"Your identity server's URL": "Tava Identitātes servera URL adrese",
|
"Your identity server's URL": "Tava Identitātes servera URL adrese",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "Informācija, kura mums tiek nosūtīta, lai ļautu padarīt Riot.im labāku, ietver:",
|
"The information being sent to us to help make Riot.im better includes:": "Informācija, kura mums tiek nosūtīta, lai ļautu padarīt Riot.im labāku, ietver:",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Mēs arī fiksējam katru lapu, kuru tu izmanto programmā (currently <CurrentPageHash>), Tavu lietotāja aģentu (<CurrentUserAgent>) un Tavas ierīces ekrāna izšķirtspēju (<CurrentDeviceResolution>).",
|
|
||||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Ja šī lapa ietver identificējamu informāciju, tādu kā istaba, lietotājs, grupas Id, šie dati tiek noņemti pirms nosūtīšanas uz serveri.",
|
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Ja šī lapa ietver identificējamu informāciju, tādu kā istaba, lietotājs, grupas Id, šie dati tiek noņemti pirms nosūtīšanas uz serveri.",
|
||||||
"Call Failed": "Zvans neizdevās",
|
"Call Failed": "Zvans neizdevās",
|
||||||
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Šajā istabā ir nepazīstamas ierīces: ja Tu turpināsi bez to pārbaudes, ir iespējams, ka kāda nepiederoša persona var noklausīties Tavas sarunas.",
|
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Šajā istabā ir nepazīstamas ierīces: ja Tu turpināsi bez to pārbaudes, ir iespējams, ka kāda nepiederoša persona var noklausīties Tavas sarunas.",
|
||||||
|
@ -987,7 +986,7 @@
|
||||||
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|other": "Tagad<resendText>visas atkārtoti sūtīt</resendText> vai <cancelText>visas atcelt</cancelText>. Tu vari atzīmēt arī individuālas ziņas, kuras atkārtoti sūtīt vai atcelt.",
|
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|other": "Tagad<resendText>visas atkārtoti sūtīt</resendText> vai <cancelText>visas atcelt</cancelText>. Tu vari atzīmēt arī individuālas ziņas, kuras atkārtoti sūtīt vai atcelt.",
|
||||||
"Clear filter": "Attīrīt filtru",
|
"Clear filter": "Attīrīt filtru",
|
||||||
"Debug Logs Submission": "Iesniegt atutošanas logfailus",
|
"Debug Logs Submission": "Iesniegt atutošanas logfailus",
|
||||||
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Ja esi paziņojis par kļūdu caur GitHub, atutošanas logfaili var mums palīdzēt identificēt problēmu. Atutošanas logfaili satur programmas lietošanas datus, tostarp Tavu lietotājvārdu, istabu/grupu Id vai aliases, kuras esi apmeklējis un citu lietotāju lietotājvārdus. Tie nesatur pašas ziņas.",
|
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Ja esi paziņojis par kļūdu caur GitHub, atutošanas logfaili var mums palīdzēt identificēt problēmu. Atutošanas logfaili satur programmas lietošanas datus, tostarp Tavu lietotājvārdu, istabu/grupu Id vai aliases, kuras esi apmeklējis un citu lietotāju lietotājvārdus. Tie nesatur pašas ziņas.",
|
||||||
"Submit debug logs": "Iesniegt atutošanas logfailus",
|
"Submit debug logs": "Iesniegt atutošanas logfailus",
|
||||||
"Opens the Developer Tools dialog": "Atver Izstrādātāja instrumentus",
|
"Opens the Developer Tools dialog": "Atver Izstrādātāja instrumentus",
|
||||||
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Skatījis %(displayName)s (%(userName)s) %(dateTime)s",
|
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Skatījis %(displayName)s (%(userName)s) %(dateTime)s",
|
||||||
|
|
|
@ -959,7 +959,6 @@
|
||||||
"Notify the whole room": "Notificeer de gehele ruimte",
|
"Notify the whole room": "Notificeer de gehele ruimte",
|
||||||
"Room Notification": "Ruimte Notificatie",
|
"Room Notification": "Ruimte Notificatie",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "De informatie dat naar ons wordt verstuurd om Riot.im beter te maken betrekt:",
|
"The information being sent to us to help make Riot.im better includes:": "De informatie dat naar ons wordt verstuurd om Riot.im beter te maken betrekt:",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "We nemen ook elke pagina die je in de applicatie gebruikt (momenteel <CurrentPageHash>), je User Agent (<CurrentUserAgent>) en de resolutie van je apparaat (<CurrentDeviceResolution>) op.",
|
|
||||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Waar deze pagina identificeerbare informatie bevat, zoals een ruimte, gebruiker of groep ID, zal deze data verwijderd worden voordat het naar de server gestuurd wordt.",
|
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Waar deze pagina identificeerbare informatie bevat, zoals een ruimte, gebruiker of groep ID, zal deze data verwijderd worden voordat het naar de server gestuurd wordt.",
|
||||||
"The platform you're on": "Het platform waar je je op bevindt",
|
"The platform you're on": "Het platform waar je je op bevindt",
|
||||||
"The version of Riot.im": "De versie van Riot.im",
|
"The version of Riot.im": "De versie van Riot.im",
|
||||||
|
@ -1002,7 +1001,7 @@
|
||||||
"Everyone": "Iedereen",
|
"Everyone": "Iedereen",
|
||||||
"Leave this community": "Deze gemeenschap verlaten",
|
"Leave this community": "Deze gemeenschap verlaten",
|
||||||
"Debug Logs Submission": "Debug Logs Indienen",
|
"Debug Logs Submission": "Debug Logs Indienen",
|
||||||
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Als je een bug via Github hebt ingediend kunnen debug logs ons helpen om het probleem te vinden. Debug logs bevatten applicatie-gebruik data inclusief je gebruikersnaam, de ID's of namen van de ruimtes en groepen die je hebt bezocht en de gebruikersnamen van andere gebruikers. Ze bevatten geen berichten.",
|
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Als je een bug via Github hebt ingediend kunnen debug logs ons helpen om het probleem te vinden. Debug logs bevatten applicatie-gebruik data inclusief je gebruikersnaam, de ID's of namen van de ruimtes en groepen die je hebt bezocht en de gebruikersnamen van andere gebruikers. Ze bevatten geen berichten.",
|
||||||
"Submit debug logs": "Debug logs indienen",
|
"Submit debug logs": "Debug logs indienen",
|
||||||
"Opens the Developer Tools dialog": "Opent het Ontwikkelaars Gereedschappen dialoog",
|
"Opens the Developer Tools dialog": "Opent het Ontwikkelaars Gereedschappen dialoog",
|
||||||
"Fetching third party location failed": "Het ophalen van de locatie van de derde partij is mislukt",
|
"Fetching third party location failed": "Het ophalen van de locatie van de derde partij is mislukt",
|
||||||
|
|
|
@ -693,7 +693,6 @@
|
||||||
"Your homeserver's URL": "Adres URL twojego serwera domowego",
|
"Your homeserver's URL": "Adres URL twojego serwera domowego",
|
||||||
"Your identity server's URL": "Adres URL twojego serwera tożsamości",
|
"Your identity server's URL": "Adres URL twojego serwera tożsamości",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "Oto informacje przesyłane do nas, służące do poprawy Riot.im:",
|
"The information being sent to us to help make Riot.im better includes:": "Oto informacje przesyłane do nas, służące do poprawy Riot.im:",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Zapisujemy również każdą stronę, z której korzystasz w aplikacji (obecnie <CurrentPageHash>), twój User Agent (<CurrentUserAgent>) oraz rozdzielczość ekranu twojego urządzenia (<CurrentDeviceResolution>).",
|
|
||||||
"The platform you're on": "Platforma na której jesteś",
|
"The platform you're on": "Platforma na której jesteś",
|
||||||
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "W tym pokoju są nieznane urządzenia: jeżeli będziesz kontynuować bez ich weryfikacji, możliwe będzie podsłuchiwanie Twojego połączenia.",
|
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "W tym pokoju są nieznane urządzenia: jeżeli będziesz kontynuować bez ich weryfikacji, możliwe będzie podsłuchiwanie Twojego połączenia.",
|
||||||
"Answer": "Odbierz",
|
"Answer": "Odbierz",
|
||||||
|
|
|
@ -664,7 +664,6 @@
|
||||||
"Your homeserver's URL": "A URL do seu Servidor de Base (homeserver)",
|
"Your homeserver's URL": "A URL do seu Servidor de Base (homeserver)",
|
||||||
"Your identity server's URL": "A URL do seu servidor de identidade",
|
"Your identity server's URL": "A URL do seu servidor de identidade",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "As informações que estão sendo usadas para ajudar a melhorar o Riot.im incluem:",
|
"The information being sent to us to help make Riot.im better includes:": "As informações que estão sendo usadas para ajudar a melhorar o Riot.im incluem:",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Nós também gravamos cada página que você usa no app (atualmente <CurrentPageHash>), o seu User Agent (<CurrentUserAgent>) e a resolução do seu dispositivo (<CurrentDeviceResolution>).",
|
|
||||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Quando esta página tem informação de identificação, como uma sala, ID de usuária/o ou de grupo, estes dados são removidos antes de serem enviados ao servidor.",
|
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Quando esta página tem informação de identificação, como uma sala, ID de usuária/o ou de grupo, estes dados são removidos antes de serem enviados ao servidor.",
|
||||||
"Call Failed": "A chamada falhou",
|
"Call Failed": "A chamada falhou",
|
||||||
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Há dispositivos desconhecidos nesta sala: se você continuar sem verificá-los, será possível alguém espiar sua chamada.",
|
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Há dispositivos desconhecidos nesta sala: se você continuar sem verificá-los, será possível alguém espiar sua chamada.",
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -957,7 +957,6 @@
|
||||||
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Vaše súkromie je pre nás dôležité, preto nezhromažďujeme žiadne osobné údaje alebo údaje, na základe ktorých je možné vás identifikovať.",
|
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Vaše súkromie je pre nás dôležité, preto nezhromažďujeme žiadne osobné údaje alebo údaje, na základe ktorých je možné vás identifikovať.",
|
||||||
"Learn more about how we use analytics.": "Zistite viac o tom, ako spracúvame analytické údaje.",
|
"Learn more about how we use analytics.": "Zistite viac o tom, ako spracúvame analytické údaje.",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "S cieľom vylepšovať aplikáciu Riot.im zbierame nasledujúce údaje:",
|
"The information being sent to us to help make Riot.im better includes:": "S cieľom vylepšovať aplikáciu Riot.im zbierame nasledujúce údaje:",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Zaznamenávame tiež každú stránku aplikácie Riot.im, ktorú otvoríte (momentálne <CurrentPageHash>), reťazec user agent (<CurrentUserAgent>) a rozlíšenie obrazovky vašeho zariadenia (<CurrentDeviceResolution>).",
|
|
||||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Ak sa na stránke vyskytujú identifikujúce údaje, akými sú napríklad názov miestnosti, ID používateľa, miestnosti alebo skupiny, tieto sú pred odoslaním na server odstránené.",
|
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Ak sa na stránke vyskytujú identifikujúce údaje, akými sú napríklad názov miestnosti, ID používateľa, miestnosti alebo skupiny, tieto sú pred odoslaním na server odstránené.",
|
||||||
"The platform you're on": "Vami používaná platforma",
|
"The platform you're on": "Vami používaná platforma",
|
||||||
"The version of Riot.im": "Verzia Riot.im",
|
"The version of Riot.im": "Verzia Riot.im",
|
||||||
|
@ -993,7 +992,7 @@
|
||||||
"To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Ak si chcete nastaviť filter, pretiahnite obrázok komunity na panel filtrovania úplne na ľavej strane obrazovky. Potom môžete kedykoľvek kliknúť na obrázok komunity na tomto panely a Riot.im vám bude zobrazovať len miestnosti a ľudí z komunity, na ktorej obrázok ste klikli.",
|
"To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Ak si chcete nastaviť filter, pretiahnite obrázok komunity na panel filtrovania úplne na ľavej strane obrazovky. Potom môžete kedykoľvek kliknúť na obrázok komunity na tomto panely a Riot.im vám bude zobrazovať len miestnosti a ľudí z komunity, na ktorej obrázok ste klikli.",
|
||||||
"Clear filter": "Zrušiť filter",
|
"Clear filter": "Zrušiť filter",
|
||||||
"Debug Logs Submission": "Odoslanie ladiacich záznamov",
|
"Debug Logs Submission": "Odoslanie ladiacich záznamov",
|
||||||
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "Ak ste nám poslali hlásenie o chybe cez Github, ladiace záznamy nám môžu pomôcť lepšie identifikovať chybu. Ladiace záznamy obsahujú údaje o používaní aplikácii, vrátane vašeho používateľského mena, názvy a aliasy miestností a komunít, ku ktorým ste sa pripojili a mená ostatných používateľov. Tieto záznamy neobsahujú samotný obsah vašich správ.",
|
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Ak ste nám poslali hlásenie o chybe cez Github, ladiace záznamy nám môžu pomôcť lepšie identifikovať chybu. Ladiace záznamy obsahujú údaje o používaní aplikácii, vrátane vašeho používateľského mena, názvy a aliasy miestností a komunít, ku ktorým ste sa pripojili a mená ostatných používateľov. Tieto záznamy neobsahujú samotný obsah vašich správ.",
|
||||||
"Submit debug logs": "Odoslať ladiace záznamy",
|
"Submit debug logs": "Odoslať ladiace záznamy",
|
||||||
"Opens the Developer Tools dialog": "Otvorí dialóg nástroje pre vývojárov",
|
"Opens the Developer Tools dialog": "Otvorí dialóg nástroje pre vývojárov",
|
||||||
"Stickerpack": "Balíček nálepiek",
|
"Stickerpack": "Balíček nálepiek",
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"Your identity server's URL": "URL-ja e server-it identiteti tëndë",
|
"Your identity server's URL": "URL-ja e server-it identiteti tëndë",
|
||||||
"Analytics": "Analiza",
|
"Analytics": "Analiza",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "Informacionet që dërgohen për t'i ndihmuar Riot.im-it të përmirësohet përmbajnë:",
|
"The information being sent to us to help make Riot.im better includes:": "Informacionet që dërgohen për t'i ndihmuar Riot.im-it të përmirësohet përmbajnë:",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Ne gjithashtu inçizojmë çdo faqe që përdorë në aplikacion (në këtë çast <CurrentPageHash>), agjentin e përdoruesit tëndë (<CurrentUserAgent>) dhe rezolucionin e pajisjes tënde (<CurrentDeviceResolution>).",
|
|
||||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Kur kjo faqe pëmban informacione që mund të të identifikojnë, sikur një dhomë, përdorues apo identifikatues grupi, këto të dhëna do të mënjanohen para se t‘i dërgohën një server-it.",
|
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Kur kjo faqe pëmban informacione që mund të të identifikojnë, sikur një dhomë, përdorues apo identifikatues grupi, këto të dhëna do të mënjanohen para se t‘i dërgohën një server-it.",
|
||||||
"Call Failed": "Thirrja nuk mundej të realizohet",
|
"Call Failed": "Thirrja nuk mundej të realizohet",
|
||||||
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Pajisje të panjohura ndodhen në këtë dhomë: nësë vazhdon pa i vërtetuar, është e mundshme që dikush të jua përgjon thirrjen.",
|
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Pajisje të panjohura ndodhen në këtë dhomë: nësë vazhdon pa i vërtetuar, është e mundshme që dikush të jua përgjon thirrjen.",
|
||||||
|
|
|
@ -773,7 +773,6 @@
|
||||||
"Your identity server's URL": "Адреса вашег идентитеског сервера",
|
"Your identity server's URL": "Адреса вашег идентитеског сервера",
|
||||||
"Analytics": "Аналитика",
|
"Analytics": "Аналитика",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "У податке које нам шаљете зарад побољшавања Riot.im-а спадају:",
|
"The information being sent to us to help make Riot.im better includes:": "У податке које нам шаљете зарад побољшавања Riot.im-а спадају:",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "Такође бележимо сваку страницу коју користите у апликацији (тренутно <CurrentPageHash>), ваш кориснички агент (<CurrentUserAgent>) и резолуцију вашег уређаја (<CurrentDeviceResolution>).",
|
|
||||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Ако страница садржи поверљиве податке (као што је назив собе, корисника или ИБ-ја групе), ти подаци се уклањају пре слања на сервер.",
|
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Ако страница садржи поверљиве податке (као што је назив собе, корисника или ИБ-ја групе), ти подаци се уклањају пре слања на сервер.",
|
||||||
"%(oldDisplayName)s changed their display name to %(displayName)s.": "Корисник %(oldDisplayName)s је променио приказно име у %(displayName)s.",
|
"%(oldDisplayName)s changed their display name to %(displayName)s.": "Корисник %(oldDisplayName)s је променио приказно име у %(displayName)s.",
|
||||||
"Failed to set direct chat tag": "Нисам успео да поставим ознаку директног ћаскања",
|
"Failed to set direct chat tag": "Нисам успео да поставим ознаку директног ћаскања",
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
"Decryption error": "解密出错",
|
"Decryption error": "解密出错",
|
||||||
"Delete": "删除",
|
"Delete": "删除",
|
||||||
"Default": "默认",
|
"Default": "默认",
|
||||||
"Device ID": "设备识别码",
|
"Device ID": "设备 ID",
|
||||||
"Devices": "设备列表",
|
"Devices": "设备列表",
|
||||||
"Devices will not yet be able to decrypt history from before they joined the room": "新加入聊天室的设备不能解密加入之前的聊天记录",
|
"Devices will not yet be able to decrypt history from before they joined the room": "新加入聊天室的设备不能解密加入之前的聊天记录",
|
||||||
"Direct chats": "私聊",
|
"Direct chats": "私聊",
|
||||||
|
@ -20,8 +20,8 @@
|
||||||
"Don't send typing notifications": "不要发送我的打字状态",
|
"Don't send typing notifications": "不要发送我的打字状态",
|
||||||
"Download %(text)s": "下载 %(text)s",
|
"Download %(text)s": "下载 %(text)s",
|
||||||
"Email": "电子邮箱",
|
"Email": "电子邮箱",
|
||||||
"Email address": "电子邮箱地址",
|
"Email address": "邮箱地址",
|
||||||
"Email, name or matrix ID": "电子邮箱,姓名或者matrix ID",
|
"Email, name or matrix ID": "邮箱地址,名称或者Matrix ID",
|
||||||
"Emoji": "表情",
|
"Emoji": "表情",
|
||||||
"Enable encryption": "启用加密",
|
"Enable encryption": "启用加密",
|
||||||
"Encrypted messages will not be visible on clients that do not yet implement encryption": "不支持加密的客户端将看不到加密的消息",
|
"Encrypted messages will not be visible on clients that do not yet implement encryption": "不支持加密的客户端将看不到加密的消息",
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
"%(senderName)s ended the call.": "%(senderName)s 结束了通话。.",
|
"%(senderName)s ended the call.": "%(senderName)s 结束了通话。.",
|
||||||
"End-to-end encryption information": "端到端加密信息",
|
"End-to-end encryption information": "端到端加密信息",
|
||||||
"End-to-end encryption is in beta and may not be reliable": "端到端加密现为 beta 版,不一定可靠",
|
"End-to-end encryption is in beta and may not be reliable": "端到端加密现为 beta 版,不一定可靠",
|
||||||
"Enter Code": "输入代码",
|
"Enter Code": "输入验证码",
|
||||||
"Error": "错误",
|
"Error": "错误",
|
||||||
"Error decrypting attachment": "解密附件时出错",
|
"Error decrypting attachment": "解密附件时出错",
|
||||||
"Event information": "事件信息",
|
"Event information": "事件信息",
|
||||||
|
@ -39,8 +39,8 @@
|
||||||
"Failed to change password. Is your password correct?": "修改密码失败。确认原密码输入正确吗?",
|
"Failed to change password. Is your password correct?": "修改密码失败。确认原密码输入正确吗?",
|
||||||
"Failed to forget room %(errCode)s": "忘记聊天室失败,错误代码: %(errCode)s",
|
"Failed to forget room %(errCode)s": "忘记聊天室失败,错误代码: %(errCode)s",
|
||||||
"Failed to join room": "无法加入聊天室",
|
"Failed to join room": "无法加入聊天室",
|
||||||
"Failed to kick": "踢人失败",
|
"Failed to kick": "移除失败",
|
||||||
"Failed to leave room": "无法离开聊天室",
|
"Failed to leave room": "无法退出聊天室",
|
||||||
"Failed to load timeline position": "无法加载时间轴位置",
|
"Failed to load timeline position": "无法加载时间轴位置",
|
||||||
"Failed to lookup current room": "找不到当前聊天室",
|
"Failed to lookup current room": "找不到当前聊天室",
|
||||||
"Failed to mute user": "禁言用户失败",
|
"Failed to mute user": "禁言用户失败",
|
||||||
|
@ -48,7 +48,7 @@
|
||||||
"Failed to reject invitation": "拒绝邀请失败",
|
"Failed to reject invitation": "拒绝邀请失败",
|
||||||
"Failed to save settings": "保存设置失败",
|
"Failed to save settings": "保存设置失败",
|
||||||
"Failed to send email": "发送邮件失败",
|
"Failed to send email": "发送邮件失败",
|
||||||
"Failed to send request.": "发送请求失败。",
|
"Failed to send request.": "请求发送失败。",
|
||||||
"Failed to set avatar.": "设置头像失败。.",
|
"Failed to set avatar.": "设置头像失败。.",
|
||||||
"Failed to set display name": "设置昵称失败",
|
"Failed to set display name": "设置昵称失败",
|
||||||
"Failed to set up conference call": "无法启动群组通话",
|
"Failed to set up conference call": "无法启动群组通话",
|
||||||
|
@ -104,7 +104,7 @@
|
||||||
"Server may be unavailable or overloaded": "服务器可能不可用或者超载",
|
"Server may be unavailable or overloaded": "服务器可能不可用或者超载",
|
||||||
"Server may be unavailable, overloaded, or search timed out :(": "服务器可能不可用、超载,或者搜索超时 :(",
|
"Server may be unavailable, overloaded, or search timed out :(": "服务器可能不可用、超载,或者搜索超时 :(",
|
||||||
"Server may be unavailable, overloaded, or the file too big": "服务器可能不可用、超载,或者文件过大",
|
"Server may be unavailable, overloaded, or the file too big": "服务器可能不可用、超载,或者文件过大",
|
||||||
"Server may be unavailable, overloaded, or you hit a bug.": "服务器可能不可用、超载,或者你遇到了一个漏洞.",
|
"Server may be unavailable, overloaded, or you hit a bug.": "服务器可能不可用、超载,或者你遇到了一个 bug。",
|
||||||
"Server unavailable, overloaded, or something else went wrong.": "服务器可能不可用、超载,或者其他东西出错了.",
|
"Server unavailable, overloaded, or something else went wrong.": "服务器可能不可用、超载,或者其他东西出错了.",
|
||||||
"Session ID": "会话 ID",
|
"Session ID": "会话 ID",
|
||||||
"%(senderName)s set a profile picture.": "%(senderName)s 设置了头像。.",
|
"%(senderName)s set a profile picture.": "%(senderName)s 设置了头像。.",
|
||||||
|
@ -129,11 +129,11 @@
|
||||||
"The file '%(fileName)s' exceeds this home server's size limit for uploads": "文件 '%(fileName)s' 超过了此主服务器的上传大小限制",
|
"The file '%(fileName)s' exceeds this home server's size limit for uploads": "文件 '%(fileName)s' 超过了此主服务器的上传大小限制",
|
||||||
"The file '%(fileName)s' failed to upload": "文件 '%(fileName)s' 上传失败",
|
"The file '%(fileName)s' failed to upload": "文件 '%(fileName)s' 上传失败",
|
||||||
"Add email address": "添加邮件地址",
|
"Add email address": "添加邮件地址",
|
||||||
"Add phone number": "添加电话号码",
|
"Add phone number": "添加手机号码",
|
||||||
"Advanced": "高级",
|
"Advanced": "高级",
|
||||||
"Algorithm": "算法",
|
"Algorithm": "算法",
|
||||||
"Always show message timestamps": "总是显示消息时间戳",
|
"Always show message timestamps": "总是显示消息时间戳",
|
||||||
"%(names)s and %(lastPerson)s are typing": "%(names)s 和 %(lastPerson)s 正在打字",
|
"%(names)s and %(lastPerson)s are typing": "%(names)s 和 %(lastPerson)s 正在输入",
|
||||||
"A new password must be entered.": "一个新的密码必须被输入。.",
|
"A new password must be entered.": "一个新的密码必须被输入。.",
|
||||||
"%(senderName)s answered the call.": "%(senderName)s 接了通话。.",
|
"%(senderName)s answered the call.": "%(senderName)s 接了通话。.",
|
||||||
"An error has occurred.": "一个错误出现了。",
|
"An error has occurred.": "一个错误出现了。",
|
||||||
|
@ -152,7 +152,7 @@
|
||||||
"%(targetName)s joined the room.": "%(targetName)s 已加入聊天室。",
|
"%(targetName)s joined the room.": "%(targetName)s 已加入聊天室。",
|
||||||
"Jump to first unread message.": "跳到第一条未读消息。",
|
"Jump to first unread message.": "跳到第一条未读消息。",
|
||||||
"%(senderName)s kicked %(targetName)s.": "%(senderName)s 把 %(targetName)s 踢出了聊天室。.",
|
"%(senderName)s kicked %(targetName)s.": "%(senderName)s 把 %(targetName)s 踢出了聊天室。.",
|
||||||
"Leave room": "离开聊天室",
|
"Leave room": "退出聊天室",
|
||||||
"Login as guest": "以游客的身份登录",
|
"Login as guest": "以游客的身份登录",
|
||||||
"New password": "新密码",
|
"New password": "新密码",
|
||||||
"Add a topic": "添加一个主题",
|
"Add a topic": "添加一个主题",
|
||||||
|
@ -177,10 +177,10 @@
|
||||||
"Anyone who knows the room's link, apart from guests": "任何知道聊天室链接的人,游客除外",
|
"Anyone who knows the room's link, apart from guests": "任何知道聊天室链接的人,游客除外",
|
||||||
"Anyone who knows the room's link, including guests": "任何知道聊天室链接的人,包括游客",
|
"Anyone who knows the room's link, including guests": "任何知道聊天室链接的人,包括游客",
|
||||||
"Are you sure?": "你确定吗?",
|
"Are you sure?": "你确定吗?",
|
||||||
"Are you sure you want to leave the room '%(roomName)s'?": "你确定要离开聊天室 “%(roomName)s” 吗?",
|
"Are you sure you want to leave the room '%(roomName)s'?": "你确定要退出聊天室 “%(roomName)s” 吗?",
|
||||||
"Are you sure you want to reject the invitation?": "你确定要拒绝邀请吗?",
|
"Are you sure you want to reject the invitation?": "你确定要拒绝邀请吗?",
|
||||||
"Are you sure you want to upload the following files?": "你确定要上传这些文件吗?",
|
"Are you sure you want to upload the following files?": "你确定要上传这些文件吗?",
|
||||||
"Bans user with given id": "封禁指定 ID 的用户",
|
"Bans user with given id": "按照 ID 封禁指定的用户",
|
||||||
"Blacklisted": "已列入黑名单",
|
"Blacklisted": "已列入黑名单",
|
||||||
"Bulk Options": "批量操作",
|
"Bulk Options": "批量操作",
|
||||||
"Call Timeout": "通话超时",
|
"Call Timeout": "通话超时",
|
||||||
|
@ -210,8 +210,8 @@
|
||||||
"Conference calling is in development and may not be reliable.": "视频会议功能还在开发状态,可能不稳定。",
|
"Conference calling is in development and may not be reliable.": "视频会议功能还在开发状态,可能不稳定。",
|
||||||
"Conference calls are not supported in encrypted rooms": "加密聊天室不支持视频会议",
|
"Conference calls are not supported in encrypted rooms": "加密聊天室不支持视频会议",
|
||||||
"Conference calls are not supported in this client": "此客户端不支持视频会议",
|
"Conference calls are not supported in this client": "此客户端不支持视频会议",
|
||||||
"%(count)s new messages|one": "%(count)s 条新消息",
|
"%(count)s new messages|one": "%(count)s 条未读消息",
|
||||||
"%(count)s new messages|other": "%(count)s 新消息",
|
"%(count)s new messages|other": "%(count)s 未读消息",
|
||||||
"Create a new chat or reuse an existing one": "创建新聊天或使用已有的聊天",
|
"Create a new chat or reuse an existing one": "创建新聊天或使用已有的聊天",
|
||||||
"Custom": "自定义",
|
"Custom": "自定义",
|
||||||
"Custom level": "自定义级别",
|
"Custom level": "自定义级别",
|
||||||
|
@ -222,7 +222,7 @@
|
||||||
"Device key:": "设备密钥 :",
|
"Device key:": "设备密钥 :",
|
||||||
"Disable Notifications": "关闭消息通知",
|
"Disable Notifications": "关闭消息通知",
|
||||||
"Drop File Here": "把文件拖拽到这里",
|
"Drop File Here": "把文件拖拽到这里",
|
||||||
"Email address (optional)": "电子邮件地址 (可选)",
|
"Email address (optional)": "邮箱地址 (可选)",
|
||||||
"Enable Notifications": "启用消息通知",
|
"Enable Notifications": "启用消息通知",
|
||||||
"Encrypted by a verified device": "由一个已验证的设备加密",
|
"Encrypted by a verified device": "由一个已验证的设备加密",
|
||||||
"Encrypted by an unverified device": "由一个未经验证的设备加密",
|
"Encrypted by an unverified device": "由一个未经验证的设备加密",
|
||||||
|
@ -232,31 +232,31 @@
|
||||||
"Error: Problem communicating with the given homeserver.": "错误: 与指定的主服务器通信时出错。",
|
"Error: Problem communicating with the given homeserver.": "错误: 与指定的主服务器通信时出错。",
|
||||||
"Export": "导出",
|
"Export": "导出",
|
||||||
"Failed to fetch avatar URL": "获取 Avatar URL 失败",
|
"Failed to fetch avatar URL": "获取 Avatar URL 失败",
|
||||||
"Failed to upload profile picture!": "无法上传头像!",
|
"Failed to upload profile picture!": "头像上传失败!",
|
||||||
"Guest access is disabled on this Home Server.": "此服务器禁用了游客访问。",
|
"Guest access is disabled on this Home Server.": "此服务器禁用了游客访问。",
|
||||||
"Home": "主页面",
|
"Home": "主页面",
|
||||||
"Import": "导入",
|
"Import": "导入",
|
||||||
"Incoming call from %(name)s": "来自 %(name)s 的通话",
|
"Incoming call from %(name)s": "来自 %(name)s 的通话",
|
||||||
"Incoming video call from %(name)s": "来自 %(name)s 的视频通话",
|
"Incoming video call from %(name)s": "来自 %(name)s 的视频通话",
|
||||||
"Incoming voice call from %(name)s": "来自 %(name)s 的视频通话",
|
"Incoming voice call from %(name)s": "来自 %(name)s 的语音通话",
|
||||||
"Incorrect username and/or password.": "用户名或密码错误。",
|
"Incorrect username and/or password.": "用户名或密码错误。",
|
||||||
"%(senderName)s invited %(targetName)s.": "%(senderName)s 邀请了 %(targetName)s。",
|
"%(senderName)s invited %(targetName)s.": "%(senderName)s 邀请了 %(targetName)s。",
|
||||||
"Invited": "已邀请",
|
"Invited": "已邀请",
|
||||||
"Invites": "邀请",
|
"Invites": "邀请",
|
||||||
"Invites user with given id to current room": "邀请指定 ID 的用户加入当前聊天室",
|
"Invites user with given id to current room": "按照 ID 邀请指定用户加入当前聊天室",
|
||||||
"'%(alias)s' is not a valid format for an address": "'%(alias)s' 不是一个合法的电子邮件地址格式",
|
"'%(alias)s' is not a valid format for an address": "'%(alias)s' 不是一个合法的邮箱地址格式",
|
||||||
"'%(alias)s' is not a valid format for an alias": "'%(alias)s' 不是一个合法的昵称格式",
|
"'%(alias)s' is not a valid format for an alias": "'%(alias)s' 不是一个合法的昵称格式",
|
||||||
"%(displayName)s is typing": "%(displayName)s 正在打字",
|
"%(displayName)s is typing": "%(displayName)s 正在输入",
|
||||||
"Sign in with": "第三方登录",
|
"Sign in with": "第三方登录",
|
||||||
"Message not sent due to unknown devices being present": "消息未发送,因为有未知的设备存在",
|
"Message not sent due to unknown devices being present": "消息未发送,因为有未知的设备存在",
|
||||||
"Missing room_id in request": "请求中没有 room_id",
|
"Missing room_id in request": "请求中没有 聊天室 ID",
|
||||||
"Missing user_id in request": "请求中没有 user_id",
|
"Missing user_id in request": "请求中没有 user_id",
|
||||||
"Mobile phone number": "手机号码",
|
"Mobile phone number": "手机号码",
|
||||||
"Mobile phone number (optional)": "手机号码 (可选)",
|
"Mobile phone number (optional)": "手机号码 (可选)",
|
||||||
"Moderator": "协管员",
|
"Moderator": "协管员",
|
||||||
"Mute": "静音",
|
"Mute": "静音",
|
||||||
"Name": "姓名",
|
"Name": "姓名",
|
||||||
"Never send encrypted messages to unverified devices from this device": "不要从此设备向未验证的设备发送消息",
|
"Never send encrypted messages to unverified devices from this device": "在此设备上不向未经验证的设备发送消息",
|
||||||
"New passwords don't match": "两次输入的新密码不符",
|
"New passwords don't match": "两次输入的新密码不符",
|
||||||
"none": "无",
|
"none": "无",
|
||||||
"not set": "未设置",
|
"not set": "未设置",
|
||||||
|
@ -274,7 +274,7 @@
|
||||||
"Password:": "密码:",
|
"Password:": "密码:",
|
||||||
"Passwords can't be empty": "密码不能为空",
|
"Passwords can't be empty": "密码不能为空",
|
||||||
"Permissions": "权限",
|
"Permissions": "权限",
|
||||||
"Phone": "电话",
|
"Phone": "手机号码",
|
||||||
"Cancel": "取消",
|
"Cancel": "取消",
|
||||||
"Create new room": "创建新聊天室",
|
"Create new room": "创建新聊天室",
|
||||||
"Custom Server Options": "自定义服务器选项",
|
"Custom Server Options": "自定义服务器选项",
|
||||||
|
@ -293,7 +293,7 @@
|
||||||
"Edit": "编辑",
|
"Edit": "编辑",
|
||||||
"Joins room with given alias": "以指定的别名加入聊天室",
|
"Joins room with given alias": "以指定的别名加入聊天室",
|
||||||
"Labs": "实验室",
|
"Labs": "实验室",
|
||||||
"%(targetName)s left the room.": "%(targetName)s 离开了聊天室。",
|
"%(targetName)s left the room.": "%(targetName)s 退出了聊天室。",
|
||||||
"Logged in as:": "登录为:",
|
"Logged in as:": "登录为:",
|
||||||
"Logout": "登出",
|
"Logout": "登出",
|
||||||
"Low priority": "低优先级",
|
"Low priority": "低优先级",
|
||||||
|
@ -365,11 +365,11 @@
|
||||||
"Unverify": "取消验证",
|
"Unverify": "取消验证",
|
||||||
"ex. @bob:example.com": "例如 @bob:example.com",
|
"ex. @bob:example.com": "例如 @bob:example.com",
|
||||||
"Add User": "添加用户",
|
"Add User": "添加用户",
|
||||||
"This Home Server would like to make sure you are not a robot": "这个Home Server想要确认你不是一个机器人",
|
"This Home Server would like to make sure you are not a robot": "此主服务器想确认你不是机器人",
|
||||||
"Token incorrect": "令牌错误",
|
"Token incorrect": "令牌错误",
|
||||||
"Default server": "默认服务器",
|
"Default server": "默认服务器",
|
||||||
"Custom server": "自定义服务器",
|
"Custom server": "自定义服务器",
|
||||||
"URL Previews": "URL 预览",
|
"URL Previews": "链接预览",
|
||||||
"Drop file here to upload": "把文件拖到这里以上传",
|
"Drop file here to upload": "把文件拖到这里以上传",
|
||||||
"Online": "在线",
|
"Online": "在线",
|
||||||
"Idle": "空闲",
|
"Idle": "空闲",
|
||||||
|
@ -395,8 +395,8 @@
|
||||||
"Drop here to tag %(section)s": "拖拽到这里标记 %(section)s",
|
"Drop here to tag %(section)s": "拖拽到这里标记 %(section)s",
|
||||||
"Enable automatic language detection for syntax highlighting": "启用自动语言检测用于语法高亮",
|
"Enable automatic language detection for syntax highlighting": "启用自动语言检测用于语法高亮",
|
||||||
"Failed to change power level": "修改特权级别失败",
|
"Failed to change power level": "修改特权级别失败",
|
||||||
"Kick": "踢出",
|
"Kick": "移除",
|
||||||
"Kicks user with given id": "踢出指定 ID 的用户",
|
"Kicks user with given id": "按照 ID 移除特定的用户",
|
||||||
"Last seen": "上次看见",
|
"Last seen": "上次看见",
|
||||||
"Level:": "级别:",
|
"Level:": "级别:",
|
||||||
"Local addresses for this room:": "这个聊天室的本地地址:",
|
"Local addresses for this room:": "这个聊天室的本地地址:",
|
||||||
|
@ -416,9 +416,9 @@
|
||||||
"Sets the room topic": "设置聊天室主题",
|
"Sets the room topic": "设置聊天室主题",
|
||||||
"Show Text Formatting Toolbar": "显示文字格式工具栏",
|
"Show Text Formatting Toolbar": "显示文字格式工具栏",
|
||||||
"This room has no local addresses": "这个聊天室没有本地地址",
|
"This room has no local addresses": "这个聊天室没有本地地址",
|
||||||
"This doesn't appear to be a valid email address": "这看起来不是一个合法的电子邮件地址",
|
"This doesn't appear to be a valid email address": "这看起来不是一个合法的邮箱地址",
|
||||||
"This is a preview of this room. Room interactions have been disabled": "这是这个聊天室的一个预览。聊天室交互已禁用",
|
"This is a preview of this room. Room interactions have been disabled": "这是这个聊天室的一个预览。聊天室交互已禁用",
|
||||||
"This phone number is already in use": "此电话号码已被使用",
|
"This phone number is already in use": "此手机号码已被使用",
|
||||||
"This room": "这个聊天室",
|
"This room": "这个聊天室",
|
||||||
"This room is not accessible by remote Matrix servers": "这个聊天室无法被远程 Matrix 服务器访问",
|
"This room is not accessible by remote Matrix servers": "这个聊天室无法被远程 Matrix 服务器访问",
|
||||||
"This room's internal ID is": "这个聊天室的内部 ID 是",
|
"This room's internal ID is": "这个聊天室的内部 ID 是",
|
||||||
|
@ -433,7 +433,7 @@
|
||||||
"Unencrypted room": "未加密的聊天室",
|
"Unencrypted room": "未加密的聊天室",
|
||||||
"unencrypted": "未加密的",
|
"unencrypted": "未加密的",
|
||||||
"Unencrypted message": "未加密的消息",
|
"Unencrypted message": "未加密的消息",
|
||||||
"unknown caller": "未知的呼叫者",
|
"unknown caller": "未知呼叫者",
|
||||||
"unknown device": "未知设备",
|
"unknown device": "未知设备",
|
||||||
"Unnamed Room": "未命名的聊天室",
|
"Unnamed Room": "未命名的聊天室",
|
||||||
"Unverified": "未验证",
|
"Unverified": "未验证",
|
||||||
|
@ -449,10 +449,10 @@
|
||||||
"Passwords don't match.": "密码不匹配。",
|
"Passwords don't match.": "密码不匹配。",
|
||||||
"I already have an account": "我已经有一个帐号",
|
"I already have an account": "我已经有一个帐号",
|
||||||
"Unblacklist": "移出黑名单",
|
"Unblacklist": "移出黑名单",
|
||||||
"Not a valid Riot keyfile": "不是一个合法的 Riot 密钥文件",
|
"Not a valid Riot keyfile": "不是一个有效的 Riot 密钥文件",
|
||||||
"%(targetName)s accepted an invitation.": "%(targetName)s 接受了一个邀请。",
|
"%(targetName)s accepted an invitation.": "%(targetName)s 接受了一个邀请。",
|
||||||
"Do you want to load widget from URL:": "你想从此 URL 加载小组件吗:",
|
"Do you want to load widget from URL:": "你想从此 URL 加载小组件吗:",
|
||||||
"Hide join/leave messages (invites/kicks/bans unaffected)": "隐藏加入/离开消息(邀请/踢出/封禁不受影响)",
|
"Hide join/leave messages (invites/kicks/bans unaffected)": "隐藏加入/退出消息(邀请/踢出/封禁不受影响)",
|
||||||
"Integrations Error": "集成错误",
|
"Integrations Error": "集成错误",
|
||||||
"Publish this room to the public in %(domain)s's room directory?": "把这个聊天室发布到 %(domain)s 的聊天室目录吗?",
|
"Publish this room to the public in %(domain)s's room directory?": "把这个聊天室发布到 %(domain)s 的聊天室目录吗?",
|
||||||
"Manage Integrations": "管理集成",
|
"Manage Integrations": "管理集成",
|
||||||
|
@ -464,12 +464,12 @@
|
||||||
"%(senderName)s requested a VoIP conference.": "%(senderName)s 请求一个 VoIP 会议。",
|
"%(senderName)s requested a VoIP conference.": "%(senderName)s 请求一个 VoIP 会议。",
|
||||||
"Seen by %(userName)s at %(dateTime)s": "在 %(dateTime)s 被 %(userName)s 看到",
|
"Seen by %(userName)s at %(dateTime)s": "在 %(dateTime)s 被 %(userName)s 看到",
|
||||||
"Tagged as: ": "标记为: ",
|
"Tagged as: ": "标记为: ",
|
||||||
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "验证码将发送到+%(msisdn)s,请输入接收到的验证码",
|
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "验证码将发送至 +%(msisdn)s,请输入收到的验证码",
|
||||||
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s 接受了 %(displayName)s 的邀请。",
|
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s 接受了 %(displayName)s 的邀请。",
|
||||||
"Active call (%(roomName)s)": "%(roomName)s 的呼叫",
|
"Active call (%(roomName)s)": "当前通话 (来自聊天室 %(roomName)s)",
|
||||||
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s 将级别调整到%(powerLevelDiffText)s 。",
|
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s 将级别调整到%(powerLevelDiffText)s 。",
|
||||||
"Changes colour scheme of current room": "修改了样式",
|
"Changes colour scheme of current room": "修改了样式",
|
||||||
"Deops user with given id": "Deops user",
|
"Deops user with given id": "按照 ID 取消特定用户的管理员权限",
|
||||||
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.": "通过 <voiceText>语言</voiceText> 或者 <videoText>视频</videoText>加入.",
|
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.": "通过 <voiceText>语言</voiceText> 或者 <videoText>视频</videoText>加入.",
|
||||||
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s 设定历史浏览功能为 所有聊天室成员,从他们被邀请开始.",
|
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s 设定历史浏览功能为 所有聊天室成员,从他们被邀请开始.",
|
||||||
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s 设定历史浏览功能为 所有聊天室成员,从他们加入开始.",
|
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s 设定历史浏览功能为 所有聊天室成员,从他们加入开始.",
|
||||||
|
@ -486,16 +486,16 @@
|
||||||
"%(roomName)s is not accessible at this time.": "%(roomName)s 此时无法访问。",
|
"%(roomName)s is not accessible at this time.": "%(roomName)s 此时无法访问。",
|
||||||
"Start authentication": "开始认证",
|
"Start authentication": "开始认证",
|
||||||
"The maximum permitted number of widgets have already been added to this room.": "小部件的最大允许数量已经添加到这个聊天室了。",
|
"The maximum permitted number of widgets have already been added to this room.": "小部件的最大允许数量已经添加到这个聊天室了。",
|
||||||
"The phone number entered looks invalid": "输入的电话号码看起来无效",
|
"The phone number entered looks invalid": "输入的手机号码看起来无效",
|
||||||
"The remote side failed to pick up": "远端未能接收到",
|
"The remote side failed to pick up": "远端未能接收到",
|
||||||
"This Home Server does not support login using email address.": "HS不支持使用电子邮件地址登陆。",
|
"This Home Server does not support login using email address.": "HS不支持使用邮箱地址登陆。",
|
||||||
"This invitation was sent to an email address which is not associated with this account:": "此邀请被发送到与此帐户不相关的电子邮件地址:",
|
"This invitation was sent to an email address which is not associated with this account:": "此邀请被发送到与此帐户不相关的邮箱地址:",
|
||||||
"This room is not recognised.": "无法识别此聊天室。",
|
"This room is not recognised.": "无法识别此聊天室。",
|
||||||
"To get started, please pick a username!": "请点击用户名!",
|
"To get started, please pick a username!": "请点击用户名!",
|
||||||
"Unable to add email address": "无法添加电子邮件地址",
|
"Unable to add email address": "无法添加邮箱地址",
|
||||||
"Automatically replace plain text Emoji": "文字、表情自动转换",
|
"Automatically replace plain text Emoji": "文字、表情自动转换",
|
||||||
"To reset your password, enter the email address linked to your account": "要重置你的密码,请输入关联你的帐号的电子邮箱地址",
|
"To reset your password, enter the email address linked to your account": "要重置你的密码,请输入关联你的帐号的邮箱地址",
|
||||||
"Unable to verify email address.": "无法验证电子邮箱地址。",
|
"Unable to verify email address.": "无法验证邮箱地址。",
|
||||||
"Unknown room %(roomId)s": "未知聊天室 %(roomId)s",
|
"Unknown room %(roomId)s": "未知聊天室 %(roomId)s",
|
||||||
"Unknown (user, device) pair:": "未知(用户,设备)对:",
|
"Unknown (user, device) pair:": "未知(用户,设备)对:",
|
||||||
"Unrecognised command:": "无法识别的命令:",
|
"Unrecognised command:": "无法识别的命令:",
|
||||||
|
@ -515,18 +515,18 @@
|
||||||
"You cannot place VoIP calls in this browser.": "你不能在这个浏览器中发起 VoIP 通话。",
|
"You cannot place VoIP calls in this browser.": "你不能在这个浏览器中发起 VoIP 通话。",
|
||||||
"You do not have permission to post to this room": "你没有发送到这个聊天室的权限",
|
"You do not have permission to post to this room": "你没有发送到这个聊天室的权限",
|
||||||
"You have been invited to join this room by %(inviterName)s": "你已经被 %(inviterName)s 邀请加入这个聊天室",
|
"You have been invited to join this room by %(inviterName)s": "你已经被 %(inviterName)s 邀请加入这个聊天室",
|
||||||
"You seem to be in a call, are you sure you want to quit?": "你好像在一个通话中,你确定要退出吗?",
|
"You seem to be in a call, are you sure you want to quit?": "您似乎正在进行通话,确定要退出吗?",
|
||||||
"You seem to be uploading files, are you sure you want to quit?": "你好像正在上传文件,你确定要退出吗?",
|
"You seem to be uploading files, are you sure you want to quit?": "您似乎正在上传文件,确定要退出吗?",
|
||||||
"You should not yet trust it to secure data": "你不应该相信它来保护你的数据",
|
"You should not yet trust it to secure data": "你不应该相信它来保护你的数据",
|
||||||
"Upload an avatar:": "上传一个头像:",
|
"Upload an avatar:": "上传一个头像:",
|
||||||
"This doesn't look like a valid email address.": "这看起来不是一个合法的电子邮件地址。",
|
"This doesn't look like a valid email address.": "这看起来不是一个合法的邮箱地址。",
|
||||||
"This doesn't look like a valid phone number.": "这看起来不是一个合法的电话号码。",
|
"This doesn't look like a valid phone number.": "这看起来不是一个合法的手机号码。",
|
||||||
"User names may only contain letters, numbers, dots, hyphens and underscores.": "用户名只可以包含字母、数字、点、连字号和下划线。",
|
"User names may only contain letters, numbers, dots, hyphens and underscores.": "用户名只可以包含字母、数字、点、连字号和下划线。",
|
||||||
"An unknown error occurred.": "一个未知错误出现了。",
|
"An unknown error occurred.": "一个未知错误出现了。",
|
||||||
"An error occurred: %(error_string)s": "一个错误出现了: %(error_string)s",
|
"An error occurred: %(error_string)s": "一个错误出现了: %(error_string)s",
|
||||||
"Encrypt room": "加密聊天室",
|
"Encrypt room": "加密聊天室",
|
||||||
"There are no visible files in this room": "这个聊天室里面没有可见的文件",
|
"There are no visible files in this room": "这个聊天室里面没有可见的文件",
|
||||||
"Active call": "活跃的通话",
|
"Active call": "当前通话",
|
||||||
"Verify...": "验证...",
|
"Verify...": "验证...",
|
||||||
"Error decrypting audio": "解密音频时出错",
|
"Error decrypting audio": "解密音频时出错",
|
||||||
"Error decrypting image": "解密图像时出错",
|
"Error decrypting image": "解密图像时出错",
|
||||||
|
@ -537,7 +537,7 @@
|
||||||
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s 移除了聊天室头像。",
|
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s 移除了聊天室头像。",
|
||||||
"Something went wrong!": "出了点问题!",
|
"Something went wrong!": "出了点问题!",
|
||||||
"If you already have a Matrix account you can <a>log in</a> instead.": "如果你已经有一个 Matrix 帐号,你可以<a>登录</a>。",
|
"If you already have a Matrix account you can <a>log in</a> instead.": "如果你已经有一个 Matrix 帐号,你可以<a>登录</a>。",
|
||||||
"Do you want to set an email address?": "你要设置一个电子邮箱地址吗?",
|
"Do you want to set an email address?": "你要设置一个邮箱地址吗?",
|
||||||
"New address (e.g. #foo:%(localDomain)s)": "新的地址(例如 #foo:%(localDomain)s)",
|
"New address (e.g. #foo:%(localDomain)s)": "新的地址(例如 #foo:%(localDomain)s)",
|
||||||
"Upload new:": "上传新的:",
|
"Upload new:": "上传新的:",
|
||||||
"User ID": "用户 ID",
|
"User ID": "用户 ID",
|
||||||
|
@ -547,16 +547,16 @@
|
||||||
"WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "警告:密钥验证失败!%(userId)s 和 device %(deviceId)s 的签名密钥是 \"%(fprint)s\",和提供的咪呀 \"%(fingerprint)s\" 不匹配。这可能意味着你的通信正在被窃听!",
|
"WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "警告:密钥验证失败!%(userId)s 和 device %(deviceId)s 的签名密钥是 \"%(fprint)s\",和提供的咪呀 \"%(fingerprint)s\" 不匹配。这可能意味着你的通信正在被窃听!",
|
||||||
"%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s 收回了 %(targetName)s 的邀请。",
|
"%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s 收回了 %(targetName)s 的邀请。",
|
||||||
"Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?": "你想要 <acceptText>接受</acceptText> 还是 <declineText>拒绝</declineText> 这个邀请?",
|
"Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?": "你想要 <acceptText>接受</acceptText> 还是 <declineText>拒绝</declineText> 这个邀请?",
|
||||||
"You already have existing direct chats with this user:": "你已经有和这个用户的直接聊天:",
|
"You already have existing direct chats with this user:": "你已经有和此用户的直接聊天:",
|
||||||
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or <RoomDirectoryButton> to browse the directory": "你现在还不再任何聊天室!按下 <CreateRoomButton> 来创建一个聊天室或者 <RoomDirectoryButton> 来浏览目录",
|
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or <RoomDirectoryButton> to browse the directory": "你现在还不再任何聊天室!按下 <CreateRoomButton> 来创建一个聊天室或者 <RoomDirectoryButton> 来浏览目录",
|
||||||
"You cannot place a call with yourself.": "你不能和你自己发起一个通话。",
|
"You cannot place a call with yourself.": "你不能和你自己发起一个通话。",
|
||||||
"You have been kicked from %(roomName)s by %(userName)s.": "你已经被 %(userName)s 踢出了 %(roomName)s.",
|
"You have been kicked from %(roomName)s by %(userName)s.": "你已经被 %(userName)s 踢出了 %(roomName)s.",
|
||||||
"You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "你已经登出了所有的设备并不再接收推送通知。要重新启用通知,请再在每个设备上登录",
|
"You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "你已经登出了所有的设备并不再接收推送通知。要重新启用通知,请再在每个设备上登录",
|
||||||
"You have <a>disabled</a> URL previews by default.": "你已经默认 <a>禁用</a> URL 预览。",
|
"You have <a>disabled</a> URL previews by default.": "你已经默认 <a>禁用</a> 链接预览。",
|
||||||
"You have <a>enabled</a> URL previews by default.": "你已经默认 <a>启用</a> URL 预览。",
|
"You have <a>enabled</a> URL previews by default.": "你已经默认 <a>启用</a> 链接预览。",
|
||||||
"Your home server does not support device management.": "你的 home server 不支持设备管理。",
|
"Your home server does not support device management.": "你的 home server 不支持设备管理。",
|
||||||
"Set a display name:": "设置一个昵称:",
|
"Set a display name:": "设置一个昵称:",
|
||||||
"This server does not support authentication with a phone number.": "这个服务器不支持用电话号码认证。",
|
"This server does not support authentication with a phone number.": "这个服务器不支持用手机号码认证。",
|
||||||
"Password too short (min %(MIN_PASSWORD_LENGTH)s).": "密码过短(最短为 %(MIN_PASSWORD_LENGTH)s)。",
|
"Password too short (min %(MIN_PASSWORD_LENGTH)s).": "密码过短(最短为 %(MIN_PASSWORD_LENGTH)s)。",
|
||||||
"Make this room private": "使这个聊天室私密",
|
"Make this room private": "使这个聊天室私密",
|
||||||
"Share message history with new users": "和新用户共享消息历史",
|
"Share message history with new users": "和新用户共享消息历史",
|
||||||
|
@ -593,7 +593,7 @@
|
||||||
"You must join the room to see its files": "你必须加入聊天室以看到它的文件",
|
"You must join the room to see its files": "你必须加入聊天室以看到它的文件",
|
||||||
"Failed to invite the following users to the %(roomName)s room:": "邀请以下用户到 %(roomName)s 聊天室失败:",
|
"Failed to invite the following users to the %(roomName)s room:": "邀请以下用户到 %(roomName)s 聊天室失败:",
|
||||||
"Confirm Removal": "确认移除",
|
"Confirm Removal": "确认移除",
|
||||||
"This will make your account permanently unusable. You will not be able to re-register the same user ID.": "这会让你的账户永远不可用。你无法重新注册同一个用户 ID.",
|
"This will make your account permanently unusable. You will not be able to re-register the same user ID.": "这将会导致您的账户永远无法使用。你将无法重新注册同样的用户 ID。",
|
||||||
"Verifies a user, device, and pubkey tuple": "验证一个用户、设备和密钥元组",
|
"Verifies a user, device, and pubkey tuple": "验证一个用户、设备和密钥元组",
|
||||||
"We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.": "我们在尝试恢复你之前的会话时遇到了一个错误。如果你继续,你将需要重新登录,加密的聊天历史将会不可读。",
|
"We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.": "我们在尝试恢复你之前的会话时遇到了一个错误。如果你继续,你将需要重新登录,加密的聊天历史将会不可读。",
|
||||||
"Unknown devices": "未知设备",
|
"Unknown devices": "未知设备",
|
||||||
|
@ -609,9 +609,9 @@
|
||||||
"You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.": "你可以使用自定义的服务器选项来通过指定一个不同的主服务器 URL 来登录其他 Matrix 服务器。",
|
"You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.": "你可以使用自定义的服务器选项来通过指定一个不同的主服务器 URL 来登录其他 Matrix 服务器。",
|
||||||
"This allows you to use this app with an existing Matrix account on a different home server.": "这允许你用一个已有在不同主服务器的 Matrix 账户使用这个应用。",
|
"This allows you to use this app with an existing Matrix account on a different home server.": "这允许你用一个已有在不同主服务器的 Matrix 账户使用这个应用。",
|
||||||
"Please check your email to continue registration.": "请查看你的电子邮件以继续注册。",
|
"Please check your email to continue registration.": "请查看你的电子邮件以继续注册。",
|
||||||
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "如果你不指定一个电子邮箱地址,你将不能重置你的密码。你确定吗?",
|
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "如果你不指定一个邮箱地址,你将不能重置你的密码。你确定吗?",
|
||||||
"Home server URL": "主服务器 URL",
|
"Home server URL": "主服务器 URL",
|
||||||
"Identity server URL": "身份服务器 URL",
|
"Identity server URL": "身份认证服务器 URL",
|
||||||
"What does this mean?": "这是什么意思?",
|
"What does this mean?": "这是什么意思?",
|
||||||
"Image '%(Body)s' cannot be displayed.": "图像 '%(Body)s' 无法显示。",
|
"Image '%(Body)s' cannot be displayed.": "图像 '%(Body)s' 无法显示。",
|
||||||
"This image cannot be displayed.": "图像无法显示。",
|
"This image cannot be displayed.": "图像无法显示。",
|
||||||
|
@ -620,12 +620,12 @@
|
||||||
"Ongoing conference call%(supportedText)s.": "正在进行的会议通话 %(supportedText)s.",
|
"Ongoing conference call%(supportedText)s.": "正在进行的会议通话 %(supportedText)s.",
|
||||||
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s 修改了 %(roomName)s 的头像",
|
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s 修改了 %(roomName)s 的头像",
|
||||||
"This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "这将会成为你在 <span></span> 主服务器上的账户名,或者你可以选择一个 <a>不同的服务器</a>。",
|
"This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "这将会成为你在 <span></span> 主服务器上的账户名,或者你可以选择一个 <a>不同的服务器</a>。",
|
||||||
"Your browser does not support the required cryptography extensions": "你的浏览器不支持所需的密码学扩展",
|
"Your browser does not support the required cryptography extensions": "你的浏览器不支持 Riot 所需的密码学特性",
|
||||||
"Authentication check failed: incorrect password?": "身份验证失败:密码错误?",
|
"Authentication check failed: incorrect password?": "身份验证失败:密码错误?",
|
||||||
"This will allow you to reset your password and receive notifications.": "这将允许你重置你的密码和接收通知。",
|
"This will allow you to reset your password and receive notifications.": "这将允许你重置你的密码和接收通知。",
|
||||||
"Share without verifying": "不验证就分享",
|
"Share without verifying": "不验证就分享",
|
||||||
"You added a new device '%(displayName)s', which is requesting encryption keys.": "你添加了一个新的设备 '%(displayName)s',它正在请求加密密钥。",
|
"You added a new device '%(displayName)s', which is requesting encryption keys.": "你添加了一个新的设备 '%(displayName)s',它正在请求加密密钥。",
|
||||||
"Your unverified device '%(displayName)s' is requesting encryption keys.": "你的未验证的设备 '%(displayName)s' 正在请求加密密钥。",
|
"Your unverified device '%(displayName)s' is requesting encryption keys.": "你的未经验证的设备 '%(displayName)s' 正在请求加密密钥。",
|
||||||
"Encryption key request": "加密密钥请求",
|
"Encryption key request": "加密密钥请求",
|
||||||
"Autocomplete Delay (ms):": "自动补全延迟(毫秒):",
|
"Autocomplete Delay (ms):": "自动补全延迟(毫秒):",
|
||||||
"%(widgetName)s widget added by %(senderName)s": "%(widgetName)s 小组建被 %(senderName)s 添加",
|
"%(widgetName)s widget added by %(senderName)s": "%(widgetName)s 小组建被 %(senderName)s 添加",
|
||||||
|
@ -650,18 +650,18 @@
|
||||||
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s 解除了 %(targetName)s 的封禁。",
|
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s 解除了 %(targetName)s 的封禁。",
|
||||||
"(could not connect media)": "(无法连接媒体)",
|
"(could not connect media)": "(无法连接媒体)",
|
||||||
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s 更改了聊天室的置顶消息。",
|
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s 更改了聊天室的置顶消息。",
|
||||||
"%(names)s and %(count)s others are typing|other": "%(names)s 和另外 %(count)s 个人正在打字",
|
"%(names)s and %(count)s others are typing|other": "%(names)s 和另外 %(count)s 个人正在输入",
|
||||||
"%(names)s and %(count)s others are typing|one": "%(names)s 和另一个人正在打字",
|
"%(names)s and %(count)s others are typing|one": "%(names)s 和另一个人正在输入",
|
||||||
"Send": "发送",
|
"Send": "发送",
|
||||||
"Message Pinning": "消息置顶",
|
"Message Pinning": "消息置顶",
|
||||||
"Disable Emoji suggestions while typing": "禁用打字时Emoji建议",
|
"Disable Emoji suggestions while typing": "输入时禁用 Emoji 建议",
|
||||||
"Use compact timeline layout": "使用紧凑的时间线布局",
|
"Use compact timeline layout": "使用紧凑的时间线布局",
|
||||||
"Hide avatar changes": "隐藏头像修改",
|
"Hide avatar changes": "隐藏头像修改",
|
||||||
"Hide display name changes": "隐藏昵称的修改",
|
"Hide display name changes": "隐藏昵称修改",
|
||||||
"Disable big emoji in chat": "禁用聊天中的大Emoji",
|
"Disable big emoji in chat": "禁用聊天中的大Emoji",
|
||||||
"Never send encrypted messages to unverified devices in this room from this device": "在这个聊天室永不从这个设备发送加密消息到未验证的设备",
|
"Never send encrypted messages to unverified devices in this room from this device": "在此设备上,在此聊天室中不向未经验证的设备发送加密的消息",
|
||||||
"Enable URL previews for this room (only affects you)": "在这个聊天室启用 URL 预览(只影响你)",
|
"Enable URL previews for this room (only affects you)": "在此聊天室启用链接预览(只影响你)",
|
||||||
"Enable URL previews by default for participants in this room": "对这个聊天室的参与者默认启用 URL 预览",
|
"Enable URL previews by default for participants in this room": "对这个聊天室的参与者默认启用 链接预览",
|
||||||
"Delete %(count)s devices|other": "删除了 %(count)s 个设备",
|
"Delete %(count)s devices|other": "删除了 %(count)s 个设备",
|
||||||
"Delete %(count)s devices|one": "删除设备",
|
"Delete %(count)s devices|one": "删除设备",
|
||||||
"Select devices": "选择设备",
|
"Select devices": "选择设备",
|
||||||
|
@ -706,10 +706,10 @@
|
||||||
"were kicked %(count)s times|one": "被踢出",
|
"were kicked %(count)s times|one": "被踢出",
|
||||||
"was kicked %(count)s times|other": "被踢出 %(count)s 次",
|
"was kicked %(count)s times|other": "被踢出 %(count)s 次",
|
||||||
"was kicked %(count)s times|one": "被踢出",
|
"was kicked %(count)s times|one": "被踢出",
|
||||||
"%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s 改了他们的名字 %(count)s 次",
|
"%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s 改了他们的名称 %(count)s 次",
|
||||||
"%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s 改了他们的名字",
|
"%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s 改了他们的名称",
|
||||||
"%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s 改了他们的名字 %(count)s 次",
|
"%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s 改了他们的名称 %(count)s 次",
|
||||||
"%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s 改了他们的名字",
|
"%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s 改了他们的名称",
|
||||||
"%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)s 更换了他们的的头像 %(count)s 次",
|
"%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)s 更换了他们的的头像 %(count)s 次",
|
||||||
"%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)s 更换了他们的头像",
|
"%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)s 更换了他们的头像",
|
||||||
"%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s 更换了他们的头像 %(count)s 次",
|
"%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s 更换了他们的头像 %(count)s 次",
|
||||||
|
@ -718,10 +718,10 @@
|
||||||
"%(items)s and %(count)s others|one": "%(items)s 和另一个人",
|
"%(items)s and %(count)s others|one": "%(items)s 和另一个人",
|
||||||
"collapse": "折叠",
|
"collapse": "折叠",
|
||||||
"expand": "展开",
|
"expand": "展开",
|
||||||
"email address": "电子邮箱地址",
|
"email address": "邮箱地址",
|
||||||
"You have entered an invalid address.": "你输入了一个无效地址。",
|
"You have entered an invalid address.": "你输入了一个无效地址。",
|
||||||
"Advanced options": "高级选项",
|
"Advanced options": "高级选项",
|
||||||
"Leave": "离开",
|
"Leave": "退出",
|
||||||
"Description": "描述",
|
"Description": "描述",
|
||||||
"Warning": "警告",
|
"Warning": "警告",
|
||||||
"Light theme": "浅色主题",
|
"Light theme": "浅色主题",
|
||||||
|
@ -738,14 +738,13 @@
|
||||||
"Your homeserver's URL": "您的主服务器的链接",
|
"Your homeserver's URL": "您的主服务器的链接",
|
||||||
"Your identity server's URL": "您的身份认证服务器的链接",
|
"Your identity server's URL": "您的身份认证服务器的链接",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "将要为帮助 Riot.im 发展而发送的信息包含:",
|
"The information being sent to us to help make Riot.im better includes:": "将要为帮助 Riot.im 发展而发送的信息包含:",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "我们也记录了您在本应用中使用的页面(目前为 <CurrentPageHash>), User Agent(<CurrentUserAgent>)和设备的分辨率(<CurrentDeviceResolution>)。",
|
|
||||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "这个页面中含有可能能用于识别您身份的信息,比如聊天室、用户或群组 ID,在它们发送到服务器上之前,这些数据会被移除。",
|
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "这个页面中含有可能能用于识别您身份的信息,比如聊天室、用户或群组 ID,在它们发送到服务器上之前,这些数据会被移除。",
|
||||||
"%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
|
"%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
|
||||||
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s,%(monthName)s %(day)s %(time)s",
|
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s,%(monthName)s %(day)s %(time)s",
|
||||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s,%(monthName)s %(day)s %(fullYear)s",
|
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s,%(monthName)s %(day)s %(fullYear)s",
|
||||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s,%(monthName)s %(day)s %(fullYear)s %(time)s",
|
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s,%(monthName)s %(day)s %(fullYear)s %(time)s",
|
||||||
"Who would you like to add to this community?": "您想把谁添加到这个社区内?",
|
"Who would you like to add to this community?": "您想把谁添加到这个社区内?",
|
||||||
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "警告:您添加的用户对一切知道这个社区的 ID 的人公开",
|
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "警告:您添加的一切用户都将会对一切知道此社区的 ID 的人公开",
|
||||||
"Name or matrix ID": "名称或 Matrix ID",
|
"Name or matrix ID": "名称或 Matrix ID",
|
||||||
"Which rooms would you like to add to this community?": "您想把哪个聊天室添加到这个社区中?",
|
"Which rooms would you like to add to this community?": "您想把哪个聊天室添加到这个社区中?",
|
||||||
"Add rooms to the community": "添加聊天室到社区",
|
"Add rooms to the community": "添加聊天室到社区",
|
||||||
|
@ -753,11 +752,11 @@
|
||||||
"Failed to invite users to community": "邀请用户到社区失败",
|
"Failed to invite users to community": "邀请用户到社区失败",
|
||||||
"Message Replies": "消息回复",
|
"Message Replies": "消息回复",
|
||||||
"Disable Peer-to-Peer for 1:1 calls": "在1:1通话中禁用点到点",
|
"Disable Peer-to-Peer for 1:1 calls": "在1:1通话中禁用点到点",
|
||||||
"Enable inline URL previews by default": "默认启用自动网址预览",
|
"Enable inline URL previews by default": "默认启用网址预览",
|
||||||
"Disinvite this user?": "取消邀请这个用户?",
|
"Disinvite this user?": "取消邀请此用户?",
|
||||||
"Kick this user?": "踢出这个用户?",
|
"Kick this user?": "移除此用户?",
|
||||||
"Unban this user?": "解除这个用户的封禁?",
|
"Unban this user?": "解除此用户的封禁?",
|
||||||
"Ban this user?": "封紧这个用户?",
|
"Ban this user?": "封紧此用户?",
|
||||||
"Send an encrypted reply…": "发送加密的回复…",
|
"Send an encrypted reply…": "发送加密的回复…",
|
||||||
"Send a reply (unencrypted)…": "发送回复(未加密)…",
|
"Send a reply (unencrypted)…": "发送回复(未加密)…",
|
||||||
"Send an encrypted message…": "发送加密消息…",
|
"Send an encrypted message…": "发送加密消息…",
|
||||||
|
@ -790,7 +789,7 @@
|
||||||
"Add a User": "添加一个用户",
|
"Add a User": "添加一个用户",
|
||||||
"Unable to accept invite": "无法接受邀请",
|
"Unable to accept invite": "无法接受邀请",
|
||||||
"Unable to reject invite": "无法拒绝邀请",
|
"Unable to reject invite": "无法拒绝邀请",
|
||||||
"Leave Community": "离开社区",
|
"Leave Community": "退出社区",
|
||||||
"Community Settings": "社区设置",
|
"Community Settings": "社区设置",
|
||||||
"Community %(groupId)s not found": "找不到社区 %(groupId)s",
|
"Community %(groupId)s not found": "找不到社区 %(groupId)s",
|
||||||
"Your Communities": "你的社区",
|
"Your Communities": "你的社区",
|
||||||
|
@ -802,13 +801,13 @@
|
||||||
"Failed to invite users to %(groupId)s": "邀请用户到 %(groupId)s 失败",
|
"Failed to invite users to %(groupId)s": "邀请用户到 %(groupId)s 失败",
|
||||||
"Failed to invite the following users to %(groupId)s:": "邀请下列用户到 %(groupId)s 失败:",
|
"Failed to invite the following users to %(groupId)s:": "邀请下列用户到 %(groupId)s 失败:",
|
||||||
"Failed to add the following rooms to %(groupId)s:": "添加以下聊天室到 %(groupId)s 失败:",
|
"Failed to add the following rooms to %(groupId)s:": "添加以下聊天室到 %(groupId)s 失败:",
|
||||||
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "你似乎没有将此邮箱地址同在此主服务器上的任何一个 Matrix 账号相关联。",
|
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "你似乎没有将此邮箱地址同在此主服务器上的任何一个 Matrix 账号绑定。",
|
||||||
"Restricted": "受限用户",
|
"Restricted": "受限用户",
|
||||||
"To use it, just wait for autocomplete results to load and tab through them.": "若要使用自动补全,只要等待自动补全结果加载完成,按 Tab 键切换即可。",
|
"To use it, just wait for autocomplete results to load and tab through them.": "若要使用自动补全,只要等待自动补全结果加载完成,按 Tab 键切换即可。",
|
||||||
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s 将他们的昵称修改成了 %(displayName)s 。",
|
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s 将他们的昵称修改成了 %(displayName)s 。",
|
||||||
"Hide avatars in user and room mentions": "隐藏头像",
|
"Hide avatars in user and room mentions": "隐藏头像",
|
||||||
"Disable Community Filter Panel": "停用社区面板",
|
"Disable Community Filter Panel": "停用社区面板",
|
||||||
"Opt out of analytics": "禁用开发数据上传",
|
"Opt out of analytics": "退出统计分析服务",
|
||||||
"Stickerpack": "贴图集",
|
"Stickerpack": "贴图集",
|
||||||
"Sticker Messages": "贴图消息",
|
"Sticker Messages": "贴图消息",
|
||||||
"You don't currently have any stickerpacks enabled": "您目前没有启用任何贴纸包",
|
"You don't currently have any stickerpacks enabled": "您目前没有启用任何贴纸包",
|
||||||
|
@ -849,13 +848,13 @@
|
||||||
"%(serverName)s Matrix ID": "%(serverName)s Matrix ID",
|
"%(serverName)s Matrix ID": "%(serverName)s Matrix ID",
|
||||||
"You are registering with %(SelectedTeamName)s": "你将注册为 %(SelectedTeamName)s",
|
"You are registering with %(SelectedTeamName)s": "你将注册为 %(SelectedTeamName)s",
|
||||||
"Remove from community": "从社区中移除",
|
"Remove from community": "从社区中移除",
|
||||||
"Disinvite this user from community?": "是否要取消邀请此用户加入社区?",
|
"Disinvite this user from community?": "是否不再邀请此用户加入本社区?",
|
||||||
"Remove this user from community?": "是否要从社区中移除此用户?",
|
"Remove this user from community?": "是否要从社区中移除此用户?",
|
||||||
"Failed to withdraw invitation": "撤回邀请失败",
|
"Failed to withdraw invitation": "撤回邀请失败",
|
||||||
"Failed to remove user from community": "移除用户失败",
|
"Failed to remove user from community": "移除用户失败",
|
||||||
"Filter community members": "过滤社区成员",
|
"Filter community members": "过滤社区成员",
|
||||||
"Flair will not appear": "将不会显示 Flair",
|
"Flair will not appear": "将不会显示 Flair",
|
||||||
"Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "你确定要从 %(groupId)s 中删除1吗?",
|
"Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "你确定要从 %(groupId)s 中移除 %(roomName)s 吗?",
|
||||||
"Removing a room from the community will also remove it from the community page.": "从社区中移除房间时,同时也会将其从社区页面中移除。",
|
"Removing a room from the community will also remove it from the community page.": "从社区中移除房间时,同时也会将其从社区页面中移除。",
|
||||||
"Failed to remove room from community": "从社区中移除聊天室失败",
|
"Failed to remove room from community": "从社区中移除聊天室失败",
|
||||||
"Failed to remove '%(roomName)s' from %(groupId)s": "从 %(groupId)s 中移除 “%(roomName)s” 失败",
|
"Failed to remove '%(roomName)s' from %(groupId)s": "从 %(groupId)s 中移除 “%(roomName)s” 失败",
|
||||||
|
@ -868,18 +867,18 @@
|
||||||
"%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s 已加入",
|
"%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s 已加入",
|
||||||
"%(oneUser)sjoined %(count)s times|other": "%(oneUser)s 已加入 %(count)s 次",
|
"%(oneUser)sjoined %(count)s times|other": "%(oneUser)s 已加入 %(count)s 次",
|
||||||
"%(oneUser)sjoined %(count)s times|one": "%(oneUser)s 已加入",
|
"%(oneUser)sjoined %(count)s times|one": "%(oneUser)s 已加入",
|
||||||
"%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s 已离开 %(count)s 次",
|
"%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s 已退出 %(count)s 次",
|
||||||
"%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s 已离开",
|
"%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s 已退出",
|
||||||
"%(oneUser)sleft %(count)s times|other": "%(oneUser)s 已离开 %(count)s 次",
|
"%(oneUser)sleft %(count)s times|other": "%(oneUser)s 已退出 %(count)s 次",
|
||||||
"%(oneUser)sleft %(count)s times|one": "%(oneUser)s 已离开",
|
"%(oneUser)sleft %(count)s times|one": "%(oneUser)s 已退出",
|
||||||
"%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s 已加入&已离开 %(count)s 次",
|
"%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s 已加入&已退出 %(count)s 次",
|
||||||
"%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s 已加入&已离开",
|
"%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s 已加入&已退出",
|
||||||
"%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s 已加入&已离开 %(count)s 次",
|
"%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s 已加入&已退出 %(count)s 次",
|
||||||
"%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s 已加入&已离开",
|
"%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s 已加入&已退出",
|
||||||
"%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s 离开并重新加入了 %(count)s 次",
|
"%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s 退出并重新加入了 %(count)s 次",
|
||||||
"%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s 离开并重新加入了",
|
"%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s 退出并重新加入了",
|
||||||
"%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s 离开并重新加入了 %(count)s 次",
|
"%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s 退出并重新加入了 %(count)s 次",
|
||||||
"%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s 离开并重新加入了",
|
"%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s 退出并重新加入了",
|
||||||
"%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s 拒绝了他们的邀请",
|
"%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s 拒绝了他们的邀请",
|
||||||
"%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s 拒绝了他们的邀请共 %(count)s 次",
|
"%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s 拒绝了他们的邀请共 %(count)s 次",
|
||||||
"%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s 拒绝了他们的邀请共 %(count)s 次",
|
"%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s 拒绝了他们的邀请共 %(count)s 次",
|
||||||
|
@ -893,7 +892,7 @@
|
||||||
"Community IDs cannot not be empty.": "社区 ID 不能为空。",
|
"Community IDs cannot not be empty.": "社区 ID 不能为空。",
|
||||||
"Community IDs may only contain characters a-z, 0-9, or '=_-./'": "社区 ID 只能包含 a-z、0-9 或 “=_-./” 等字符",
|
"Community IDs may only contain characters a-z, 0-9, or '=_-./'": "社区 ID 只能包含 a-z、0-9 或 “=_-./” 等字符",
|
||||||
"Something went wrong whilst creating your community": "创建社区时出现问题",
|
"Something went wrong whilst creating your community": "创建社区时出现问题",
|
||||||
"You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "您目前默认将未验证的设备列入黑名单;在发送消息到这些设备上之前,您必须先验证它们。",
|
"You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "您目前默认将未经验证的设备列入黑名单;在发送消息到这些设备上之前,您必须先验证它们。",
|
||||||
"If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "如果您之前使用过较新版本的 Riot,则您的会话可能与当前版本不兼容。请关闭此窗口并使用最新版本。",
|
"If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "如果您之前使用过较新版本的 Riot,则您的会话可能与当前版本不兼容。请关闭此窗口并使用最新版本。",
|
||||||
"To change the room's avatar, you must be a": "无法修改此聊天室的头像,因为您不是此聊天室的",
|
"To change the room's avatar, you must be a": "无法修改此聊天室的头像,因为您不是此聊天室的",
|
||||||
"To change the room's name, you must be a": "无法修改此聊天室的名称,因为您不是此聊天室的",
|
"To change the room's name, you must be a": "无法修改此聊天室的名称,因为您不是此聊天室的",
|
||||||
|
@ -906,7 +905,7 @@
|
||||||
"URL previews are enabled by default for participants in this room.": "此聊天室默认启用链接预览。",
|
"URL previews are enabled by default for participants in this room.": "此聊天室默认启用链接预览。",
|
||||||
"URL previews are disabled by default for participants in this room.": "此聊天室默认禁用链接预览。",
|
"URL previews are disabled by default for participants in this room.": "此聊天室默认禁用链接预览。",
|
||||||
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s 将聊天室的头像更改为 <img/>",
|
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s 将聊天室的头像更改为 <img/>",
|
||||||
"You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "您也可以自定义身份验证服务器,但这通常会阻止基于电子邮件地址的与用户的交互。",
|
"You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "您也可以自定义身份认证服务器,但这通常会阻止基于邮箱地址的与用户的交互。",
|
||||||
"Please enter the code it contains:": "请输入它包含的代码:",
|
"Please enter the code it contains:": "请输入它包含的代码:",
|
||||||
"Flair will appear if enabled in room settings": "如果在聊天室设置中启用, flair 将会显示",
|
"Flair will appear if enabled in room settings": "如果在聊天室设置中启用, flair 将会显示",
|
||||||
"Matrix ID": "Matrix ID",
|
"Matrix ID": "Matrix ID",
|
||||||
|
@ -956,7 +955,7 @@
|
||||||
"Please note you are logging into the %(hs)s server, not matrix.org.": "请注意,您正在登录的服务器是 %(hs)s,不是 matrix.org。",
|
"Please note you are logging into the %(hs)s server, not matrix.org.": "请注意,您正在登录的服务器是 %(hs)s,不是 matrix.org。",
|
||||||
"This homeserver doesn't offer any login flows which are supported by this client.": "此主服务器不兼容本客户端支持的任何登录方式。",
|
"This homeserver doesn't offer any login flows which are supported by this client.": "此主服务器不兼容本客户端支持的任何登录方式。",
|
||||||
"Sign in to get started": "登录以开始使用",
|
"Sign in to get started": "登录以开始使用",
|
||||||
"Unbans user with given id": "使用 ID 解封特定的用户",
|
"Unbans user with given id": "按照 ID 解封特定的用户",
|
||||||
"Opens the Developer Tools dialog": "打开开发者工具窗口",
|
"Opens the Developer Tools dialog": "打开开发者工具窗口",
|
||||||
"Notify the whole room": "通知聊天室全体成员",
|
"Notify the whole room": "通知聊天室全体成员",
|
||||||
"This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "此操作允许您将加密聊天室中收到的消息的密钥导出为本地文件。您可以将文件导入其他 Matrix 客户端,以便让别的客户端在未收到密钥的情况下解密这些消息。",
|
"This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "此操作允许您将加密聊天室中收到的消息的密钥导出为本地文件。您可以将文件导入其他 Matrix 客户端,以便让别的客户端在未收到密钥的情况下解密这些消息。",
|
||||||
|
@ -966,7 +965,7 @@
|
||||||
"Ignores a user, hiding their messages from you": "忽略用户,隐藏他们的消息",
|
"Ignores a user, hiding their messages from you": "忽略用户,隐藏他们的消息",
|
||||||
"Stops ignoring a user, showing their messages going forward": "解除忽略用户,显示他们的消息",
|
"Stops ignoring a user, showing their messages going forward": "解除忽略用户,显示他们的消息",
|
||||||
"To return to your account in future you need to set a password": "如果你想再次使用账号,您得为它设置一个密码",
|
"To return to your account in future you need to set a password": "如果你想再次使用账号,您得为它设置一个密码",
|
||||||
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "如果你在 GitHub 提交了一个 bug,调试日志可以帮助我们追踪这个问题。 调试日志包含应用程序使用数据,这包括您的用户名、您访问的房间或社区的 ID 或名称以及其他用户的用户名,不包扩聊天记录。",
|
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "如果你在 GitHub 提交了一个 bug,调试日志可以帮助我们追踪这个问题。 调试日志包含应用程序使用数据,这包括您的用户名、您访问的房间或社区的 ID 或名称以及其他用户的用户名,不包扩聊天记录。",
|
||||||
"Debug Logs Submission": "发送调试日志",
|
"Debug Logs Submission": "发送调试日志",
|
||||||
"Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "密码修改成功。在您在其他设备上重新登录之前,其他设备不会收到推送通知",
|
"Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "密码修改成功。在您在其他设备上重新登录之前,其他设备不会收到推送通知",
|
||||||
"Tried to load a specific point in this room's timeline, but was unable to find it.": "尝试加载此房间的时间线的特定时间点,但是无法找到。",
|
"Tried to load a specific point in this room's timeline, but was unable to find it.": "尝试加载此房间的时间线的特定时间点,但是无法找到。",
|
||||||
|
@ -990,7 +989,7 @@
|
||||||
"Friday": "星期五",
|
"Friday": "星期五",
|
||||||
"Update": "更新",
|
"Update": "更新",
|
||||||
"What's New": "新鲜事",
|
"What's New": "新鲜事",
|
||||||
"Add an email address above to configure email notifications": "请在上方输入电子邮件地址以接收邮件通知",
|
"Add an email address above to configure email notifications": "请在上方输入邮箱地址以接收邮件通知",
|
||||||
"Expand panel": "展开面板",
|
"Expand panel": "展开面板",
|
||||||
"On": "打开",
|
"On": "打开",
|
||||||
"%(count)s Members|other": "%(count)s 位成员",
|
"%(count)s Members|other": "%(count)s 位成员",
|
||||||
|
@ -1044,7 +1043,7 @@
|
||||||
"Tuesday": "星期二",
|
"Tuesday": "星期二",
|
||||||
"Enter keywords separated by a comma:": "输入以逗号间隔的关键字:",
|
"Enter keywords separated by a comma:": "输入以逗号间隔的关键字:",
|
||||||
"Forward Message": "转发消息",
|
"Forward Message": "转发消息",
|
||||||
"You have successfully set a password and an email address!": "您已经成功设置了密码和电子邮件地址!",
|
"You have successfully set a password and an email address!": "您已经成功设置了密码和邮箱地址!",
|
||||||
"Remove %(name)s from the directory?": "从目录中移除 %(name)s 吗?",
|
"Remove %(name)s from the directory?": "从目录中移除 %(name)s 吗?",
|
||||||
"Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot 使用了许多先进的浏览器功能,有些在你目前所用的浏览器上无法使用或仅为实验性的功能。",
|
"Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot 使用了许多先进的浏览器功能,有些在你目前所用的浏览器上无法使用或仅为实验性的功能。",
|
||||||
"Developer Tools": "开发者工具",
|
"Developer Tools": "开发者工具",
|
||||||
|
@ -1129,5 +1128,9 @@
|
||||||
"Collapse panel": "折叠面板",
|
"Collapse panel": "折叠面板",
|
||||||
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "您目前的浏览器,应用程序的外观和感觉完全不正确,有些或全部功能可能无法使用。如果您仍想继续尝试,可以继续,但请自行负担其后果!",
|
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "您目前的浏览器,应用程序的外观和感觉完全不正确,有些或全部功能可能无法使用。如果您仍想继续尝试,可以继续,但请自行负担其后果!",
|
||||||
"Checking for an update...": "正在检查更新…",
|
"Checking for an update...": "正在检查更新…",
|
||||||
"There are advanced notifications which are not shown here": "更多的通知并没有在此显示出来"
|
"There are advanced notifications which are not shown here": "更多的通知并没有在此显示出来",
|
||||||
|
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "这里没有其他人了!你是想 <inviteText>邀请用户</inviteText> 还是 <nowarnText>不再提示</nowarnText>?",
|
||||||
|
"You need to be able to invite users to do that.": "你需要有邀请用户的权限才能进行此操作。",
|
||||||
|
"Missing roomId.": "找不到此聊天室 ID 所对应的聊天室。",
|
||||||
|
"Tag Panel": "标签面板"
|
||||||
}
|
}
|
||||||
|
|
|
@ -956,7 +956,6 @@
|
||||||
"Notify the whole room": "通知整個聊天室",
|
"Notify the whole room": "通知整個聊天室",
|
||||||
"Room Notification": "聊天室通知",
|
"Room Notification": "聊天室通知",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "協助讓 Riot.im 變得更好的傳送給我們的資訊包含了:",
|
"The information being sent to us to help make Riot.im better includes:": "協助讓 Riot.im 變得更好的傳送給我們的資訊包含了:",
|
||||||
"We also record each page you use in the app (currently <CurrentPageHash>), your User Agent (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).": "我們也紀錄了每個您在應用程式中使用的頁面(目前 <CurrentPageHash>),您的使用者代理(<CurrentUserAgent>)與您的裝置解析度(<CurrentDeviceResolution>)。",
|
|
||||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "這個頁面包含了可識別的資訊,如聊天室、使用者或群組 ID,這些資料會在傳到伺服器前被刪除。",
|
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "這個頁面包含了可識別的資訊,如聊天室、使用者或群組 ID,這些資料會在傳到伺服器前被刪除。",
|
||||||
"The platform you're on": "您使用的平臺是",
|
"The platform you're on": "您使用的平臺是",
|
||||||
"The version of Riot.im": "Riot.im 的版本",
|
"The version of Riot.im": "Riot.im 的版本",
|
||||||
|
@ -987,7 +986,7 @@
|
||||||
"%(user)s is a %(userRole)s": "%(user)s 是 %(userRole)s",
|
"%(user)s is a %(userRole)s": "%(user)s 是 %(userRole)s",
|
||||||
"Code": "代碼",
|
"Code": "代碼",
|
||||||
"Debug Logs Submission": "除錯訊息傳送",
|
"Debug Logs Submission": "除錯訊息傳送",
|
||||||
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contian messages.": "如果您透過 GitHub 來回報錯誤,除錯訊息可以用來追蹤問題。除錯訊息包含應用程式的使用資料,包括您的使用者名稱、您所造訪的房間/群組的 ID 或別名、其他使用者的使用者名稱等,其中不包含訊息本身。",
|
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "如果您透過 GitHub 來回報錯誤,除錯訊息可以用來追蹤問題。除錯訊息包含應用程式的使用資料,包括您的使用者名稱、您所造訪的房間/群組的 ID 或別名、其他使用者的使用者名稱等,其中不包含訊息本身。",
|
||||||
"Submit debug logs": "傳送除錯訊息",
|
"Submit debug logs": "傳送除錯訊息",
|
||||||
"Opens the Developer Tools dialog": "開啟開發者工具對話視窗",
|
"Opens the Developer Tools dialog": "開啟開發者工具對話視窗",
|
||||||
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "被 %(displayName)s (%(userName)s) 於 %(dateTime)s 看過",
|
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "被 %(displayName)s (%(userName)s) 於 %(dateTime)s 看過",
|
||||||
|
@ -1157,5 +1156,7 @@
|
||||||
"Collapse panel": "摺疊面板",
|
"Collapse panel": "摺疊面板",
|
||||||
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "您目前的瀏覽器,其應用程式的外觀和感覺可能完全不正確,有些或全部功能可以無法使用。如果您仍想要繼續嘗試,可以繼續,但必須自行承擔後果!",
|
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "您目前的瀏覽器,其應用程式的外觀和感覺可能完全不正確,有些或全部功能可以無法使用。如果您仍想要繼續嘗試,可以繼續,但必須自行承擔後果!",
|
||||||
"Checking for an update...": "正在檢查更新...",
|
"Checking for an update...": "正在檢查更新...",
|
||||||
"There are advanced notifications which are not shown here": "有些進階的通知並未在此顯示"
|
"There are advanced notifications which are not shown here": "有些進階的通知並未在此顯示",
|
||||||
|
"Missing roomId.": "缺少 roomid。",
|
||||||
|
"Picture": "圖片"
|
||||||
}
|
}
|
||||||
|
|
|
@ -395,9 +395,6 @@ function selectQuery(store, keyRange, resultMapper) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let store = null;
|
|
||||||
let logger = null;
|
|
||||||
let initPromise = null;
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -406,11 +403,11 @@ module.exports = {
|
||||||
* @return {Promise} Resolves when set up.
|
* @return {Promise} Resolves when set up.
|
||||||
*/
|
*/
|
||||||
init: function() {
|
init: function() {
|
||||||
if (initPromise) {
|
if (global.mx_rage_initPromise) {
|
||||||
return initPromise;
|
return global.mx_rage_initPromise;
|
||||||
}
|
}
|
||||||
logger = new ConsoleLogger();
|
global.mx_rage_logger = new ConsoleLogger();
|
||||||
logger.monkeyPatch(window.console);
|
global.mx_rage_logger.monkeyPatch(window.console);
|
||||||
|
|
||||||
// just *accessing* indexedDB throws an exception in firefox with
|
// just *accessing* indexedDB throws an exception in firefox with
|
||||||
// indexeddb disabled.
|
// indexeddb disabled.
|
||||||
|
@ -420,19 +417,19 @@ module.exports = {
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
|
|
||||||
if (indexedDB) {
|
if (indexedDB) {
|
||||||
store = new IndexedDBLogStore(indexedDB, logger);
|
global.mx_rage_store = new IndexedDBLogStore(indexedDB, global.mx_rage_logger);
|
||||||
initPromise = store.connect();
|
global.mx_rage_initPromise = global.mx_rage_store.connect();
|
||||||
return initPromise;
|
return global.mx_rage_initPromise;
|
||||||
}
|
}
|
||||||
initPromise = Promise.resolve();
|
global.mx_rage_initPromise = Promise.resolve();
|
||||||
return initPromise;
|
return global.mx_rage_initPromise;
|
||||||
},
|
},
|
||||||
|
|
||||||
flush: function() {
|
flush: function() {
|
||||||
if (!store) {
|
if (!global.mx_rage_store) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
store.flush();
|
global.mx_rage_store.flush();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -440,10 +437,10 @@ module.exports = {
|
||||||
* @return Promise Resolves if cleaned logs.
|
* @return Promise Resolves if cleaned logs.
|
||||||
*/
|
*/
|
||||||
cleanup: async function() {
|
cleanup: async function() {
|
||||||
if (!store) {
|
if (!global.mx_rage_store) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await store.consume();
|
await global.mx_rage_store.consume();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -452,21 +449,21 @@ module.exports = {
|
||||||
* @return {Array<{lines: string, id, string}>} list of log data
|
* @return {Array<{lines: string, id, string}>} list of log data
|
||||||
*/
|
*/
|
||||||
getLogsForReport: async function() {
|
getLogsForReport: async function() {
|
||||||
if (!logger) {
|
if (!global.mx_rage_logger) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"No console logger, did you forget to call init()?"
|
"No console logger, did you forget to call init()?"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// If in incognito mode, store is null, but we still want bug report
|
// If in incognito mode, store is null, but we still want bug report
|
||||||
// sending to work going off the in-memory console logs.
|
// sending to work going off the in-memory console logs.
|
||||||
if (store) {
|
if (global.mx_rage_store) {
|
||||||
// flush most recent logs
|
// flush most recent logs
|
||||||
await store.flush();
|
await global.mx_rage_store.flush();
|
||||||
return await store.consume();
|
return await global.mx_rage_store.consume();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return [{
|
return [{
|
||||||
lines: logger.flush(true),
|
lines: global.mx_rage_logger.flush(true),
|
||||||
id: "-",
|
id: "-",
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,6 +150,11 @@ export const SETTINGS = {
|
||||||
displayName: _td('Autoplay GIFs and videos'),
|
displayName: _td('Autoplay GIFs and videos'),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"alwaysShowEncryptionIcons": {
|
||||||
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
|
displayName: _td('Always show encryption icons'),
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
"enableSyntaxHighlightLanguageDetection": {
|
"enableSyntaxHighlightLanguageDetection": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
displayName: _td('Enable automatic language detection for syntax highlighting'),
|
displayName: _td('Enable automatic language detection for syntax highlighting'),
|
||||||
|
|
|
@ -48,106 +48,110 @@ function checkBacklog() {
|
||||||
|
|
||||||
// Limit the maximum number of ongoing promises returned by fn to LIMIT and
|
// Limit the maximum number of ongoing promises returned by fn to LIMIT and
|
||||||
// use a FIFO queue to handle the backlog.
|
// use a FIFO queue to handle the backlog.
|
||||||
function limitConcurrency(fn) {
|
async function limitConcurrency(fn) {
|
||||||
return new Promise((resolve, reject) => {
|
if (ongoingRequestCount >= LIMIT) {
|
||||||
const item = () => {
|
// Enqueue this request for later execution
|
||||||
ongoingRequestCount++;
|
await new Promise((resolve, reject) => {
|
||||||
resolve();
|
backlogQueue.push(resolve);
|
||||||
};
|
});
|
||||||
if (ongoingRequestCount >= LIMIT) {
|
}
|
||||||
// Enqueue this request for later execution
|
|
||||||
backlogQueue.push(item);
|
ongoingRequestCount++;
|
||||||
} else {
|
try {
|
||||||
item();
|
return await fn();
|
||||||
}
|
} catch (err) {
|
||||||
})
|
// We explicitly do not handle the error here, but let it propogate.
|
||||||
.then(fn)
|
throw err;
|
||||||
.then((result) => {
|
} finally {
|
||||||
ongoingRequestCount--;
|
ongoingRequestCount--;
|
||||||
checkBacklog();
|
checkBacklog();
|
||||||
return result;
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the group summary for a room and provides an API to change it and
|
* Global store for tracking group summary, members, invited members and rooms.
|
||||||
* other useful group APIs that may have an effect on the group summary.
|
|
||||||
*/
|
*/
|
||||||
export default class GroupStore extends EventEmitter {
|
class GroupStore extends EventEmitter {
|
||||||
|
STATE_KEY = {
|
||||||
static STATE_KEY = {
|
|
||||||
GroupMembers: 'GroupMembers',
|
GroupMembers: 'GroupMembers',
|
||||||
GroupInvitedMembers: 'GroupInvitedMembers',
|
GroupInvitedMembers: 'GroupInvitedMembers',
|
||||||
Summary: 'Summary',
|
Summary: 'Summary',
|
||||||
GroupRooms: 'GroupRooms',
|
GroupRooms: 'GroupRooms',
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(groupId) {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
if (!groupId) {
|
|
||||||
throw new Error('GroupStore needs a valid groupId to be created');
|
|
||||||
}
|
|
||||||
this.groupId = groupId;
|
|
||||||
this._state = {};
|
this._state = {};
|
||||||
this._state[GroupStore.STATE_KEY.Summary] = {};
|
this._state[this.STATE_KEY.Summary] = {};
|
||||||
this._state[GroupStore.STATE_KEY.GroupRooms] = [];
|
this._state[this.STATE_KEY.GroupRooms] = {};
|
||||||
this._state[GroupStore.STATE_KEY.GroupMembers] = [];
|
this._state[this.STATE_KEY.GroupMembers] = {};
|
||||||
this._state[GroupStore.STATE_KEY.GroupInvitedMembers] = [];
|
this._state[this.STATE_KEY.GroupInvitedMembers] = {};
|
||||||
this._ready = {};
|
|
||||||
|
this._ready = {};
|
||||||
|
this._ready[this.STATE_KEY.Summary] = {};
|
||||||
|
this._ready[this.STATE_KEY.GroupRooms] = {};
|
||||||
|
this._ready[this.STATE_KEY.GroupMembers] = {};
|
||||||
|
this._ready[this.STATE_KEY.GroupInvitedMembers] = {};
|
||||||
|
|
||||||
|
this._fetchResourcePromise = {
|
||||||
|
[this.STATE_KEY.Summary]: {},
|
||||||
|
[this.STATE_KEY.GroupRooms]: {},
|
||||||
|
[this.STATE_KEY.GroupMembers]: {},
|
||||||
|
[this.STATE_KEY.GroupInvitedMembers]: {},
|
||||||
|
};
|
||||||
|
|
||||||
this._fetchResourcePromise = {};
|
|
||||||
this._resourceFetcher = {
|
this._resourceFetcher = {
|
||||||
[GroupStore.STATE_KEY.Summary]: () => {
|
[this.STATE_KEY.Summary]: (groupId) => {
|
||||||
return limitConcurrency(
|
return limitConcurrency(
|
||||||
() => MatrixClientPeg.get().getGroupSummary(this.groupId),
|
() => MatrixClientPeg.get().getGroupSummary(groupId),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[GroupStore.STATE_KEY.GroupRooms]: () => {
|
[this.STATE_KEY.GroupRooms]: (groupId) => {
|
||||||
return limitConcurrency(
|
return limitConcurrency(
|
||||||
() => MatrixClientPeg.get().getGroupRooms(this.groupId).then(parseRoomsResponse),
|
() => MatrixClientPeg.get().getGroupRooms(groupId).then(parseRoomsResponse),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[GroupStore.STATE_KEY.GroupMembers]: () => {
|
[this.STATE_KEY.GroupMembers]: (groupId) => {
|
||||||
return limitConcurrency(
|
return limitConcurrency(
|
||||||
() => MatrixClientPeg.get().getGroupUsers(this.groupId).then(parseMembersResponse),
|
() => MatrixClientPeg.get().getGroupUsers(groupId).then(parseMembersResponse),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[GroupStore.STATE_KEY.GroupInvitedMembers]: () => {
|
[this.STATE_KEY.GroupInvitedMembers]: (groupId) => {
|
||||||
return limitConcurrency(
|
return limitConcurrency(
|
||||||
() => MatrixClientPeg.get().getGroupInvitedUsers(this.groupId).then(parseMembersResponse),
|
() => MatrixClientPeg.get().getGroupInvitedUsers(groupId).then(parseMembersResponse),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.on('error', (err) => {
|
this.on('error', (err, groupId) => {
|
||||||
console.error(`GroupStore for ${this.groupId} encountered error`, err);
|
console.error(`GroupStore encountered error whilst fetching data for ${groupId}`, err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_fetchResource(stateKey) {
|
_fetchResource(stateKey, groupId) {
|
||||||
// Ongoing request, ignore
|
// Ongoing request, ignore
|
||||||
if (this._fetchResourcePromise[stateKey]) return;
|
if (this._fetchResourcePromise[stateKey][groupId]) return;
|
||||||
|
|
||||||
const clientPromise = this._resourceFetcher[stateKey]();
|
const clientPromise = this._resourceFetcher[stateKey](groupId);
|
||||||
|
|
||||||
// Indicate ongoing request
|
// Indicate ongoing request
|
||||||
this._fetchResourcePromise[stateKey] = clientPromise;
|
this._fetchResourcePromise[stateKey][groupId] = clientPromise;
|
||||||
|
|
||||||
clientPromise.then((result) => {
|
clientPromise.then((result) => {
|
||||||
this._state[stateKey] = result;
|
this._state[stateKey][groupId] = result;
|
||||||
this._ready[stateKey] = true;
|
this._ready[stateKey][groupId] = true;
|
||||||
this._notifyListeners();
|
this._notifyListeners();
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
// Invited users not visible to non-members
|
// Invited users not visible to non-members
|
||||||
if (stateKey === GroupStore.STATE_KEY.GroupInvitedMembers && err.httpStatus === 403) {
|
if (stateKey === this.STATE_KEY.GroupInvitedMembers && err.httpStatus === 403) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error("Failed to get resource " + stateKey + ":" + err);
|
console.error(`Failed to get resource ${stateKey} for ${groupId}`, err);
|
||||||
this.emit('error', err);
|
this.emit('error', err, groupId);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
// Indicate finished request, allow for future fetches
|
// Indicate finished request, allow for future fetches
|
||||||
delete this._fetchResourcePromise[stateKey];
|
delete this._fetchResourcePromise[stateKey][groupId];
|
||||||
});
|
});
|
||||||
|
|
||||||
return clientPromise;
|
return clientPromise;
|
||||||
|
@ -162,25 +166,29 @@ export default class GroupStore extends EventEmitter {
|
||||||
* immediately triggers an update to send the current state of the
|
* immediately triggers an update to send the current state of the
|
||||||
* store (which could be the initial state).
|
* store (which could be the initial state).
|
||||||
*
|
*
|
||||||
* This also causes a fetch of all group data, which might cause
|
* If a group ID is specified, this also causes a fetch of all data
|
||||||
* 4 separate HTTP requests, but only said requests aren't already
|
* of the specified group, which might cause 4 separate HTTP
|
||||||
* ongoing.
|
* requests, but only if said requests aren't already ongoing.
|
||||||
*
|
*
|
||||||
|
* @param {string?} groupId the ID of the group to fetch data for.
|
||||||
|
* Optional.
|
||||||
* @param {function} fn the function to call when the store updates.
|
* @param {function} fn the function to call when the store updates.
|
||||||
* @return {Object} tok a registration "token" with a single
|
* @return {Object} tok a registration "token" with a single
|
||||||
* property `unregister`, a function that can
|
* property `unregister`, a function that can
|
||||||
* be called to unregister the listener such
|
* be called to unregister the listener such
|
||||||
* that it won't be called any more.
|
* that it won't be called any more.
|
||||||
*/
|
*/
|
||||||
registerListener(fn) {
|
registerListener(groupId, fn) {
|
||||||
this.on('update', fn);
|
this.on('update', fn);
|
||||||
// Call to set initial state (before fetching starts)
|
// Call to set initial state (before fetching starts)
|
||||||
this.emit('update');
|
this.emit('update');
|
||||||
|
|
||||||
this._fetchResource(GroupStore.STATE_KEY.Summary);
|
if (groupId) {
|
||||||
this._fetchResource(GroupStore.STATE_KEY.GroupRooms);
|
this._fetchResource(this.STATE_KEY.Summary, groupId);
|
||||||
this._fetchResource(GroupStore.STATE_KEY.GroupMembers);
|
this._fetchResource(this.STATE_KEY.GroupRooms, groupId);
|
||||||
this._fetchResource(GroupStore.STATE_KEY.GroupInvitedMembers);
|
this._fetchResource(this.STATE_KEY.GroupMembers, groupId);
|
||||||
|
this._fetchResource(this.STATE_KEY.GroupInvitedMembers, groupId);
|
||||||
|
}
|
||||||
|
|
||||||
// Similar to the Store of flux/utils, we return a "token" that
|
// Similar to the Store of flux/utils, we return a "token" that
|
||||||
// can be used to unregister the listener.
|
// can be used to unregister the listener.
|
||||||
|
@ -195,123 +203,137 @@ export default class GroupStore extends EventEmitter {
|
||||||
this.removeListener('update', fn);
|
this.removeListener('update', fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
isStateReady(id) {
|
isStateReady(groupId, id) {
|
||||||
return this._ready[id];
|
return this._ready[id][groupId];
|
||||||
}
|
}
|
||||||
|
|
||||||
getSummary() {
|
getSummary(groupId) {
|
||||||
return this._state[GroupStore.STATE_KEY.Summary];
|
return this._state[this.STATE_KEY.Summary][groupId] || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
getGroupRooms() {
|
getGroupRooms(groupId) {
|
||||||
return this._state[GroupStore.STATE_KEY.GroupRooms];
|
return this._state[this.STATE_KEY.GroupRooms][groupId] || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getGroupMembers() {
|
getGroupMembers(groupId) {
|
||||||
return this._state[GroupStore.STATE_KEY.GroupMembers];
|
return this._state[this.STATE_KEY.GroupMembers][groupId] || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getGroupInvitedMembers() {
|
getGroupInvitedMembers(groupId) {
|
||||||
return this._state[GroupStore.STATE_KEY.GroupInvitedMembers];
|
return this._state[this.STATE_KEY.GroupInvitedMembers][groupId] || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getGroupPublicity() {
|
getGroupPublicity(groupId) {
|
||||||
return this._state[GroupStore.STATE_KEY.Summary].user ?
|
return (this._state[this.STATE_KEY.Summary][groupId] || {}).user ?
|
||||||
this._state[GroupStore.STATE_KEY.Summary].user.is_publicised : null;
|
(this._state[this.STATE_KEY.Summary][groupId] || {}).user.is_publicised : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
isUserPrivileged() {
|
isUserPrivileged(groupId) {
|
||||||
return this._state[GroupStore.STATE_KEY.Summary].user ?
|
return (this._state[this.STATE_KEY.Summary][groupId] || {}).user ?
|
||||||
this._state[GroupStore.STATE_KEY.Summary].user.is_privileged : null;
|
(this._state[this.STATE_KEY.Summary][groupId] || {}).user.is_privileged : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
addRoomToGroup(roomId, isPublic) {
|
refreshGroupRooms(groupId) {
|
||||||
|
return this._fetchResource(this.STATE_KEY.GroupRooms, groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshGroupMembers(groupId) {
|
||||||
|
return this._fetchResource(this.STATE_KEY.GroupMembers, groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
addRoomToGroup(groupId, roomId, isPublic) {
|
||||||
return MatrixClientPeg.get()
|
return MatrixClientPeg.get()
|
||||||
.addRoomToGroup(this.groupId, roomId, isPublic)
|
.addRoomToGroup(groupId, roomId, isPublic)
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms));
|
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateGroupRoomVisibility(roomId, isPublic) {
|
updateGroupRoomVisibility(groupId, roomId, isPublic) {
|
||||||
return MatrixClientPeg.get()
|
return MatrixClientPeg.get()
|
||||||
.updateGroupRoomVisibility(this.groupId, roomId, isPublic)
|
.updateGroupRoomVisibility(groupId, roomId, isPublic)
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms));
|
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId));
|
||||||
}
|
}
|
||||||
|
|
||||||
removeRoomFromGroup(roomId) {
|
removeRoomFromGroup(groupId, roomId) {
|
||||||
return MatrixClientPeg.get()
|
return MatrixClientPeg.get()
|
||||||
.removeRoomFromGroup(this.groupId, roomId)
|
.removeRoomFromGroup(groupId, roomId)
|
||||||
// Room might be in the summary, refresh just in case
|
// Room might be in the summary, refresh just in case
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary))
|
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms));
|
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId));
|
||||||
}
|
}
|
||||||
|
|
||||||
inviteUserToGroup(userId) {
|
inviteUserToGroup(groupId, userId) {
|
||||||
return MatrixClientPeg.get().inviteUserToGroup(this.groupId, userId)
|
return MatrixClientPeg.get().inviteUserToGroup(groupId, userId)
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupInvitedMembers));
|
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupInvitedMembers, groupId));
|
||||||
}
|
}
|
||||||
|
|
||||||
acceptGroupInvite() {
|
acceptGroupInvite(groupId) {
|
||||||
return MatrixClientPeg.get().acceptGroupInvite(this.groupId)
|
return MatrixClientPeg.get().acceptGroupInvite(groupId)
|
||||||
// The user should now be able to access (personal) group settings
|
// The user should now be able to access (personal) group settings
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary))
|
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
|
||||||
// The user might be able to see more rooms now
|
// The user might be able to see more rooms now
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms))
|
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId))
|
||||||
// The user should now appear as a member
|
// The user should now appear as a member
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupMembers))
|
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupMembers, groupId))
|
||||||
// The user should now not appear as an invited member
|
// The user should now not appear as an invited member
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupInvitedMembers));
|
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupInvitedMembers, groupId));
|
||||||
}
|
}
|
||||||
|
|
||||||
joinGroup() {
|
joinGroup(groupId) {
|
||||||
return MatrixClientPeg.get().joinGroup(this.groupId)
|
return MatrixClientPeg.get().joinGroup(groupId)
|
||||||
// The user should now be able to access (personal) group settings
|
// The user should now be able to access (personal) group settings
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary))
|
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
|
||||||
// The user might be able to see more rooms now
|
// The user might be able to see more rooms now
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms))
|
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId))
|
||||||
// The user should now appear as a member
|
// The user should now appear as a member
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupMembers))
|
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupMembers, groupId))
|
||||||
// The user should now not appear as an invited member
|
// The user should now not appear as an invited member
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupInvitedMembers));
|
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupInvitedMembers, groupId));
|
||||||
}
|
}
|
||||||
|
|
||||||
leaveGroup() {
|
leaveGroup(groupId) {
|
||||||
return MatrixClientPeg.get().leaveGroup(this.groupId)
|
return MatrixClientPeg.get().leaveGroup(groupId)
|
||||||
// The user should now not be able to access group settings
|
// The user should now not be able to access group settings
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary))
|
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
|
||||||
// The user might only be able to see a subset of rooms now
|
// The user might only be able to see a subset of rooms now
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms))
|
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId))
|
||||||
// The user should now not appear as a member
|
// The user should now not appear as a member
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupMembers));
|
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupMembers, groupId));
|
||||||
}
|
}
|
||||||
|
|
||||||
addRoomToGroupSummary(roomId, categoryId) {
|
addRoomToGroupSummary(groupId, roomId, categoryId) {
|
||||||
return MatrixClientPeg.get()
|
return MatrixClientPeg.get()
|
||||||
.addRoomToGroupSummary(this.groupId, roomId, categoryId)
|
.addRoomToGroupSummary(groupId, roomId, categoryId)
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
|
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
|
||||||
}
|
}
|
||||||
|
|
||||||
addUserToGroupSummary(userId, roleId) {
|
addUserToGroupSummary(groupId, userId, roleId) {
|
||||||
return MatrixClientPeg.get()
|
return MatrixClientPeg.get()
|
||||||
.addUserToGroupSummary(this.groupId, userId, roleId)
|
.addUserToGroupSummary(groupId, userId, roleId)
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
|
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
|
||||||
}
|
}
|
||||||
|
|
||||||
removeRoomFromGroupSummary(roomId) {
|
removeRoomFromGroupSummary(groupId, roomId) {
|
||||||
return MatrixClientPeg.get()
|
return MatrixClientPeg.get()
|
||||||
.removeRoomFromGroupSummary(this.groupId, roomId)
|
.removeRoomFromGroupSummary(groupId, roomId)
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
|
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
|
||||||
}
|
}
|
||||||
|
|
||||||
removeUserFromGroupSummary(userId) {
|
removeUserFromGroupSummary(groupId, userId) {
|
||||||
return MatrixClientPeg.get()
|
return MatrixClientPeg.get()
|
||||||
.removeUserFromGroupSummary(this.groupId, userId)
|
.removeUserFromGroupSummary(groupId, userId)
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
|
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
|
||||||
}
|
}
|
||||||
|
|
||||||
setGroupPublicity(isPublished) {
|
setGroupPublicity(groupId, isPublished) {
|
||||||
return MatrixClientPeg.get()
|
return MatrixClientPeg.get()
|
||||||
.setGroupPublicity(this.groupId, isPublished)
|
.setGroupPublicity(groupId, isPublished)
|
||||||
.then(() => { FlairStore.invalidatePublicisedGroups(MatrixClientPeg.get().credentials.userId); })
|
.then(() => { FlairStore.invalidatePublicisedGroups(MatrixClientPeg.get().credentials.userId); })
|
||||||
.then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
|
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let singletonGroupStore = null;
|
||||||
|
if (!singletonGroupStore) {
|
||||||
|
singletonGroupStore = new GroupStore();
|
||||||
|
}
|
||||||
|
module.exports = singletonGroupStore;
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2017 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import GroupStore from './GroupStore';
|
|
||||||
|
|
||||||
class GroupStoreCache {
|
|
||||||
constructor() {
|
|
||||||
this.groupStore = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getGroupStore(groupId) {
|
|
||||||
if (!this.groupStore || this.groupStore.groupId !== groupId) {
|
|
||||||
// This effectively throws away the reference to any previous GroupStore,
|
|
||||||
// allowing it to be GCd once the components referencing it have stopped
|
|
||||||
// referencing it.
|
|
||||||
this.groupStore = new GroupStore(groupId);
|
|
||||||
}
|
|
||||||
return this.groupStore;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (global.singletonGroupStoreCache === undefined) {
|
|
||||||
global.singletonGroupStoreCache = new GroupStoreCache();
|
|
||||||
}
|
|
||||||
export default global.singletonGroupStoreCache;
|
|
|
@ -111,10 +111,11 @@ class RoomViewStore extends Store {
|
||||||
forwardingEvent: payload.event,
|
forwardingEvent: payload.event,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'quote_event':
|
case 'reply_to_event':
|
||||||
this._setState({
|
this._setState({
|
||||||
quotingEvent: payload.event,
|
replyingToEvent: payload.event,
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,8 +133,8 @@ class RoomViewStore extends Store {
|
||||||
shouldPeek: payload.should_peek === undefined ? true : payload.should_peek,
|
shouldPeek: payload.should_peek === undefined ? true : payload.should_peek,
|
||||||
// have we sent a join request for this room and are waiting for a response?
|
// have we sent a join request for this room and are waiting for a response?
|
||||||
joining: payload.joining || false,
|
joining: payload.joining || false,
|
||||||
// Reset quotingEvent because we don't want cross-room because bad UX
|
// Reset replyingToEvent because we don't want cross-room because bad UX
|
||||||
quotingEvent: null,
|
replyingToEvent: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this._state.forwardingEvent) {
|
if (this._state.forwardingEvent) {
|
||||||
|
@ -297,7 +298,7 @@ class RoomViewStore extends Store {
|
||||||
|
|
||||||
// The mxEvent if one is currently being replied to/quoted
|
// The mxEvent if one is currently being replied to/quoted
|
||||||
getQuotingEvent() {
|
getQuotingEvent() {
|
||||||
return this._state.quotingEvent;
|
return this._state.replyingToEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldPeek() {
|
shouldPeek() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -22,25 +23,62 @@ import 'isomorphic-fetch';
|
||||||
import MatrixClientPeg from '../MatrixClientPeg';
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
|
// WARNING: We have to be very careful about what mime-types we allow into blobs,
|
||||||
|
// as for performance reasons these are now rendered via URL.createObjectURL()
|
||||||
|
// rather than by converting into data: URIs.
|
||||||
|
//
|
||||||
|
// This means that the content is rendered using the origin of the script which
|
||||||
|
// called createObjectURL(), and so if the content contains any scripting then it
|
||||||
|
// will pose a XSS vulnerability when the browser renders it. This is particularly
|
||||||
|
// bad if the user right-clicks the URI and pastes it into a new window or tab,
|
||||||
|
// as the blob will then execute with access to Riot's full JS environment(!)
|
||||||
|
//
|
||||||
|
// See https://github.com/matrix-org/matrix-react-sdk/pull/1820#issuecomment-385210647
|
||||||
|
// for details.
|
||||||
|
//
|
||||||
|
// We mitigate this by only allowing mime-types into blobs which we know don't
|
||||||
|
// contain any scripting, and instantiate all others as application/octet-stream
|
||||||
|
// regardless of what mime-type the event claimed. Even if the payload itself
|
||||||
|
// is some malicious HTML, the fact we instantiate it with a media mimetype or
|
||||||
|
// application/octet-stream means the browser doesn't try to render it as such.
|
||||||
|
//
|
||||||
|
// One interesting edge case is image/svg+xml, which empirically *is* rendered
|
||||||
|
// correctly if the blob is set to the src attribute of an img tag (for thumbnails)
|
||||||
|
// *even if the mimetype is application/octet-stream*. However, empirically JS
|
||||||
|
// in the SVG isn't executed in this scenario, so we seem to be okay.
|
||||||
|
//
|
||||||
|
// Tested on Chrome 65 and Firefox 60
|
||||||
|
//
|
||||||
|
// The list below is taken mainly from
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
|
||||||
|
// N.B. Matrix doesn't currently specify which mimetypes are valid in given
|
||||||
|
// events, so we pick the ones which HTML5 browsers should be able to display
|
||||||
|
//
|
||||||
|
// For the record, mime-types which must NEVER enter this list below include:
|
||||||
|
// text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar.
|
||||||
|
|
||||||
/**
|
const ALLOWED_BLOB_MIMETYPES = {
|
||||||
* Read blob as a data:// URI.
|
'image/jpeg': true,
|
||||||
* @return {Promise} A promise that resolves with the data:// URI.
|
'image/gif': true,
|
||||||
*/
|
'image/png': true,
|
||||||
export function readBlobAsDataUri(file) {
|
|
||||||
const deferred = Promise.defer();
|
'video/mp4': true,
|
||||||
const reader = new FileReader();
|
'video/webm': true,
|
||||||
reader.onload = function(e) {
|
'video/ogg': true,
|
||||||
deferred.resolve(e.target.result);
|
|
||||||
};
|
'audio/mp4': true,
|
||||||
reader.onerror = function(e) {
|
'audio/webm': true,
|
||||||
deferred.reject(e);
|
'audio/aac': true,
|
||||||
};
|
'audio/mpeg': true,
|
||||||
reader.readAsDataURL(file);
|
'audio/ogg': true,
|
||||||
return deferred.promise;
|
'audio/wave': true,
|
||||||
|
'audio/wav': true,
|
||||||
|
'audio/x-wav': true,
|
||||||
|
'audio/x-pn-wav': true,
|
||||||
|
'audio/flac': true,
|
||||||
|
'audio/x-flac': true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypt a file attached to a matrix event.
|
* Decrypt a file attached to a matrix event.
|
||||||
* @param file {Object} The json taken from the matrix event.
|
* @param file {Object} The json taken from the matrix event.
|
||||||
|
@ -61,7 +99,17 @@ export function decryptFile(file) {
|
||||||
return encrypt.decryptAttachment(responseData, file);
|
return encrypt.decryptAttachment(responseData, file);
|
||||||
}).then(function(dataArray) {
|
}).then(function(dataArray) {
|
||||||
// Turn the array into a Blob and give it the correct MIME-type.
|
// Turn the array into a Blob and give it the correct MIME-type.
|
||||||
const blob = new Blob([dataArray], {type: file.mimetype});
|
|
||||||
|
// IMPORTANT: we must not allow scriptable mime-types into Blobs otherwise
|
||||||
|
// they introduce XSS attacks if the Blob URI is viewed directly in the
|
||||||
|
// browser (e.g. by copying the URI into a new tab or window.)
|
||||||
|
// See warning at top of file.
|
||||||
|
let mimetype = file.mimetype ? file.mimetype.split(";")[0].trim() : '';
|
||||||
|
if (!ALLOWED_BLOB_MIMETYPES[mimetype]) {
|
||||||
|
mimetype = 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([dataArray], {type: mimetype});
|
||||||
return blob;
|
return blob;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
import MatrixClientPeg from '../MatrixClientPeg';
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
import {getAddressType} from '../UserAddress';
|
import {getAddressType} from '../UserAddress';
|
||||||
import {inviteToRoom} from '../RoomInvite';
|
import {inviteToRoom} from '../RoomInvite';
|
||||||
import GroupStoreCache from '../stores/GroupStoreCache';
|
import GroupStore from '../stores/GroupStore';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -118,9 +118,7 @@ export default class MultiInviter {
|
||||||
|
|
||||||
let doInvite;
|
let doInvite;
|
||||||
if (this.groupId !== null) {
|
if (this.groupId !== null) {
|
||||||
doInvite = GroupStoreCache
|
doInvite = GroupStore.inviteUserToGroup(this.groupId, addr);
|
||||||
.getGroupStore(this.groupId)
|
|
||||||
.inviteUserToGroup(addr);
|
|
||||||
} else {
|
} else {
|
||||||
doInvite = inviteToRoom(this.roomId, addr);
|
doInvite = inviteToRoom(this.roomId, addr);
|
||||||
}
|
}
|
||||||
|
|
358
test/components/structures/GroupView-test.js
Normal file
358
test/components/structures/GroupView-test.js
Normal file
|
@ -0,0 +1,358 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import ReactTestUtils from 'react-dom/test-utils';
|
||||||
|
import expect from 'expect';
|
||||||
|
|
||||||
|
import MockHttpBackend from 'matrix-mock-request';
|
||||||
|
import MatrixClientPeg from '../../../src/MatrixClientPeg';
|
||||||
|
import sdk from 'matrix-react-sdk';
|
||||||
|
import Matrix from 'matrix-js-sdk';
|
||||||
|
|
||||||
|
import * as TestUtils from 'test-utils';
|
||||||
|
const { waitForUpdate } = TestUtils;
|
||||||
|
|
||||||
|
const GroupView = sdk.getComponent('structures.GroupView');
|
||||||
|
const WrappedGroupView = TestUtils.wrapInMatrixClientContext(GroupView);
|
||||||
|
|
||||||
|
const Spinner = sdk.getComponent('elements.Spinner');
|
||||||
|
|
||||||
|
describe('GroupView', function() {
|
||||||
|
let root;
|
||||||
|
let rootElement;
|
||||||
|
let httpBackend;
|
||||||
|
let summaryResponse;
|
||||||
|
let summaryResponseWithComplicatedLongDesc;
|
||||||
|
let summaryResponseWithNoLongDesc;
|
||||||
|
let summaryResponseWithBadImg;
|
||||||
|
let groupId;
|
||||||
|
let groupIdEncoded;
|
||||||
|
|
||||||
|
// Summary response fields
|
||||||
|
const user = {
|
||||||
|
is_privileged: true, // can edit the group
|
||||||
|
is_public: true, // appear as a member to non-members
|
||||||
|
is_publicised: true, // display flair
|
||||||
|
};
|
||||||
|
const usersSection = {
|
||||||
|
roles: {},
|
||||||
|
total_user_count_estimate: 0,
|
||||||
|
users: [],
|
||||||
|
};
|
||||||
|
const roomsSection = {
|
||||||
|
categories: {},
|
||||||
|
rooms: [],
|
||||||
|
total_room_count_estimate: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
TestUtils.beforeEach(this);
|
||||||
|
|
||||||
|
httpBackend = new MockHttpBackend();
|
||||||
|
|
||||||
|
Matrix.request(httpBackend.requestFn);
|
||||||
|
|
||||||
|
MatrixClientPeg.get = () => Matrix.createClient({
|
||||||
|
baseUrl: 'https://my.home.server',
|
||||||
|
userId: '@me:here',
|
||||||
|
accessToken: '123456789',
|
||||||
|
});
|
||||||
|
|
||||||
|
summaryResponse = {
|
||||||
|
profile: {
|
||||||
|
avatar_url: "mxc://someavatarurl",
|
||||||
|
is_openly_joinable: true,
|
||||||
|
is_public: true,
|
||||||
|
long_description: "This is a <b>LONG</b> description.",
|
||||||
|
name: "The name of a community",
|
||||||
|
short_description: "This is a community",
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
users_section: usersSection,
|
||||||
|
rooms_section: roomsSection,
|
||||||
|
};
|
||||||
|
summaryResponseWithNoLongDesc = {
|
||||||
|
profile: {
|
||||||
|
avatar_url: "mxc://someavatarurl",
|
||||||
|
is_openly_joinable: true,
|
||||||
|
is_public: true,
|
||||||
|
long_description: null,
|
||||||
|
name: "The name of a community",
|
||||||
|
short_description: "This is a community",
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
users_section: usersSection,
|
||||||
|
rooms_section: roomsSection,
|
||||||
|
};
|
||||||
|
summaryResponseWithComplicatedLongDesc = {
|
||||||
|
profile: {
|
||||||
|
avatar_url: "mxc://someavatarurl",
|
||||||
|
is_openly_joinable: true,
|
||||||
|
is_public: true,
|
||||||
|
long_description: `
|
||||||
|
<h1>This is a more complicated group page</h1>
|
||||||
|
<p>With paragraphs</p>
|
||||||
|
<ul>
|
||||||
|
<li>And lists!</li>
|
||||||
|
<li>With list items.</li>
|
||||||
|
</ul>
|
||||||
|
<p>And also images: <img src="mxc://someimageurl"/></p>`,
|
||||||
|
name: "The name of a community",
|
||||||
|
short_description: "This is a community",
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
users_section: usersSection,
|
||||||
|
rooms_section: roomsSection,
|
||||||
|
};
|
||||||
|
|
||||||
|
summaryResponseWithBadImg = {
|
||||||
|
profile: {
|
||||||
|
avatar_url: "mxc://someavatarurl",
|
||||||
|
is_openly_joinable: true,
|
||||||
|
is_public: true,
|
||||||
|
long_description: '<p>Evil image: <img src="http://evilimageurl"/></p>',
|
||||||
|
name: "The name of a community",
|
||||||
|
short_description: "This is a community",
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
users_section: usersSection,
|
||||||
|
rooms_section: roomsSection,
|
||||||
|
};
|
||||||
|
|
||||||
|
groupId = "+" + Math.random().toString(16).slice(2) + ':domain';
|
||||||
|
groupIdEncoded = encodeURIComponent(groupId);
|
||||||
|
|
||||||
|
rootElement = document.createElement('div');
|
||||||
|
root = ReactDOM.render(<WrappedGroupView groupId={groupId} />, rootElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
ReactDOM.unmountComponentAtNode(rootElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a spinner when first displayed', function() {
|
||||||
|
ReactTestUtils.findRenderedComponentWithType(root, Spinner);
|
||||||
|
|
||||||
|
// If we don't respond here, the rate limiting done to ensure a maximum of
|
||||||
|
// 3 concurrent network requests for GroupStore will block subsequent requests
|
||||||
|
// in other tests.
|
||||||
|
//
|
||||||
|
// This is a good case for doing the rate limiting somewhere other than the module
|
||||||
|
// scope of GroupStore.js
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse);
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
|
||||||
|
|
||||||
|
return httpBackend.flush(undefined, undefined, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate failure after failed /summary', function() {
|
||||||
|
const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
|
||||||
|
const prom = waitForUpdate(groupView).then(() => {
|
||||||
|
ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_error');
|
||||||
|
});
|
||||||
|
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(500, {});
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
|
||||||
|
|
||||||
|
httpBackend.flush(undefined, undefined, 0);
|
||||||
|
return prom;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a group avatar, name, id and short description after successful /summary', function() {
|
||||||
|
const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
|
||||||
|
const prom = waitForUpdate(groupView).then(() => {
|
||||||
|
ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView');
|
||||||
|
|
||||||
|
const avatar = ReactTestUtils.findRenderedComponentWithType(root, sdk.getComponent('avatars.GroupAvatar'));
|
||||||
|
const img = ReactTestUtils.findRenderedDOMComponentWithTag(avatar, 'img');
|
||||||
|
const avatarImgElement = ReactDOM.findDOMNode(img);
|
||||||
|
expect(avatarImgElement).toExist();
|
||||||
|
expect(avatarImgElement.src).toInclude(
|
||||||
|
'https://my.home.server/_matrix/media/v1/thumbnail/' +
|
||||||
|
'someavatarurl?width=48&height=48&method=crop',
|
||||||
|
);
|
||||||
|
|
||||||
|
const name = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_header_name');
|
||||||
|
const nameElement = ReactDOM.findDOMNode(name);
|
||||||
|
expect(nameElement).toExist();
|
||||||
|
expect(nameElement.innerText).toInclude('The name of a community');
|
||||||
|
expect(nameElement.innerText).toInclude(groupId);
|
||||||
|
|
||||||
|
const shortDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_header_shortDesc');
|
||||||
|
const shortDescElement = ReactDOM.findDOMNode(shortDesc);
|
||||||
|
expect(shortDescElement).toExist();
|
||||||
|
expect(shortDescElement.innerText).toBe('This is a community');
|
||||||
|
});
|
||||||
|
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse);
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
|
||||||
|
|
||||||
|
httpBackend.flush(undefined, undefined, 0);
|
||||||
|
return prom;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a simple long description after successful /summary', function() {
|
||||||
|
const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
|
||||||
|
const prom = waitForUpdate(groupView).then(() => {
|
||||||
|
ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView');
|
||||||
|
|
||||||
|
const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc');
|
||||||
|
const longDescElement = ReactDOM.findDOMNode(longDesc);
|
||||||
|
expect(longDescElement).toExist();
|
||||||
|
expect(longDescElement.innerText).toBe('This is a LONG description.');
|
||||||
|
expect(longDescElement.innerHTML).toBe('<div dir="auto">This is a <b>LONG</b> description.</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse);
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
|
||||||
|
|
||||||
|
httpBackend.flush(undefined, undefined, 0);
|
||||||
|
return prom;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a placeholder if a long description is not set', function() {
|
||||||
|
const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
|
||||||
|
const prom = waitForUpdate(groupView).then(() => {
|
||||||
|
const placeholder = ReactTestUtils
|
||||||
|
.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc_placeholder');
|
||||||
|
const placeholderElement = ReactDOM.findDOMNode(placeholder);
|
||||||
|
expect(placeholderElement).toExist();
|
||||||
|
});
|
||||||
|
|
||||||
|
httpBackend
|
||||||
|
.when('GET', '/groups/' + groupIdEncoded + '/summary')
|
||||||
|
.respond(200, summaryResponseWithNoLongDesc);
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
|
||||||
|
|
||||||
|
httpBackend.flush(undefined, undefined, 0);
|
||||||
|
return prom;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a complicated long description after successful /summary', function() {
|
||||||
|
const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
|
||||||
|
const prom = waitForUpdate(groupView).then(() => {
|
||||||
|
const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc');
|
||||||
|
const longDescElement = ReactDOM.findDOMNode(longDesc);
|
||||||
|
expect(longDescElement).toExist();
|
||||||
|
|
||||||
|
expect(longDescElement.innerHTML).toInclude('<h1>This is a more complicated group page</h1>');
|
||||||
|
expect(longDescElement.innerHTML).toInclude('<p>With paragraphs</p>');
|
||||||
|
expect(longDescElement.innerHTML).toInclude('<ul>');
|
||||||
|
expect(longDescElement.innerHTML).toInclude('<li>And lists!</li>');
|
||||||
|
|
||||||
|
const imgSrc = "https://my.home.server/_matrix/media/v1/thumbnail/someimageurl?width=800&height=600";
|
||||||
|
expect(longDescElement.innerHTML).toInclude('<img src="' + imgSrc + '">');
|
||||||
|
});
|
||||||
|
|
||||||
|
httpBackend
|
||||||
|
.when('GET', '/groups/' + groupIdEncoded + '/summary')
|
||||||
|
.respond(200, summaryResponseWithComplicatedLongDesc);
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
|
||||||
|
|
||||||
|
httpBackend.flush(undefined, undefined, 0);
|
||||||
|
return prom;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disallow images with non-mxc URLs', function() {
|
||||||
|
const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
|
||||||
|
const prom = waitForUpdate(groupView).then(() => {
|
||||||
|
const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc');
|
||||||
|
const longDescElement = ReactDOM.findDOMNode(longDesc);
|
||||||
|
expect(longDescElement).toExist();
|
||||||
|
|
||||||
|
// If this fails, the URL could be in an img `src`, which is what we care about but
|
||||||
|
// there's no harm in keeping this simple and checking the entire HTML string.
|
||||||
|
expect(longDescElement.innerHTML).toExclude('evilimageurl');
|
||||||
|
});
|
||||||
|
|
||||||
|
httpBackend
|
||||||
|
.when('GET', '/groups/' + groupIdEncoded + '/summary')
|
||||||
|
.respond(200, summaryResponseWithBadImg);
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
|
||||||
|
|
||||||
|
httpBackend.flush(undefined, undefined, 0);
|
||||||
|
return prom;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a RoomDetailList after a successful /summary & /rooms (no rooms returned)', function() {
|
||||||
|
const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
|
||||||
|
const prom = waitForUpdate(groupView).then(() => {
|
||||||
|
const roomDetailList = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_RoomDetailList');
|
||||||
|
const roomDetailListElement = ReactDOM.findDOMNode(roomDetailList);
|
||||||
|
expect(roomDetailListElement).toExist();
|
||||||
|
});
|
||||||
|
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse);
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
|
||||||
|
|
||||||
|
httpBackend.flush(undefined, undefined, 0);
|
||||||
|
return prom;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a RoomDetailList after a successful /summary & /rooms (with a single room)', function() {
|
||||||
|
const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
|
||||||
|
const prom = waitForUpdate(groupView).then(() => {
|
||||||
|
const roomDetailList = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_RoomDetailList');
|
||||||
|
const roomDetailListElement = ReactDOM.findDOMNode(roomDetailList);
|
||||||
|
expect(roomDetailListElement).toExist();
|
||||||
|
|
||||||
|
const roomDetailListRoomName = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||||
|
root,
|
||||||
|
'mx_RoomDirectory_name',
|
||||||
|
);
|
||||||
|
const roomDetailListRoomNameElement = ReactDOM.findDOMNode(roomDetailListRoomName);
|
||||||
|
|
||||||
|
expect(roomDetailListRoomNameElement).toExist();
|
||||||
|
expect(roomDetailListRoomNameElement.innerText).toEqual('Some room name');
|
||||||
|
});
|
||||||
|
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse);
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
|
||||||
|
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [{
|
||||||
|
avatar_url: "mxc://someroomavatarurl",
|
||||||
|
canonical_alias: "#somealias:domain",
|
||||||
|
guest_can_join: true,
|
||||||
|
is_public: true,
|
||||||
|
name: "Some room name",
|
||||||
|
num_joined_members: 123,
|
||||||
|
room_id: "!someroomid",
|
||||||
|
topic: "some topic",
|
||||||
|
world_readable: true,
|
||||||
|
}] });
|
||||||
|
|
||||||
|
httpBackend.flush(undefined, undefined, 0);
|
||||||
|
return prom;
|
||||||
|
});
|
||||||
|
});
|
|
@ -75,39 +75,37 @@ describe('MessageComposerInput', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not send messages when composer is empty', () => {
|
it('should not send messages when composer is empty', () => {
|
||||||
const textSpy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
const htmlSpy = sinon.spy(client, 'sendHtmlMessage');
|
|
||||||
mci.enableRichtext(true);
|
mci.enableRichtext(true);
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(textSpy.calledOnce).toEqual(false, 'should not send text message');
|
expect(spy.calledOnce).toEqual(false, 'should not send message');
|
||||||
expect(htmlSpy.calledOnce).toEqual(false, 'should not send html message');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not change content unnecessarily on RTE -> Markdown conversion', () => {
|
it('should not change content unnecessarily on RTE -> Markdown conversion', () => {
|
||||||
const spy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(true);
|
mci.enableRichtext(true);
|
||||||
addTextToDraft('a');
|
addTextToDraft('a');
|
||||||
mci.handleKeyCommand('toggle-mode');
|
mci.handleKeyCommand('toggle-mode');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
expect(spy.calledOnce).toEqual(true);
|
||||||
expect(spy.args[0][1]).toEqual('a');
|
expect(spy.args[0][1].body).toEqual('a');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not change content unnecessarily on Markdown -> RTE conversion', () => {
|
it('should not change content unnecessarily on Markdown -> RTE conversion', () => {
|
||||||
const spy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('a');
|
addTextToDraft('a');
|
||||||
mci.handleKeyCommand('toggle-mode');
|
mci.handleKeyCommand('toggle-mode');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
expect(spy.calledOnce).toEqual(true);
|
||||||
expect(spy.args[0][1]).toEqual('a');
|
expect(spy.args[0][1].body).toEqual('a');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send emoji messages when rich text is enabled', () => {
|
it('should send emoji messages when rich text is enabled', () => {
|
||||||
const spy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(true);
|
mci.enableRichtext(true);
|
||||||
addTextToDraft('☹');
|
addTextToDraft('☹');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
@ -116,7 +114,7 @@ describe('MessageComposerInput', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send emoji messages when Markdown is enabled', () => {
|
it('should send emoji messages when Markdown is enabled', () => {
|
||||||
const spy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('☹');
|
addTextToDraft('☹');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
@ -149,98 +147,98 @@ describe('MessageComposerInput', () => {
|
||||||
// });
|
// });
|
||||||
|
|
||||||
it('should insert formatting characters in Markdown mode', () => {
|
it('should insert formatting characters in Markdown mode', () => {
|
||||||
const spy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
mci.handleKeyCommand('italic');
|
mci.handleKeyCommand('italic');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
expect(['__', '**']).toContain(spy.args[0][1]);
|
expect(['__', '**']).toContain(spy.args[0][1].body);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not entity-encode " in Markdown mode', () => {
|
it('should not entity-encode " in Markdown mode', () => {
|
||||||
const spy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('"');
|
addTextToDraft('"');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
expect(spy.calledOnce).toEqual(true);
|
||||||
expect(spy.args[0][1]).toEqual('"');
|
expect(spy.args[0][1].body).toEqual('"');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should escape characters without other markup in Markdown mode', () => {
|
it('should escape characters without other markup in Markdown mode', () => {
|
||||||
const spy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('\\*escaped\\*');
|
addTextToDraft('\\*escaped\\*');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
expect(spy.calledOnce).toEqual(true);
|
||||||
expect(spy.args[0][1]).toEqual('*escaped*');
|
expect(spy.args[0][1].body).toEqual('*escaped*');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should escape characters with other markup in Markdown mode', () => {
|
it('should escape characters with other markup in Markdown mode', () => {
|
||||||
const spy = sinon.spy(client, 'sendHtmlMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('\\*escaped\\* *italic*');
|
addTextToDraft('\\*escaped\\* *italic*');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
expect(spy.calledOnce).toEqual(true);
|
||||||
expect(spy.args[0][1]).toEqual('\\*escaped\\* *italic*');
|
expect(spy.args[0][1].body).toEqual('\\*escaped\\* *italic*');
|
||||||
expect(spy.args[0][2]).toEqual('*escaped* <em>italic</em>');
|
expect(spy.args[0][1].formatted_body).toEqual('*escaped* <em>italic</em>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not convert -_- into a horizontal rule in Markdown mode', () => {
|
it('should not convert -_- into a horizontal rule in Markdown mode', () => {
|
||||||
const spy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('-_-');
|
addTextToDraft('-_-');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
expect(spy.calledOnce).toEqual(true);
|
||||||
expect(spy.args[0][1]).toEqual('-_-');
|
expect(spy.args[0][1].body).toEqual('-_-');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not strip <del> tags in Markdown mode', () => {
|
it('should not strip <del> tags in Markdown mode', () => {
|
||||||
const spy = sinon.spy(client, 'sendHtmlMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('<del>striked-out</del>');
|
addTextToDraft('<del>striked-out</del>');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
expect(spy.calledOnce).toEqual(true);
|
||||||
expect(spy.args[0][1]).toEqual('<del>striked-out</del>');
|
expect(spy.args[0][1].body).toEqual('<del>striked-out</del>');
|
||||||
expect(spy.args[0][2]).toEqual('<del>striked-out</del>');
|
expect(spy.args[0][1].formatted_body).toEqual('<del>striked-out</del>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not strike-through ~~~ in Markdown mode', () => {
|
it('should not strike-through ~~~ in Markdown mode', () => {
|
||||||
const spy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('~~~striked-out~~~');
|
addTextToDraft('~~~striked-out~~~');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
expect(spy.calledOnce).toEqual(true);
|
||||||
expect(spy.args[0][1]).toEqual('~~~striked-out~~~');
|
expect(spy.args[0][1].body).toEqual('~~~striked-out~~~');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not mark single unmarkedup paragraphs as HTML in Markdown mode', () => {
|
it('should not mark single unmarkedup paragraphs as HTML in Markdown mode', () => {
|
||||||
const spy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('Lorem ipsum dolor sit amet, consectetur adipiscing elit.');
|
addTextToDraft('Lorem ipsum dolor sit amet, consectetur adipiscing elit.');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
expect(spy.calledOnce).toEqual(true);
|
||||||
expect(spy.args[0][1]).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit.');
|
expect(spy.args[0][1].body).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not mark two unmarkedup paragraphs as HTML in Markdown mode', () => {
|
it('should not mark two unmarkedup paragraphs as HTML in Markdown mode', () => {
|
||||||
const spy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nFusce congue sapien sed neque molestie volutpat.');
|
addTextToDraft('Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nFusce congue sapien sed neque molestie volutpat.');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
expect(spy.calledOnce).toEqual(true);
|
||||||
expect(spy.args[0][1]).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nFusce congue sapien sed neque molestie volutpat.');
|
expect(spy.args[0][1].body).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nFusce congue sapien sed neque molestie volutpat.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should strip tab-completed mentions so that only the display name is sent in the plain body in Markdown mode', () => {
|
it('should strip tab-completed mentions so that only the display name is sent in the plain body in Markdown mode', () => {
|
||||||
// Sending a HTML message because we have entities in the composer (because of completions)
|
// Sending a HTML message because we have entities in the composer (because of completions)
|
||||||
const spy = sinon.spy(client, 'sendHtmlMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
mci.setDisplayedCompletion({
|
mci.setDisplayedCompletion({
|
||||||
completion: 'Some Member',
|
completion: 'Some Member',
|
||||||
|
@ -250,11 +248,11 @@ describe('MessageComposerInput', () => {
|
||||||
|
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.args[0][1]).toEqual(
|
expect(spy.args[0][1].body).toEqual(
|
||||||
'Some Member',
|
'Some Member',
|
||||||
'the plaintext body should only include the display name',
|
'the plaintext body should only include the display name',
|
||||||
);
|
);
|
||||||
expect(spy.args[0][2]).toEqual(
|
expect(spy.args[0][1].formatted_body).toEqual(
|
||||||
'<a href="https://matrix.to/#/@some_member:domain.bla">Some Member</a>',
|
'<a href="https://matrix.to/#/@some_member:domain.bla">Some Member</a>',
|
||||||
'the html body should contain an anchor tag with a matrix.to href and display name text',
|
'the html body should contain an anchor tag with a matrix.to href and display name text',
|
||||||
);
|
);
|
||||||
|
@ -262,7 +260,7 @@ describe('MessageComposerInput', () => {
|
||||||
|
|
||||||
it('should strip tab-completed mentions so that only the display name is sent in the plain body in RTE mode', () => {
|
it('should strip tab-completed mentions so that only the display name is sent in the plain body in RTE mode', () => {
|
||||||
// Sending a HTML message because we have entities in the composer (because of completions)
|
// Sending a HTML message because we have entities in the composer (because of completions)
|
||||||
const spy = sinon.spy(client, 'sendHtmlMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(true);
|
mci.enableRichtext(true);
|
||||||
mci.setDisplayedCompletion({
|
mci.setDisplayedCompletion({
|
||||||
completion: 'Some Member',
|
completion: 'Some Member',
|
||||||
|
@ -272,33 +270,33 @@ describe('MessageComposerInput', () => {
|
||||||
|
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.args[0][1]).toEqual('Some Member');
|
expect(spy.args[0][1].body).toEqual('Some Member');
|
||||||
expect(spy.args[0][2]).toEqual('<a href="https://matrix.to/#/@some_member:domain.bla">Some Member</a>');
|
expect(spy.args[0][1].formatted_body).toEqual('<a href="https://matrix.to/#/@some_member:domain.bla">Some Member</a>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not strip non-tab-completed mentions when manually typing MD', () => {
|
it('should not strip non-tab-completed mentions when manually typing MD', () => {
|
||||||
// Sending a HTML message because we have entities in the composer (because of completions)
|
// Sending a HTML message because we have entities in the composer (because of completions)
|
||||||
const spy = sinon.spy(client, 'sendHtmlMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
// Markdown mode enabled
|
// Markdown mode enabled
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('[My Not-Tab-Completed Mention](https://matrix.to/#/@some_member:domain.bla)');
|
addTextToDraft('[My Not-Tab-Completed Mention](https://matrix.to/#/@some_member:domain.bla)');
|
||||||
|
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.args[0][1]).toEqual('[My Not-Tab-Completed Mention](https://matrix.to/#/@some_member:domain.bla)');
|
expect(spy.args[0][1].body).toEqual('[My Not-Tab-Completed Mention](https://matrix.to/#/@some_member:domain.bla)');
|
||||||
expect(spy.args[0][2]).toEqual('<a href="https://matrix.to/#/@some_member:domain.bla">My Not-Tab-Completed Mention</a>');
|
expect(spy.args[0][1].formatted_body).toEqual('<a href="https://matrix.to/#/@some_member:domain.bla">My Not-Tab-Completed Mention</a>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not strip arbitrary typed (i.e. not tab-completed) MD links', () => {
|
it('should not strip arbitrary typed (i.e. not tab-completed) MD links', () => {
|
||||||
// Sending a HTML message because we have entities in the composer (because of completions)
|
// Sending a HTML message because we have entities in the composer (because of completions)
|
||||||
const spy = sinon.spy(client, 'sendHtmlMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
// Markdown mode enabled
|
// Markdown mode enabled
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('[Click here](https://some.lovely.url)');
|
addTextToDraft('[Click here](https://some.lovely.url)');
|
||||||
|
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.args[0][1]).toEqual('[Click here](https://some.lovely.url)');
|
expect(spy.args[0][1].body).toEqual('[Click here](https://some.lovely.url)');
|
||||||
expect(spy.args[0][2]).toEqual('<a href="https://some.lovely.url">Click here</a>');
|
expect(spy.args[0][1].formatted_body).toEqual('<a href="https://some.lovely.url">Click here</a>');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
293
test/components/views/rooms/RoomList-test.js
Normal file
293
test/components/views/rooms/RoomList-test.js
Normal file
|
@ -0,0 +1,293 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactTestUtils from 'react-addons-test-utils';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import expect from 'expect';
|
||||||
|
import lolex from 'lolex';
|
||||||
|
|
||||||
|
import * as TestUtils from 'test-utils';
|
||||||
|
|
||||||
|
import sdk from '../../../../src/index';
|
||||||
|
import MatrixClientPeg from '../../../../src/MatrixClientPeg';
|
||||||
|
import { DragDropContext } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
|
import dis from '../../../../src/dispatcher';
|
||||||
|
import DMRoomMap from '../../../../src/utils/DMRoomMap.js';
|
||||||
|
import GroupStore from '../../../../src/stores/GroupStore.js';
|
||||||
|
|
||||||
|
import { Room, RoomMember } from 'matrix-js-sdk';
|
||||||
|
|
||||||
|
function generateRoomId() {
|
||||||
|
return '!' + Math.random().toString().slice(2, 10) + ':domain';
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRoom(opts) {
|
||||||
|
const room = new Room(generateRoomId());
|
||||||
|
if (opts) {
|
||||||
|
Object.assign(room, opts);
|
||||||
|
}
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('RoomList', () => {
|
||||||
|
let parentDiv = null;
|
||||||
|
let sandbox = null;
|
||||||
|
let client = null;
|
||||||
|
let root = null;
|
||||||
|
const myUserId = '@me:domain';
|
||||||
|
let clock = null;
|
||||||
|
|
||||||
|
const movingRoomId = '!someroomid';
|
||||||
|
let movingRoom;
|
||||||
|
let otherRoom;
|
||||||
|
|
||||||
|
let myMember;
|
||||||
|
let myOtherMember;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
TestUtils.beforeEach(this);
|
||||||
|
sandbox = TestUtils.stubClient(sandbox);
|
||||||
|
client = MatrixClientPeg.get();
|
||||||
|
client.credentials = {userId: myUserId};
|
||||||
|
|
||||||
|
clock = lolex.install();
|
||||||
|
|
||||||
|
DMRoomMap.makeShared();
|
||||||
|
|
||||||
|
parentDiv = document.createElement('div');
|
||||||
|
document.body.appendChild(parentDiv);
|
||||||
|
|
||||||
|
const RoomList = sdk.getComponent('views.rooms.RoomList');
|
||||||
|
const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList);
|
||||||
|
root = ReactDOM.render(
|
||||||
|
<DragDropContext>
|
||||||
|
<WrappedRoomList searchFilter="" />
|
||||||
|
</DragDropContext>
|
||||||
|
, parentDiv);
|
||||||
|
ReactTestUtils.findRenderedComponentWithType(root, RoomList);
|
||||||
|
|
||||||
|
movingRoom = createRoom({name: 'Moving room'});
|
||||||
|
expect(movingRoom.roomId).toNotBe(null);
|
||||||
|
|
||||||
|
// Mock joined member
|
||||||
|
myMember = new RoomMember(movingRoomId, myUserId);
|
||||||
|
myMember.membership = 'join';
|
||||||
|
movingRoom.getMember = (userId) => ({
|
||||||
|
[client.credentials.userId]: myMember,
|
||||||
|
}[userId]);
|
||||||
|
|
||||||
|
otherRoom = createRoom({name: 'Other room'});
|
||||||
|
myOtherMember = new RoomMember(otherRoom.roomId, myUserId);
|
||||||
|
myOtherMember.membership = 'join';
|
||||||
|
otherRoom.getMember = (userId) => ({
|
||||||
|
[client.credentials.userId]: myOtherMember,
|
||||||
|
}[userId]);
|
||||||
|
|
||||||
|
// Mock the matrix client
|
||||||
|
client.getRooms = () => [
|
||||||
|
movingRoom,
|
||||||
|
otherRoom,
|
||||||
|
createRoom({tags: {'m.favourite': {order: 0.1}}, name: 'Some other room'}),
|
||||||
|
createRoom({tags: {'m.favourite': {order: 0.2}}, name: 'Some other room 2'}),
|
||||||
|
createRoom({tags: {'m.lowpriority': {}}, name: 'Some unimportant room'}),
|
||||||
|
createRoom({tags: {'custom.tag': {}}, name: 'Some room customly tagged'}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const roomMap = {};
|
||||||
|
client.getRooms().forEach((r) => {
|
||||||
|
roomMap[r.roomId] = r;
|
||||||
|
});
|
||||||
|
|
||||||
|
client.getRoom = (roomId) => roomMap[roomId];
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach((done) => {
|
||||||
|
if (parentDiv) {
|
||||||
|
ReactDOM.unmountComponentAtNode(parentDiv);
|
||||||
|
parentDiv.remove();
|
||||||
|
parentDiv = null;
|
||||||
|
}
|
||||||
|
sandbox.restore();
|
||||||
|
|
||||||
|
clock.uninstall();
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
function expectRoomInSubList(room, subListTest) {
|
||||||
|
const RoomSubList = sdk.getComponent('structures.RoomSubList');
|
||||||
|
const RoomTile = sdk.getComponent('views.rooms.RoomTile');
|
||||||
|
|
||||||
|
const subLists = ReactTestUtils.scryRenderedComponentsWithType(root, RoomSubList);
|
||||||
|
const containingSubList = subLists.find(subListTest);
|
||||||
|
|
||||||
|
let expectedRoomTile;
|
||||||
|
try {
|
||||||
|
const roomTiles = ReactTestUtils.scryRenderedComponentsWithType(containingSubList, RoomTile);
|
||||||
|
console.info({roomTiles: roomTiles.length});
|
||||||
|
expectedRoomTile = roomTiles.find((tile) => tile.props.room === room);
|
||||||
|
} catch (err) {
|
||||||
|
// truncate the error message because it's spammy
|
||||||
|
err.message = 'Error finding RoomTile for ' + room.roomId + ' in ' +
|
||||||
|
subListTest + ': ' +
|
||||||
|
err.message.split('componentType')[0] + '...';
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(expectedRoomTile).toExist();
|
||||||
|
expect(expectedRoomTile.props.room).toBe(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectCorrectMove(oldTag, newTag) {
|
||||||
|
const getTagSubListTest = (tag) => {
|
||||||
|
if (tag === undefined) return (s) => s.props.label.endsWith('Rooms');
|
||||||
|
return (s) => s.props.tagName === tag;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default to finding the destination sublist with newTag
|
||||||
|
const destSubListTest = getTagSubListTest(newTag);
|
||||||
|
const srcSubListTest = getTagSubListTest(oldTag);
|
||||||
|
|
||||||
|
// Set up the room that will be moved such that it has the correct state for a room in
|
||||||
|
// the section for oldTag
|
||||||
|
if (['m.favourite', 'm.lowpriority'].includes(oldTag)) movingRoom.tags = {[oldTag]: {}};
|
||||||
|
if (oldTag === 'im.vector.fake.direct') {
|
||||||
|
// Mock inverse m.direct
|
||||||
|
DMRoomMap.shared().roomToUser = {
|
||||||
|
[movingRoom.roomId]: '@someotheruser:domain',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
dis.dispatch({action: 'MatrixActions.sync', prevState: null, state: 'PREPARED', matrixClient: client});
|
||||||
|
|
||||||
|
clock.runAll();
|
||||||
|
|
||||||
|
expectRoomInSubList(movingRoom, srcSubListTest);
|
||||||
|
|
||||||
|
dis.dispatch({action: 'RoomListActions.tagRoom.pending', request: {
|
||||||
|
oldTag, newTag, room: movingRoom,
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Run all setTimeouts for dispatches and room list rate limiting
|
||||||
|
clock.runAll();
|
||||||
|
|
||||||
|
expectRoomInSubList(movingRoom, destSubListTest);
|
||||||
|
}
|
||||||
|
|
||||||
|
function itDoesCorrectOptimisticUpdatesForDraggedRoomTiles() {
|
||||||
|
describe('does correct optimistic update when dragging from', () => {
|
||||||
|
it('rooms to people', () => {
|
||||||
|
expectCorrectMove(undefined, 'im.vector.fake.direct');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rooms to favourites', () => {
|
||||||
|
expectCorrectMove(undefined, 'm.favourite');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rooms to low priority', () => {
|
||||||
|
expectCorrectMove(undefined, 'm.lowpriority');
|
||||||
|
});
|
||||||
|
|
||||||
|
// XXX: Known to fail - the view does not update immediately to reflect the change.
|
||||||
|
// Whe running the app live, it updates when some other event occurs (likely the
|
||||||
|
// m.direct arriving) that these tests do not fire.
|
||||||
|
xit('people to rooms', () => {
|
||||||
|
expectCorrectMove('im.vector.fake.direct', undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('people to favourites', () => {
|
||||||
|
expectCorrectMove('im.vector.fake.direct', 'm.favourite');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('people to lowpriority', () => {
|
||||||
|
expectCorrectMove('im.vector.fake.direct', 'm.lowpriority');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('low priority to rooms', () => {
|
||||||
|
expectCorrectMove('m.lowpriority', undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('low priority to people', () => {
|
||||||
|
expectCorrectMove('m.lowpriority', 'im.vector.fake.direct');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('low priority to low priority', () => {
|
||||||
|
expectCorrectMove('m.lowpriority', 'm.lowpriority');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('favourites to rooms', () => {
|
||||||
|
expectCorrectMove('m.favourite', undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('favourites to people', () => {
|
||||||
|
expectCorrectMove('m.favourite', 'im.vector.fake.direct');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('favourites to low priority', () => {
|
||||||
|
expectCorrectMove('m.favourite', 'm.lowpriority');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('when no tags are selected', () => {
|
||||||
|
itDoesCorrectOptimisticUpdatesForDraggedRoomTiles();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when tags are selected', () => {
|
||||||
|
function setupSelectedTag() {
|
||||||
|
// Simulate a complete sync BEFORE dispatching anything else
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'MatrixActions.sync',
|
||||||
|
prevState: null,
|
||||||
|
state: 'PREPARED',
|
||||||
|
matrixClient: client,
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
// Simulate joined groups being received
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'GroupActions.fetchJoinedGroups.success',
|
||||||
|
result: {
|
||||||
|
groups: ['+group:domain'],
|
||||||
|
},
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
// Simulate receiving tag ordering account data
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'MatrixActions.accountData',
|
||||||
|
event_type: 'im.vector.web.tag_ordering',
|
||||||
|
event_content: {
|
||||||
|
tags: ['+group:domain'],
|
||||||
|
},
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
// GroupStore is not flux, mock and notify
|
||||||
|
GroupStore.getGroupRooms = (groupId) => {
|
||||||
|
return [movingRoom];
|
||||||
|
};
|
||||||
|
GroupStore._notifyListeners();
|
||||||
|
|
||||||
|
// Select tag
|
||||||
|
dis.dispatch({action: 'select_tag', tag: '+group:domain'}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setupSelectedTag();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays the correct rooms when the groups rooms are changed', () => {
|
||||||
|
GroupStore.getGroupRooms = (groupId) => {
|
||||||
|
return [movingRoom, otherRoom];
|
||||||
|
};
|
||||||
|
GroupStore._notifyListeners();
|
||||||
|
|
||||||
|
// Run through RoomList debouncing
|
||||||
|
clock.runAll();
|
||||||
|
|
||||||
|
// By default, the test will
|
||||||
|
expectRoomInSubList(otherRoom, (s) => s.props.label.endsWith('Rooms'));
|
||||||
|
});
|
||||||
|
|
||||||
|
itDoesCorrectOptimisticUpdatesForDraggedRoomTiles();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
@ -74,6 +74,7 @@ export function createTestClient() {
|
||||||
getPushActionsForEvent: sinon.stub(),
|
getPushActionsForEvent: sinon.stub(),
|
||||||
getRoom: sinon.stub().returns(mkStubRoom()),
|
getRoom: sinon.stub().returns(mkStubRoom()),
|
||||||
getRooms: sinon.stub().returns([]),
|
getRooms: sinon.stub().returns([]),
|
||||||
|
getGroups: sinon.stub().returns([]),
|
||||||
loginFlows: sinon.stub(),
|
loginFlows: sinon.stub(),
|
||||||
on: sinon.stub(),
|
on: sinon.stub(),
|
||||||
removeListener: sinon.stub(),
|
removeListener: sinon.stub(),
|
||||||
|
@ -92,10 +93,10 @@ export function createTestClient() {
|
||||||
content: {},
|
content: {},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
mxcUrlToHttp: (mxc) => 'http://this.is.a.url/',
|
||||||
setAccountData: sinon.stub(),
|
setAccountData: sinon.stub(),
|
||||||
sendTyping: sinon.stub().returns(Promise.resolve({})),
|
sendTyping: sinon.stub().returns(Promise.resolve({})),
|
||||||
sendTextMessage: () => Promise.resolve({}),
|
sendMessage: () => Promise.resolve({}),
|
||||||
sendHtmlMessage: () => Promise.resolve({}),
|
|
||||||
getSyncState: () => "SYNCING",
|
getSyncState: () => "SYNCING",
|
||||||
generateClientSecret: () => "t35tcl1Ent5ECr3T",
|
generateClientSecret: () => "t35tcl1Ent5ECr3T",
|
||||||
isGuest: () => false,
|
isGuest: () => false,
|
||||||
|
@ -301,3 +302,23 @@ export function wrapInMatrixClientContext(WrappedComponent) {
|
||||||
}
|
}
|
||||||
return Wrapper;
|
return Wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call fn before calling componentDidUpdate on a react component instance, inst.
|
||||||
|
* @param {React.Component} inst an instance of a React component.
|
||||||
|
* @returns {Promise} promise that resolves when componentDidUpdate is called on
|
||||||
|
* given component instance.
|
||||||
|
*/
|
||||||
|
export function waitForUpdate(inst) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const cdu = inst.componentDidUpdate;
|
||||||
|
|
||||||
|
inst.componentDidUpdate = (prevProps, prevState, snapshot) => {
|
||||||
|
resolve();
|
||||||
|
|
||||||
|
if (cdu) cdu(prevProps, prevState, snapshot);
|
||||||
|
|
||||||
|
inst.componentDidUpdate = cdu;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue