Compare commits
14 Commits
Author | SHA1 | Date |
---|---|---|
Sven Slootweg | 25d51ef4eb | 5 years ago |
Sven Slootweg | 27dc997d70 | 5 years ago |
Sven Slootweg | 6ab1082541 | 5 years ago |
Sven Slootweg | 3cc671c87d | 5 years ago |
Sven Slootweg | 49ae582ba2 | 5 years ago |
Sven Slootweg | b33207a23c | 5 years ago |
Sven Slootweg | 89efaed81a | 5 years ago |
Sven Slootweg | 5eb2e66a15 | 5 years ago |
Sven Slootweg | ed9fd6ec46 | 5 years ago |
Sven Slootweg | 0a559317ac | 5 years ago |
Sven Slootweg | 1bc0205bbf | 5 years ago |
Sven Slootweg | aca6f3768d | 5 years ago |
Sven Slootweg | b7f794ffa6 | 5 years ago |
Sven Slootweg | 799da40524 | 5 years ago |
@ -0,0 +1,78 @@
|
|||||||
|
module.exports = {
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"commonjs": true,
|
||||||
|
"es6": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"experimentalObjectRestSpread": true,
|
||||||
|
"jsx": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"react"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
/* Things that should effectively be syntax errors. */
|
||||||
|
"indent": [ "error", "tab", {
|
||||||
|
SwitchCase: 1
|
||||||
|
}],
|
||||||
|
"linebreak-style": [ "error", "unix" ],
|
||||||
|
"semi": [ "error", "always" ],
|
||||||
|
/* Things that are always mistakes. */
|
||||||
|
"getter-return": [ "error" ],
|
||||||
|
"no-compare-neg-zero": [ "error" ],
|
||||||
|
"no-dupe-args": [ "error" ],
|
||||||
|
"no-dupe-keys": [ "error" ],
|
||||||
|
"no-duplicate-case": [ "error" ],
|
||||||
|
"no-empty": [ "error" ],
|
||||||
|
"no-empty-character-class": [ "error" ],
|
||||||
|
"no-ex-assign": [ "error" ],
|
||||||
|
"no-extra-semi": [ "error" ],
|
||||||
|
"no-func-assign": [ "error" ],
|
||||||
|
"no-invalid-regexp": [ "error" ],
|
||||||
|
"no-irregular-whitespace": [ "error" ],
|
||||||
|
"no-obj-calls": [ "error" ],
|
||||||
|
"no-sparse-arrays": [ "error" ],
|
||||||
|
"no-undef": [ "error" ],
|
||||||
|
"no-unreachable": [ "error" ],
|
||||||
|
"no-unsafe-finally": [ "error" ],
|
||||||
|
"use-isnan": [ "error" ],
|
||||||
|
"valid-typeof": [ "error" ],
|
||||||
|
"curly": [ "error" ],
|
||||||
|
"no-caller": [ "error" ],
|
||||||
|
"no-fallthrough": [ "error" ],
|
||||||
|
"no-extra-bind": [ "error" ],
|
||||||
|
"no-extra-label": [ "error" ],
|
||||||
|
"array-callback-return": [ "error" ],
|
||||||
|
"prefer-promise-reject-errors": [ "error" ],
|
||||||
|
"no-with": [ "error" ],
|
||||||
|
"no-useless-concat": [ "error" ],
|
||||||
|
"no-unused-labels": [ "error" ],
|
||||||
|
"no-unused-expressions": [ "error" ],
|
||||||
|
"no-unused-vars": [ "error" , { argsIgnorePattern: "^_" } ],
|
||||||
|
"no-return-assign": [ "error" ],
|
||||||
|
"no-self-assign": [ "error" ],
|
||||||
|
"no-new-wrappers": [ "error" ],
|
||||||
|
"no-redeclare": [ "error" ],
|
||||||
|
"no-loop-func": [ "error" ],
|
||||||
|
"no-implicit-globals": [ "error" ],
|
||||||
|
"strict": [ "error", "global" ],
|
||||||
|
/* Make JSX not cause 'unused variable' errors. */
|
||||||
|
"react/jsx-uses-react": ["error"],
|
||||||
|
"react/jsx-uses-vars": ["error"],
|
||||||
|
/* Development code that should be removed before deployment. */
|
||||||
|
"no-console": [ "warn" ],
|
||||||
|
"no-constant-condition": [ "warn" ],
|
||||||
|
"no-debugger": [ "warn" ],
|
||||||
|
"no-alert": [ "warn" ],
|
||||||
|
"no-warning-comments": ["warn", {
|
||||||
|
terms: ["fixme"]
|
||||||
|
}],
|
||||||
|
/* Common mistakes that can *occasionally* be intentional. */
|
||||||
|
"no-template-curly-in-string": ["warn"],
|
||||||
|
"no-unsafe-negation": [ "warn" ],
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch debugging in Firefox",
|
||||||
|
"type": "firefox",
|
||||||
|
"request": "launch",
|
||||||
|
"reAttach": true,
|
||||||
|
"url": "http://localhost:3000/",
|
||||||
|
"webRoot": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Attach",
|
||||||
|
"type": "firefox",
|
||||||
|
"request": "attach"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Launch WebExtension",
|
||||||
|
"type": "firefox",
|
||||||
|
"request": "launch",
|
||||||
|
"reAttach": true,
|
||||||
|
"addonPath": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -1,88 +1,88 @@
|
|||||||
const gulp = require('gulp')
|
"use strict";
|
||||||
const sass = require('gulp-sass')
|
|
||||||
const concat = require('gulp-concat')
|
|
||||||
const gutil = require('gulp-util')
|
|
||||||
const imagemin = require('gulp-imagemin')
|
|
||||||
const cache = require('gulp-cache')
|
|
||||||
const gulpIf = require('gulp-if')
|
|
||||||
const browserify = require('browserify')
|
|
||||||
const del = require('del')
|
|
||||||
|
|
||||||
const source = require('vinyl-source-stream')
|
const gulp = require('gulp');
|
||||||
const buffer = require('vinyl-buffer')
|
const sass = require('gulp-sass');
|
||||||
const sourcemaps = require('gulp-sourcemaps')
|
const concat = require('gulp-concat');
|
||||||
|
const imagemin = require('gulp-imagemin');
|
||||||
|
const cache = require('gulp-cache');
|
||||||
|
const gulpIf = require('gulp-if');
|
||||||
|
const browserify = require('browserify');
|
||||||
|
const del = require('del');
|
||||||
|
|
||||||
const budo = require('budo')
|
const source = require('vinyl-source-stream');
|
||||||
const babelify = require('babelify')
|
const buffer = require('vinyl-buffer');
|
||||||
|
const sourcemaps = require('gulp-sourcemaps');
|
||||||
|
|
||||||
const cssFiles = 'src/scss/**/*.?(s)css'
|
const budo = require('budo');
|
||||||
|
const babelify = require('babelify');
|
||||||
|
|
||||||
let css = gulp.src(cssFiles)
|
const cssFiles = 'src/scss/**/*.?(s)css';
|
||||||
.pipe(sass())
|
const assetsFiles = [ "src/assets/**/*" ];
|
||||||
.pipe(concat('style.css'))
|
|
||||||
.pipe(gulp.dest('build'))
|
|
||||||
|
|
||||||
gulp.task('watch', function(cb) {
|
|
||||||
budo("src/app.js", {
|
|
||||||
live: true,
|
|
||||||
dir: "build",
|
|
||||||
port: 3000,
|
|
||||||
browserify: {
|
|
||||||
transform: babelify
|
|
||||||
}
|
|
||||||
}).on('exit', cb)
|
|
||||||
gulp.watch(cssFiles, gulp.series(["sass"]))
|
|
||||||
})
|
|
||||||
|
|
||||||
gulp.task("clean", function(done) {
|
gulp.task("clean", function(done) {
|
||||||
del.sync('build')
|
del.sync('build');
|
||||||
done()
|
done();
|
||||||
})
|
});
|
||||||
|
|
||||||
gulp.task("sass", function() {
|
gulp.task("sass", function() {
|
||||||
return gulp.src(cssFiles)
|
return gulp.src(cssFiles)
|
||||||
.pipe(sass())
|
.pipe(sass())
|
||||||
.pipe(concat('style.css'))
|
.pipe(concat('style.css'))
|
||||||
.pipe(gulp.dest('./build'))
|
.pipe(gulp.dest('./build'));
|
||||||
})
|
});
|
||||||
|
|
||||||
gulp.task("assets", function() {
|
gulp.task("assets", function() {
|
||||||
return gulp.src(["src/assets/**/*"])
|
return gulp.src(assetsFiles)
|
||||||
.pipe(gulpIf('*.+(png|jpg|jpeg|gif|svg)',
|
/* NOTE: Currently disabled, causes an error:
|
||||||
cache(imagemin({
|
[19:47:33] Error: Callback called multiple times
|
||||||
interlaced: true
|
at DestroyableTransform.afterTransform (/home/sven/projects/iris/node_modules/gulp-cache/node_modules/readable-stream/lib/_stream_transform.js:82:31)
|
||||||
}))
|
at EventEmitter.signals.on.err (/home/sven/projects/iris/node_modules/gulp-cache/lib/index.js:451:7)
|
||||||
))
|
at emitOne (events.js:116:13)
|
||||||
.pipe(gulp.dest('build'))
|
at EventEmitter.emit (events.js:211:7)
|
||||||
})
|
at DestroyableTransform.onError (/home/sven/projects/iris/node_modules/gulp-cache/lib/index.js:288:15)
|
||||||
|
at Object.onceWrapper (events.js:315:30)
|
||||||
|
at emitOne (events.js:116:13)
|
||||||
|
at DestroyableTransform.emit (events.js:211:7)
|
||||||
|
at /home/sven/projects/iris/node_modules/through2-concurrent/through2-concurrent.js:41:14
|
||||||
|
at imagemin.buffer.then.catch.error (/home/sven/projects/iris/node_modules/gulp-imagemin/index.js:98:5)
|
||||||
|
*/
|
||||||
|
// .pipe(gulpIf('*.+(png|jpg|jpeg|gif|svg)',
|
||||||
|
// cache(imagemin({
|
||||||
|
// interlaced: true
|
||||||
|
// }))
|
||||||
|
// ))
|
||||||
|
.pipe(gulp.dest('build'));
|
||||||
|
});
|
||||||
|
|
||||||
gulp.task('js', function() {
|
gulp.task('js', function() {
|
||||||
return gulp.src(['src/app.js', "src/components/**/*"])
|
let b = browserify({
|
||||||
.pipe(babel({
|
entries: 'src/app.js',
|
||||||
presets: [
|
debug: false,
|
||||||
['@babel/env', {
|
transform: [babelify.configure({
|
||||||
modules: false
|
presets: ['@babel/preset-env', '@babel/preset-react']
|
||||||
}]
|
})]
|
||||||
]
|
});
|
||||||
}))
|
return b.bundle()
|
||||||
.pipe(gulp.dest('build'))
|
.pipe(source('src/app.js'))
|
||||||
})
|
.pipe(buffer())
|
||||||
|
.pipe(sourcemaps.init({ loadMaps: true }))
|
||||||
|
.pipe(gulp.dest('build'));
|
||||||
|
});
|
||||||
|
|
||||||
gulp.task('js', function() {
|
gulp.task('watch', gulp.series(["clean", "assets", "sass", function(cb) {
|
||||||
let b = browserify({
|
budo("src/app.js", {
|
||||||
entries: 'src/app.js',
|
live: true,
|
||||||
debug: false,
|
dir: "build",
|
||||||
transform: [babelify.configure({
|
port: 3000,
|
||||||
presets: ['@babel/preset-env', '@babel/preset-react']
|
browserify: {
|
||||||
})]
|
transform: babelify
|
||||||
})
|
}
|
||||||
return b.bundle()
|
}).on('exit', cb);
|
||||||
.pipe(source('src/app.js'))
|
|
||||||
.pipe(buffer())
|
gulp.watch(cssFiles, gulp.series(["sass"]));
|
||||||
.pipe(sourcemaps.init({ loadMaps: true }))
|
gulp.watch(assetsFiles, gulp.series(["assets"]));
|
||||||
.pipe(gulp.dest('build'))
|
}]));
|
||||||
})
|
|
||||||
|
|
||||||
gulp.task('build', gulp.parallel(['clean', 'assets', 'js', 'sass', function(done) {
|
gulp.task('build', gulp.parallel(['clean', 'assets', 'js', 'sass', function(done) {
|
||||||
done()
|
done();
|
||||||
}]))
|
}]));
|
||||||
|
@ -1,228 +1,260 @@
|
|||||||
'use strict'
|
'use strict';
|
||||||
const React = require('react')
|
|
||||||
const ReactDOM = require('react-dom')
|
const React = require('react');
|
||||||
const create = require('create-react-class')
|
const create = require('create-react-class');
|
||||||
const Promise = require('bluebird')
|
const Promise = require('bluebird');
|
||||||
const urllib = require('url')
|
const urllib = require('url');
|
||||||
const debounce = require('debounce')
|
|
||||||
const defaultValue = require('default-value')
|
const createApiRequester = require("../lib/api-request");
|
||||||
|
|
||||||
let login = create({
|
let login = create({
|
||||||
displayName: "Login",
|
displayName: "Login",
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
error: null,
|
error: null,
|
||||||
formState: {
|
formState: {
|
||||||
user: "",
|
user: "",
|
||||||
pass: "",
|
pass: "",
|
||||||
hs: ""
|
hs: ""
|
||||||
},
|
},
|
||||||
hs: {
|
hs: {
|
||||||
prompt: false,
|
prompt: false,
|
||||||
error: null,
|
error: null,
|
||||||
valid: false
|
valid: false
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
login: function() {
|
login: function() {
|
||||||
this.setState({error: ""})
|
return Promise.try(() => {
|
||||||
|
this.setState({error: ""});
|
||||||
if (this.state.hs.valid) {
|
|
||||||
return this.doLogin()
|
if (this.state.hs.valid) {
|
||||||
}
|
return this.doLogin();
|
||||||
|
}
|
||||||
let parts = this.state.formState.user.split(':')
|
|
||||||
if (parts.length != 2) {
|
let parts = this.state.formState.user.split(':');
|
||||||
return this.setState({error: "Please enter a full mxid, like username:homeserver.tld"})
|
if (parts.length != 2) {
|
||||||
}
|
return this.setState({error: "Please enter a full mxid, like username:homeserver.tld"});
|
||||||
|
}
|
||||||
let hostname = urllib.parse("https://" + parts[1])
|
|
||||||
getApiServer(hostname).then((hs) => {
|
let hostname = urllib.parse("https://" + parts[1]);
|
||||||
console.log("Using API server", hs)
|
|
||||||
let formState = this.state.formState
|
return Promise.try(() => {
|
||||||
formState.user = parts[0]
|
return getApiServer(hostname);
|
||||||
formState.hs = hs
|
}).then((homeserverUrl) => {
|
||||||
let hsState = Object.assign(this.state.hs, {valid: true})
|
console.log("Using API server", homeserverUrl);
|
||||||
this.setState({apiUrl: hs, formState: formState, hs: hsState})
|
|
||||||
this.doLogin()
|
this.setState({
|
||||||
}).catch((error) => {
|
apiUrl: homeserverUrl,
|
||||||
console.log("ERROR fetching homeserver url", error)
|
apiRequest: createApiRequester(homeserverUrl),
|
||||||
let hsState = Object.assign(this.state.hs, {error: error, valid: false, prompt: true})
|
formState: Object.assign(this.state.formState, {
|
||||||
this.setState({hs: hsState})
|
user: parts[0],
|
||||||
})
|
hs: homeserverUrl
|
||||||
},
|
}),
|
||||||
|
hs: Object.assign(this.state.hs, {
|
||||||
doLogin: function() {
|
valid: true
|
||||||
console.log("Logging in")
|
})
|
||||||
let user = this.state.formState.user.replace('@', '')
|
});
|
||||||
let password = this.state.formState.pass
|
|
||||||
let hs = this.state.apiUrl
|
return this.doLogin();
|
||||||
|
}).catch((error) => {
|
||||||
let data = {
|
/* FIXME: Error filtering */
|
||||||
user: user,
|
console.log("ERROR fetching homeserver url", error);
|
||||||
password: password,
|
|
||||||
type: "m.login.password",
|
this.setState({
|
||||||
initial_device_display_name: "Neo v4",
|
hs: Object.assign(this.state.hs, {
|
||||||
};
|
error: error,
|
||||||
|
valid: false,
|
||||||
let url = hs + "/_matrix/client/r0/login"
|
prompt: true
|
||||||
|
})
|
||||||
fetch(url, {
|
});
|
||||||
body: JSON.stringify(data),
|
});
|
||||||
headers: {
|
});
|
||||||
'content-type': 'application/json'
|
},
|
||||||
},
|
|
||||||
method: 'POST',
|
doLogin: function() {
|
||||||
}).then((response) => response.json())
|
return Promise.try(() => {
|
||||||
.then((responseJson) => {
|
console.log("Logging in");
|
||||||
console.log("got access token", responseJson)
|
let user = this.state.formState.user.replace('@', '');
|
||||||
this.setState({json: responseJson})
|
let password = this.state.formState.pass;
|
||||||
if(responseJson.access_token != undefined) {
|
let homeserverUrl = this.state.apiUrl;
|
||||||
this.props.callback(responseJson.user_id, responseJson.access_token, hs)
|
|
||||||
} else {
|
return Promise.try(() => {
|
||||||
this.setState({error: responseJson.error})
|
return this.state.apiRequest("/_matrix/client/r0/login", {
|
||||||
}
|
user: user,
|
||||||
})
|
password: password,
|
||||||
.catch((error) => {
|
type: "m.login.password",
|
||||||
console.error(url, error);
|
initial_device_display_name: "Neo v4",
|
||||||
});
|
});
|
||||||
},
|
}).then((responseJson) => {
|
||||||
|
console.log("got access token", responseJson);
|
||||||
handleUserChange: function(e) {
|
|
||||||
let formState = this.state.formState
|
this.setState({ json: responseJson });
|
||||||
let user = e.target.value
|
|
||||||
formState.user = e.target.value
|
if(responseJson.access_token != undefined) {
|
||||||
let parts = user.split(':')
|
this.props.callback(responseJson.user_id, responseJson.access_token, homeserverUrl);
|
||||||
if (parts.length == 2) {
|
} else {
|
||||||
formState.hs = parts[1]
|
this.setState({ error: responseJson.error });
|
||||||
let hsState = Object.assign(this.state.hs, {error: null, valid: false})
|
}
|
||||||
this.setState({hs: hsState})
|
}).catch((error) => {
|
||||||
}
|
/* FIXME: Why are errors being swallowed here? */
|
||||||
this.setState({formState: formState})
|
console.error(error);
|
||||||
},
|
});
|
||||||
|
});
|
||||||
handlePassChange: function(e) {
|
},
|
||||||
let formState = this.state.formState
|
|
||||||
formState.pass = e.target.value
|
handleUserChange: function(e) {
|
||||||
this.setState({formState: formState})
|
let formState = this.state.formState;
|
||||||
},
|
let user = e.target.value;
|
||||||
|
formState.user = e.target.value;
|
||||||
handleHsChange: function(e) {
|
let parts = user.split(':');
|
||||||
let formState = this.state.formState
|
if (parts.length == 2) {
|
||||||
formState.hs = e.target.value
|
formState.hs = parts[1];
|
||||||
this.setState({formState: formState})
|
let hsState = Object.assign(this.state.hs, {error: null, valid: false});
|
||||||
this.setState({hs: {error: null, valid: false, prompt: true, changed: true}})
|
this.setState({hs: hsState});
|
||||||
},
|
}
|
||||||
|
this.setState({formState: formState});
|
||||||
render: function() {
|
},
|
||||||
let hsState = "inactive"
|
|
||||||
if (this.state.hs.prompt) {
|
handlePassChange: function(e) {
|
||||||
hsState = "active"
|
let formState = this.state.formState;
|
||||||
}
|
formState.pass = e.target.value;
|
||||||
if (this.state.hs.error != null) {
|
this.setState({formState: formState});
|
||||||
hsState = "error"
|
},
|
||||||
}
|
|
||||||
if (this.state.hs.valid) {
|
handleHomeserverChange: function(e) {
|
||||||
hsState = "validated"
|
let formState = this.state.formState;
|
||||||
}
|
formState.hs = e.target.value;
|
||||||
|
this.setState({formState: formState});
|
||||||
return (
|
this.setState({hs: {error: null, valid: false, prompt: true, changed: true}});
|
||||||
<div className="loginwrapper">
|
},
|
||||||
<img src="./neo.png"/>
|
|
||||||
<div className="errorMessage">{this.state.error}</div>
|
render: function() {
|
||||||
<div className="login">
|
let hsState = "inactive";
|
||||||
<label htmlFor="user">Username: </label>
|
if (this.state.hs.prompt) {
|
||||||
<input type="text" id="user" placeholder="@user:homeserver.tld" value={this.state.formState["user"]} onChange={this.handleUserChange}/>
|
hsState = "active";
|
||||||
|
}
|
||||||
<label htmlFor="pass">Password: </label>
|
if (this.state.hs.error != null) {
|
||||||
<input type="password" id="pass" placeholder="password" value={this.state.formState["pass"]} onChange={this.handlePassChange}/>
|
hsState = "error";
|
||||||
|
}
|
||||||
<label htmlFor="hs" className={hsState}>Homeserver: </label>
|
if (this.state.hs.valid) {
|
||||||
{this.state.hs.prompt ? (
|
hsState = "validated";
|
||||||
<>
|
}
|
||||||
<input type="text" id="hs" value={this.state.formState["hs"]} onChange={this.handleHsChange}/>
|
|
||||||
</>
|
return (
|
||||||
) : (
|
<div className="loginwrapper">
|
||||||
<span id="hs">{this.state.formState["hs"]}</span>
|
<img src="./neo.png"/>
|
||||||
)}
|
<div className="errorMessage">{this.state.error}</div>
|
||||||
|
<div className="login">
|
||||||
<button onClick={()=>this.login()}>Log in</button>
|
<label htmlFor="user">Username: </label>
|
||||||
</div>
|
<input type="text" id="user" placeholder="@user:homeserver.tld" value={this.state.formState["user"]} onChange={this.handleUserChange}/>
|
||||||
</div>
|
|
||||||
)
|
<label htmlFor="pass">Password: </label>
|
||||||
}
|
<input type="password" id="pass" placeholder="password" value={this.state.formState["pass"]} onChange={this.handlePassChange}/>
|
||||||
})
|
|
||||||
|
<label htmlFor="hs" className={hsState}>Homeserver: </label>
|
||||||
function getApiServer(hostname) {
|
{this.state.hs.prompt ? (
|
||||||
return new Promise((resolve, reject) => {
|
<>
|
||||||
console.log("Checking for api server from mxid", urllib.format(hostname))
|
<input type="text" id="hs" value={this.state.formState["hs"]} onChange={this.handleHomeserverChange}/>
|
||||||
checkApi(hostname).then(() => {
|
</>
|
||||||
// Hostname is a valid api server
|
) : (
|
||||||
hostname.pathname = ""
|
<span id="hs">{this.state.formState["hs"]}</span>
|
||||||
resolve(urllib.format(hostname))
|
)}
|
||||||
}).catch(() => {
|
|
||||||
console.log("trying .well-known")
|
<button onClick={()=>this.login()}>Log in</button>
|
||||||
tryWellKnown(hostname).then((hostname) => {
|
</div>
|
||||||
console.log("got .well-known host", hostname)
|
</div>
|
||||||
resolve(hostname)
|
);
|
||||||
}).catch((err) => {
|
}
|
||||||
reject("Fatal error trying to get API host")
|
});
|
||||||
})
|
|
||||||
})
|
function getApiServer(parsedUrl) {
|
||||||
})
|
return Promise.try(() => {
|
||||||
|
console.log("Checking for api server from mxid", urllib.format(parsedUrl));
|
||||||
|
|
||||||
|
return checkApi(parsedUrl);
|
||||||
|
}).then(() => {
|
||||||
|
// Hostname is a valid api server
|
||||||
|
return buildUrl(parsedUrl, "");
|
||||||
|
}).catch(() => {
|
||||||
|
/* FIXME: Error filtering */
|
||||||
|
console.log("trying .well-known");
|
||||||
|
|
||||||
|
return Promise.try(() => {
|
||||||
|
return tryWellKnown(parsedUrl);
|
||||||
|
}).then((hostname) => {
|
||||||
|
console.log("got .well-known host", hostname);
|
||||||
|
|
||||||
|
return hostname;
|
||||||
|
}).catch((_err) => {
|
||||||
|
/* FIXME: Error chaining */
|
||||||
|
throw new Error("Fatal error trying to get API host");
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkApi(host) {
|
function checkApi(host) {
|
||||||
let versionUrl = buildUrl(host, "/_matrix/client/versions")
|
return Promise.try(() => {
|
||||||
return new Promise((resolve, reject) => {
|
let versionUrl = buildUrl(host, "/_matrix/client/versions");
|
||||||
fetch(versionUrl).then((response) => {
|
|
||||||
if (response.status != 200) {
|
return Promise.try(() => {
|
||||||
console.log("Invalid homeserver url", versionUrl)
|
return fetch(versionUrl);
|
||||||
return reject()
|
}).then((response) => {
|
||||||
}
|
if (response.status != 200) {
|
||||||
resolve()
|
console.log("Invalid homeserver url", versionUrl);
|
||||||
}).catch((err) => {
|
|
||||||
reject(err)
|
/* FIXME: Error types */
|
||||||
})
|
throw new Error("Invalid homeserver URL");
|
||||||
})
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryWellKnown(host) {
|
function tryWellKnown(host) {
|
||||||
let wellKnownUrl = urllib.format(Object.assign(host, {
|
let wellKnownUrl = urllib.format(Object.assign(host, {
|
||||||
pathname: "/.well-known/matrix/client"
|
pathname: "/.well-known/matrix/client"
|
||||||
}))
|
}));
|
||||||
console.log("Trying", wellKnownUrl, "for .well-known")
|
|
||||||
return new Promise((resolve, reject) => {
|
console.log("Trying", wellKnownUrl, "for .well-known");
|
||||||
return fetch(wellKnownUrl)
|
|
||||||
.then((response) => {
|
return Promise.try(() => {
|
||||||
if (response.status != 200) {
|
return fetch(wellKnownUrl);
|
||||||
console.log("no well-known in use")
|
}).tap((response) => {
|
||||||
reject("No homeserver found")
|
if (response.status != 200) {
|
||||||
}
|
console.log("no well-known in use");
|
||||||
return response
|
|
||||||
}).catch((error) => {
|
/* FIXME: Error type */
|
||||||
reject("can't fetch .well-known")
|
throw new Error("No homeserver found");
|
||||||
})
|
}
|
||||||
.then((response) => response.json())
|
}).catch((_error) => {
|
||||||
.then((json) => {
|
/* FIXME: Error chaining */
|
||||||
console.log("Parsed json", json)
|
throw new Error("can't fetch .well-known");
|
||||||
if (json['m.homeserver'] != undefined && json['m.homeserver'].base_url != undefined) {
|
}).then((response) => {
|
||||||
resolve(json['m.homeserver'].base_url)
|
return response.json();
|
||||||
}
|
}).then((json) => {
|
||||||
})
|
console.log("Parsed json", json);
|
||||||
.catch((err) => {
|
|
||||||
console.log("Error in json", err)
|
if (json['m.homeserver'] != null && json['m.homeserver'].base_url != null) {
|
||||||
reject("Error while parsing .well-known")
|
return json['m.homeserver'].base_url;
|
||||||
})
|
} else {
|
||||||
})
|
/* FIXME: Error type */
|
||||||
|
throw new Error("No homeserver specified in .well-known");
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
/* FIXME: Error filtering? */
|
||||||
|
console.log("Error in json", err);
|
||||||
|
|
||||||
|
/* FIXME: Error chaining */
|
||||||
|
throw new Error("Error while parsing .well-known");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUrl(host, path) {
|
function buildUrl(parsedUrl, path) {
|
||||||
return urllib.format(Object.assign(host, {
|
return urllib.format(Object.assign(parsedUrl, {
|
||||||
pathname: path
|
pathname: path
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = login
|
module.exports = login;
|
||||||
|
@ -1,227 +1,201 @@
|
|||||||
'use strict'
|
"use strict";
|
||||||
const React = require('react')
|
|
||||||
const ReactDOM = require('react-dom')
|
const Promise = require("bluebird");
|
||||||
const create = require('create-react-class')
|
const React = require("react");
|
||||||
const Promise = require('bluebird')
|
const create = require("create-react-class");
|
||||||
const debounce = require('debounce')
|
const sanitize = require("sanitize-html");
|
||||||
const jdenticon = require('jdenticon')
|
const { expression } = require("dataprog");
|
||||||
const defaultValue = require('default-value')
|
|
||||||
const sdk = require('matrix-js-sdk')
|
const Event = require("./events/Event.js");
|
||||||
const sanitize = require('sanitize-html')
|
const Info = require("./info.js");
|
||||||
|
const Input = require("./input.js");
|
||||||
const Event = require('./events/Event.js')
|
const User = require("./events/user.js");
|
||||||
const Info = require('./info.js')
|
const Loading = require("./loading.js");
|
||||||
const Input = require('./input.js')
|
|
||||||
const User = require('./events/user.js')
|
const generateJdenticon = require("../lib/generate-jdenticon");
|
||||||
const Loading = require('./loading.js')
|
const generateThumbnailUrl = require("../lib/generate-thumbnail-url");
|
||||||
|
const groupEvents = require("../lib/group-events");
|
||||||
jdenticon.config = {
|
const parseMXC = require("../lib/parse-mxc");
|
||||||
lightness: {
|
const withElement = require("../lib/with-element");
|
||||||
color: [0.58, 0.66],
|
|
||||||
grayscale: [0.30, 0.90]
|
|
||||||
},
|
|
||||||
saturation: {
|
|
||||||
color: 0.66,
|
|
||||||
grayscale: 0.00
|
|
||||||
},
|
|
||||||
backColor: "#00000000"
|
|
||||||
};
|
|
||||||
|
|
||||||
let eventFunctions = {
|
let eventFunctions = {
|
||||||
plaintext: function() {
|
plaintext: function() {
|
||||||
let plain = "unknown event"
|
if (this.type == "m.room.message") {
|
||||||
|
if (this.content.format == "org.matrix.custom.html") {
|
||||||
if (this.type == "m.room.message") {
|
return sanitize(this.content.formatted_body, {allowedTags: []});
|
||||||
plain = this.content.body
|
} else {
|
||||||
|
return this.content.body;
|
||||||
if (this.content.format == "org.matrix.custom.html") {
|
}
|
||||||
plain = sanitize(this.content.formatted_body, {allowedTags: []})
|
} else if (this.type == "m.room.member") {
|
||||||
}
|
if (this.content.membership == "invite") {
|
||||||
}
|
return `${this.sender} invited ${this.state_key}`;
|
||||||
if (this.type == "m.room.member") {
|
} else if (this.content.membership == "join") {
|
||||||
if (this.content.membership == "invite") {
|
return `${this.state_key} joined the room`;
|
||||||
plain = `${this.sender} invited ${this.state_key}`
|
} else if (this.content.membership == "leave") {
|
||||||
} else if (this.content.membership == "join") {
|
return `${this.state_key} left the room`;
|
||||||
plain = `${this.state_key} joined the room`
|
} else if (this.content.membership == "kick") {
|
||||||
} else if (this.content.membership == "leave") {
|
return `${this.sender} kicked ${this.state_key}`;
|
||||||
plain = `${this.state_key} left the room`
|
} else if (this.content.membership == "ban") {
|
||||||
} else if (this.content.membership == "kick") {
|
return `${this.sender} banned ${this.state_key}`;
|
||||||
plain = `${this.sender} kicked ${this.state_key}`
|
} else {
|
||||||
} else if (this.content.membership == "ban") {
|
return "unknown member event";
|
||||||
plain = `${this.sender} banned ${this.state_key}`
|
}
|
||||||
}
|
} else if (this.type == "m.room.avatar") {
|
||||||
}
|
if (this.content.url.length > 0) {
|
||||||
if (this.type == "m.room.avatar") {
|
return `${this.sender} changed the room avatar`;
|
||||||
if (this.content.url.length > 0) {
|
}
|
||||||
plain = `${this.sender} changed the room avatar`
|
} else if (this.type == "m.room.name") {
|
||||||
}
|
return `${this.sender} changed the room name to ${this.content.name}`;
|
||||||
}
|
} else {
|
||||||
if (this.type == "m.room.name") {
|
return "unknown event";
|
||||||
return `${this.sender} changed the room name to ${this.content.name}`
|
}
|
||||||
}
|
}
|
||||||
return plain
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let chat = create({
|
let chat = create({
|
||||||
displayName: "Chat",
|
displayName: "Chat",
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
ref: null,
|
ref: null,
|
||||||
loading: false
|
loading: false
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getSnapshotBeforeUpdate: function(oldProps, oldState) {
|
getSnapshotBeforeUpdate: function(_oldProps, _oldState) {
|
||||||
let ref = this.state.ref
|
let ref = this.state.ref;
|
||||||
if (ref == null) {return null}
|
|
||||||
if ((ref.scrollHeight - ref.offsetHeight) - ref.scrollTop < 100) { // Less than 100px from bottom
|
if (ref != null && (ref.scrollHeight - ref.offsetHeight) - ref.scrollTop < 100) {
|
||||||
return true
|
// Less than 100px from bottom
|
||||||
}
|
return true;
|
||||||
return null
|
} else {
|
||||||
},
|
return null;
|
||||||
|
}
|
||||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
},
|
||||||
let ref = this.state.ref
|
|
||||||
if (ref == null) {return}
|
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||||
if (snapshot) { // scroll to bottom
|
let ref = this.state.ref;
|
||||||
ref.scrollTop = (ref.scrollHeight - ref.offsetHeight)
|
|
||||||
}
|
if (ref != null && snapshot) {
|
||||||
},
|
// scroll to bottom
|
||||||
|
ref.scrollTop = (ref.scrollHeight - ref.offsetHeight);
|
||||||
setRef: function(ref) {
|
}
|
||||||
if (ref != null) {
|
},
|
||||||
this.setState({ref: ref})
|
|
||||||
}
|
setRef: function(ref) {
|
||||||
},
|
if (ref != null) {
|
||||||
|
this.setState({ ref: ref });
|
||||||
onReplyClick: function(e) {
|
}
|
||||||
this.setState({replyEvent: e})
|
},
|
||||||
},
|
|
||||||
|
onReplyClick: function(e) {
|
||||||
paginateBackwards: function() {
|
this.setState({ replyEvent: e });
|
||||||
if (this.state.loading) {
|
},
|
||||||
return
|
|
||||||
}
|
paginateBackwards: function() {
|
||||||
let client = this.props.client
|
if (!this.state.loading) {
|
||||||
client.paginateEventTimeline(client.getRoom(this.props.roomId).getLiveTimeline(), {backwards: true}).then(() => {
|
let client = this.props.client;
|
||||||
this.setState({loading: false})
|
let timeline = client.getRoom(this.props.roomId).getLiveTimeline();
|
||||||
})
|
|
||||||
this.setState({loading: true})
|
this.setState({loading: true});
|
||||||
},
|
|
||||||
|
return Promise.try(() => {
|
||||||
render: function() {
|
return client.paginateEventTimeline(timeline, {backwards: true});
|
||||||
let client = this.props.client
|
}).then(() => {
|
||||||
let empty = (
|
this.setState({loading: false});
|
||||||
<div className="main">
|
});
|
||||||
</div>
|
}
|
||||||
)
|
},
|
||||||
if (this.props.roomId == undefined) {
|
|
||||||
//empty screen
|
render: function() {
|
||||||
return empty
|
let client = this.props.client;
|
||||||
}
|
|
||||||
|
let empty = <div className="main" />;
|
||||||
let room = client.getRoom(this.props.roomId)
|
|
||||||
if (room == null) {
|
if (this.props.roomId == null) {
|
||||||
return empty
|
//empty screen
|
||||||
}
|
return empty;
|
||||||
|
} else {
|
||||||
let messageGroups = {
|
let room = client.getRoom(this.props.roomId);
|
||||||
current: [],
|
if (room == null) {
|
||||||
groups: [],
|
return empty;
|
||||||
sender: "",
|
} else {
|
||||||
type: ""
|
let liveTimeline = room.getLiveTimeline();
|
||||||
}
|
let liveTimelineEvents = liveTimeline.getEvents();
|
||||||
|
|
||||||
// if the sender is the same, add it to the 'current' messageGroup, if not,
|
let events = liveTimelineEvents.map((item) => {
|
||||||
// push the old one to 'groups' and start with a new array.
|
let event = item.event;
|
||||||
|
|
||||||
let liveTimeline = room.getLiveTimeline()
|
return Object.assign(
|
||||||
let liveTimelineEvents = liveTimeline.getEvents()
|
event,
|
||||||
|
eventFunctions,
|
||||||
let events = []
|
(event.sender == null)
|
||||||
if (liveTimelineEvents.length > 0) {
|
/* Whether this event is a local echo */
|
||||||
liveTimelineEvents.forEach((MatrixEvent) => {
|
? { local: true, sender: event.user_id }
|
||||||
let event = MatrixEvent.event;
|
: null
|
||||||
event = Object.assign(event, eventFunctions)
|
);
|
||||||
if (event.sender == null) { // localecho messages
|
});
|
||||||
event.sender = event.user_id
|
|
||||||
event.local = true
|
let eventGroups = groupEvents(events);
|
||||||
}
|
|
||||||
if (event.sender != messageGroups.sender || event.type != messageGroups.type) {
|
//TODO: replace with something that only renders events in view
|
||||||
messageGroups.sender = event.sender
|
return (
|
||||||
messageGroups.type = event.type
|
<div className="main">
|
||||||
if (messageGroups.current.length != 0) {
|
<Info room={room} />
|
||||||
messageGroups.groups.push(messageGroups.current)
|
<div className="chat" ref={this.setRef}>
|
||||||
}
|
<div className="events">
|
||||||
messageGroups.current = []
|
<div className="paginateBackwards" onClick={this.paginateBackwards}>
|
||||||
}
|
{this.state.loading ?
|
||||||
messageGroups.current.push(event)
|
<Loading/> :
|
||||||
})
|
<span>load older messages</span>
|
||||||
messageGroups.groups.push(messageGroups.current)
|
}
|
||||||
|
</div>
|
||||||
events = messageGroups.groups.map((events, id) => {
|
{(eventGroups.map((group) => {
|
||||||
return <EventGroup key={`${this.props.roomId}-${events[0].event_id}`} events={events} client={this.props.client} room={room} onReplyClick={this.onReplyClick}/>
|
return <EventGroup key={`${this.props.roomId}-${group.events[0].event_id}`} events={group.events} client={this.props.client} room={room} onReplyClick={this.onReplyClick}/>;
|
||||||
})
|
}))}
|
||||||
}
|
</div>
|
||||||
//TODO: replace with something that only renders events in view
|
</div>
|
||||||
return (
|
<Input client={client} roomId={this.props.roomId} replyEvent={this.state.replyEvent} onReplyClick={this.onReplyClick}/>
|
||||||
<div className="main">
|
</div>
|
||||||
<Info room={room} />
|
);
|
||||||
<div className="chat" ref={this.setRef}>
|
}
|
||||||
<div className="events">
|
}
|
||||||
<div className="paginateBackwards" onClick={this.paginateBackwards}>
|
}
|
||||||
{this.state.loading ?
|
});
|
||||||
<Loading/> :
|
|
||||||
<span>load older messages</span>
|
function EventGroup({ events, room, client, onReplyClick }) {
|
||||||
}
|
let setAvatarRef = withElement((element) => {
|
||||||
</div>
|
generateJdenticon(user.userId).update(element);
|
||||||
{events}
|
});
|
||||||
</div>
|
|
||||||
</div>
|
let user = client.getUser(events[0].sender);
|
||||||
<Input client={client} roomId={this.props.roomId} replyEvent={this.state.replyEvent} onReplyClick={this.onReplyClick}/>
|
|
||||||
</div>
|
let avatar = expression(() => {
|
||||||
)
|
if (user.avatarUrl != null) {
|
||||||
}
|
let url = generateThumbnailUrl({
|
||||||
})
|
homeserver: client.baseUrl,
|
||||||
|
mxc: parseMXC(user.avatarUrl),
|
||||||
let EventGroup = create({
|
width: 128,
|
||||||
displayName: "EventGroup",
|
height: 128
|
||||||
|
});
|
||||||
getInitialState: function() {
|
|
||||||
let user = this.props.client.getUser(this.props.events[0].sender)
|
return <img id="avatar" src={url} />;
|
||||||
let avatar = <svg id="avatar" ref={this.avatarRef} />
|
} else {
|
||||||
|
return <svg id="avatar" ref={setAvatarRef} />;
|
||||||
if (user.avatarUrl != null) {
|
}
|
||||||
let hs = this.props.client.baseUrl
|
});
|
||||||
let media_mxc = user.avatarUrl.slice(6)
|
|
||||||
let url = `${hs}/_matrix/media/v1/thumbnail/${media_mxc}?width=128&height=128&method=scale`
|
return (
|
||||||
avatar = <img id="avatar" src={url}/>
|
<div className="eventGroup">
|
||||||
}
|
{avatar}
|
||||||
|
<div className="col">
|
||||||
return {
|
<User user={user}/>
|
||||||
user: user,
|
{events.map((event, key) => {
|
||||||
avatar: avatar
|
return <Event event={event} key={key} client={client} room={room} onReplyClick={onReplyClick}/>;
|
||||||
}
|
})}
|
||||||
},
|
</div>
|
||||||
|
</div>
|
||||||
avatarRef: function(ref) {
|
);
|
||||||
jdenticon.update(ref, this.state.user.userId)
|
}
|
||||||
},
|
|
||||||
|
module.exports = chat;
|
||||||
render: function() {
|
|
||||||
let events = this.props.events.map((event, key) => {
|
|
||||||
return <Event event={event} key={key} client={this.props.client} room={this.props.room} onReplyClick={this.props.onReplyClick}/>
|
|
||||||
})
|
|
||||||
return <div className="eventGroup">
|
|
||||||
{this.state.avatar}
|
|
||||||
<div className="col">
|
|
||||||
<User user={this.state.user}/>
|
|
||||||
{events}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = chat
|
|
||||||
|
@ -1,115 +1,114 @@
|
|||||||
'use strict'
|
'use strict';
|
||||||
const React = require('react')
|
const React = require('react');
|
||||||
const ReactDOM = require('react-dom')
|
const create = require('create-react-class');
|
||||||
const create = require('create-react-class')
|
const defaultValue = require('default-value');
|
||||||
const defaultValue = require('default-value')
|
|
||||||
|
|
||||||
const riot = require('../../lib/riot-utils.js')
|
const riot = require('../../lib/riot-utils.js');
|
||||||
|
|
||||||
const User = require('./user.js')
|
const User = require('./user.js');
|
||||||
const stateElement = require('./state.js')
|
const stateElement = require('./state.js');
|
||||||
|
|
||||||
const elements = {
|
const elements = {
|
||||||
"m.text": require('./text.js'),
|
"m.text": require('./text.js'),
|
||||||
"m.image": require('./image.js'),
|
"m.image": require('./image.js'),
|
||||||
"m.video": require('./video.js')
|
"m.video": require('./video.js')
|
||||||
}
|
};
|
||||||
|
|
||||||
const mxReplyRegex = /^<mx-reply>[\s\S]+<\/mx-reply>/
|
const mxReplyRegex = /^<mx-reply>[\s\S]+<\/mx-reply>/;
|
||||||
|
|
||||||
let Event = create({
|
let Event = create({
|
||||||
displayName: "Event",
|
displayName: "Event",
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
let event = this.props.event
|
let event = this.props.event;
|
||||||
let state = ""
|
let state = "";
|
||||||
let reply = ""
|
let reply = "";
|
||||||
let element = "unsupported event: " + event.type
|
let element = "unsupported event: " + event.type;
|
||||||
|
|
||||||
if (event.local) {
|
if (event.local) {
|
||||||
state = " local"
|
state = " local";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type == "m.room.message") {
|
if (event.type == "m.room.message") {
|
||||||
let msgtype = event.content.msgtype;
|
let msgtype = event.content.msgtype;
|
||||||
let formattedEvent = parseEvent(event)
|
let formattedEvent = parseEvent(event); /* FIXME: Specify context */
|
||||||
|
|
||||||
let parsedReply = formattedEvent.parsedReply
|
let parsedReply = formattedEvent.parsedReply;
|
||||||
if (parsedReply.isReply) {
|
if (parsedReply.isReply) {
|
||||||
let repliedEvent = this.props.room.findEventById(parsedReply.to)
|
let repliedEvent = this.props.room.findEventById(parsedReply.to);
|
||||||
let shortText, repliedUser
|
let shortText, repliedUser;
|
||||||
if (repliedEvent == undefined) {
|
if (repliedEvent == undefined) {
|
||||||
shortText = "Can't load this event"
|
shortText = "Can't load this event";
|
||||||
repliedUser = {userId: "NEO_UNKNOWN", displayName: "Unknown User"}
|
repliedUser = {userId: "NEO_UNKNOWN", displayName: "Unknown User"};
|
||||||
// fall back on <mx-reply> content?
|
// fall back on <mx-reply> content?
|
||||||
} else {
|
} else {
|
||||||
repliedUser = this.props.client.getUser(repliedEvent.event.sender)
|
repliedUser = this.props.client.getUser(repliedEvent.event.sender);
|
||||||
shortText = parseEvent(repliedEvent.event)
|
shortText = parseEvent(repliedEvent.event); /* FIXME: Specify context */
|
||||||
if (shortText.html) {
|
if (shortText.html) {
|
||||||
shortText = <span dangerouslySetInnerHTML={{__html: shortText.body}}/>
|
shortText = <span dangerouslySetInnerHTML={{__html: shortText.body}}/>;
|
||||||
} else {
|
} else {
|
||||||
shortText = shortText.body
|
shortText = shortText.body;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reply = (
|
reply = (
|
||||||
<div className="reply">
|
<div className="reply">
|
||||||
<User user={repliedUser}/>
|
<User user={repliedUser}/>
|
||||||
{shortText}
|
{shortText}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
element = React.createElement(defaultValue(elements[msgtype], elements["m.text"]), {formattedEvent: formattedEvent, event: event, client: this.props.client})
|
element = React.createElement(defaultValue(elements[msgtype], elements["m.text"]), {formattedEvent: formattedEvent, event: event, client: this.props.client});
|
||||||
} else if (["m.room.name", "m.room.member", "m.room.avatar"].includes(event.type)) {
|
} else if (["m.room.name", "m.room.member", "m.room.avatar"].includes(event.type)) {
|
||||||
element = React.createElement(stateElement, {event: event})
|
element = React.createElement(stateElement, {event: event});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"event" + state} onClick={() => {
|
<div className={"event" + state} onClick={() => {
|
||||||
this.props.onReplyClick(event)
|
this.props.onReplyClick(event);
|
||||||
console.log(event)
|
console.log(event);
|
||||||
}}>
|
}}>
|
||||||
{reply}
|
{reply}
|
||||||
{element}
|
{element}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
function parseEvent(event, context) {
|
function parseEvent(event, _context) {
|
||||||
// context can be either 'main' or 'reply'
|
// context can be either 'main' or 'reply'
|
||||||
let body = event.content.body
|
let body = event.content.body;
|
||||||
let html = false
|
let html = false;
|
||||||
|
|
||||||
if (event.content.format == "org.matrix.custom.html") {
|
if (event.content.format == "org.matrix.custom.html") {
|
||||||
body = riot.sanitize(event.content.formatted_body)
|
body = riot.sanitize(event.content.formatted_body);
|
||||||
html = true
|
html = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body) {
|
if (body) {
|
||||||
body = body.trim()
|
body = body.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsedReply = parseReply(event, body)
|
let parsedReply = parseReply(event, body);
|
||||||
if (parsedReply.isReply) {
|
if (parsedReply.isReply) {
|
||||||
// body with fallback stripped
|
// body with fallback stripped
|
||||||
body = parsedReply.body
|
body = parsedReply.body;
|
||||||
}
|
}
|
||||||
return {body: body, parsedReply: parsedReply, html: html}
|
return {body: body, parsedReply: parsedReply, html: html};
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseReply(event, body) {
|
function parseReply(event, body) {
|
||||||
let replyTo
|
let replyTo;
|
||||||
try {
|
try {
|
||||||
replyTo = event.content['m.relates_to']['m.in_reply_to'].event_id
|
replyTo = event.content['m.relates_to']['m.in_reply_to'].event_id;
|
||||||
if (replyTo) {
|
if (replyTo) {
|
||||||
// strip <mx-reply> from message if it exists
|
// strip <mx-reply> from message if it exists
|
||||||
body = body.replace(mxReplyRegex, "")
|
body = body.replace(mxReplyRegex, "");
|
||||||
}
|
}
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
// no reply
|
// no reply
|
||||||
return {isReply: false}
|
return {isReply: false};
|
||||||
}
|
}
|
||||||
return {isReply: true, body: body, to: replyTo}
|
return {isReply: true, body: body, to: replyTo};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Event
|
module.exports = Event;
|
||||||
|
@ -1,44 +1,35 @@
|
|||||||
'use strict'
|
'use strict';
|
||||||
const React = require('react')
|
const React = require('react');
|
||||||
const ReactDOM = require('react-dom')
|
const create = require('create-react-class');
|
||||||
const create = require('create-react-class')
|
|
||||||
const Promise = require('bluebird')
|
|
||||||
const defaultValue = require('default-value')
|
|
||||||
|
|
||||||
const mediaLib = require('../../lib/media.js')
|
const mediaLib = require('../../lib/media.js');
|
||||||
|
|
||||||
const Text = require('./text.js')
|
|
||||||
|
|
||||||
let Event = create({
|
let Event = create({
|
||||||
displayName: "m.image",
|
displayName: "m.image",
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
let event = this.props.event
|
let event = this.props.event;
|
||||||
if (event.content.url == undefined) {
|
if (event.content.url == undefined) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
return mediaLib.parseEvent(this.props.client, event, 1000, 1000)
|
return mediaLib.parseEvent(this.props.client, event, 1000, 1000);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateSize: function(e) {
|
render: function() {
|
||||||
console.log("image was loaded")
|
let event = this.props.event;
|
||||||
},
|
|
||||||
|
if (this.state == null) {
|
||||||
render: function() {
|
return "malformed image event: " + event.content.body;
|
||||||
let event = this.props.event
|
}
|
||||||
|
|
||||||
if (this.state == null) {
|
return (
|
||||||
return "malformed image event: " + event.content.body
|
<div className="body">
|
||||||
}
|
<a href={this.state.full} target="_blank">
|
||||||
|
<img src={this.state.thumb} style={{maxHeight: this.state.size.h, maxWidth: this.state.size.w}}/>
|
||||||
return (
|
</a>
|
||||||
<div className="body">
|
</div>
|
||||||
<a href={this.state.full} target="_blank">
|
);
|
||||||
<img src={this.state.thumb} style={{maxHeight: this.state.size.h, maxWidth: this.state.size.w}}/>
|
}
|
||||||
</a>
|
});
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = Event;
|
module.exports = Event;
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
'use strict'
|
'use strict';
|
||||||
const React = require('react')
|
const React = require('react');
|
||||||
const ReactDOM = require('react-dom')
|
const create = require('create-react-class');
|
||||||
const create = require('create-react-class')
|
|
||||||
|
|
||||||
let Event = create({
|
let Event = create({
|
||||||
displayName: "genericStateEvent",
|
displayName: "genericStateEvent",
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
let event = this.props.event
|
let event = this.props.event;
|
||||||
return (
|
return (
|
||||||
<div className="body">
|
<div className="body">
|
||||||
{event.plaintext()}
|
{event.plaintext()}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
module.exports = Event
|
module.exports = Event;
|
||||||
|
@ -1,43 +1,39 @@
|
|||||||
'use strict'
|
'use strict';
|
||||||
const React = require('react')
|
const React = require('react');
|
||||||
const ReactDOM = require('react-dom')
|
const create = require('create-react-class');
|
||||||
const create = require('create-react-class')
|
|
||||||
const Promise = require('bluebird')
|
|
||||||
|
|
||||||
const riot = require('../../lib/riot-utils.js')
|
|
||||||
|
|
||||||
let Event = create({
|
let Event = create({
|
||||||
displayName: "m.text",
|
displayName: "m.text",
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
let event = this.props.event
|
let event = this.props.event;
|
||||||
let formattedEvent = this.props.formattedEvent
|
let formattedEvent = this.props.formattedEvent;
|
||||||
|
|
||||||
let eventBody
|
let eventBody;
|
||||||
|
|
||||||
if (formattedEvent.html) {
|
if (formattedEvent.html) {
|
||||||
eventBody = <div
|
eventBody = <div
|
||||||
className="body"
|
className="body"
|
||||||
dangerouslySetInnerHTML={{__html: formattedEvent.body}}
|
dangerouslySetInnerHTML={{__html: formattedEvent.body}}
|
||||||
/>
|
/>;
|
||||||
} else {
|
} else {
|
||||||
eventBody =
|
eventBody =
|
||||||
<div className="body">
|
<div className="body">
|
||||||
{formattedEvent.body}
|
{formattedEvent.body}
|
||||||
</div>
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let eventClass = ""
|
let eventClass = "";
|
||||||
if (event.local) {
|
if (event.local) {
|
||||||
eventClass += " local"
|
eventClass += " local";
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={eventClass}>
|
return <div className={eventClass}>
|
||||||
{eventBody}
|
{eventBody}
|
||||||
</div>
|
</div>;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
||||||
module.exports = Event
|
module.exports = Event;
|
||||||
|
@ -1,49 +1,27 @@
|
|||||||
'use strict'
|
"use strict";
|
||||||
const React = require('react')
|
|
||||||
const ReactDOM = require('react-dom')
|
|
||||||
const create = require('create-react-class')
|
|
||||||
const jdenticon = require('jdenticon')
|
|
||||||
|
|
||||||
jdenticon.config = {
|
const React = require("react");
|
||||||
lightness: {
|
const create = require("create-react-class");
|
||||||
color: [0.58, 0.66],
|
|
||||||
grayscale: [0.30, 0.90]
|
const generateJdenticon = require("../../lib/generate-jdenticon");
|
||||||
},
|
|
||||||
saturation: {
|
|
||||||
color: 0.66,
|
|
||||||
grayscale: 0.00
|
|
||||||
},
|
|
||||||
backColor: "#00000000"
|
|
||||||
};
|
|
||||||
|
|
||||||
let User = create({
|
let User = create({
|
||||||
displayName: "user",
|
displayName: "user",
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
let icon = jdenticon.toSvg(this.props.user.userId, 200)
|
return {
|
||||||
let match = icon.match(/#([a-f0-9]{6})/g)
|
/* FIXME: Cache this to speed it up */
|
||||||
let color = '#ff0000'
|
color: generateJdenticon(this.props.user.userId).primaryColor()
|
||||||
for(let i=match.length-1; i>= 0; i--) {
|
};
|
||||||
color = match[i]
|
},
|
||||||
let r = color.substr(1, 2)
|
|
||||||
let g = color.substr(3, 2)
|
|
||||||
let b = color.substr(5, 2)
|
|
||||||
if (r != g && g != b) { // not greyscale
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
color: color
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<div className="user" style={{color: this.state.color}}>
|
<div className="user" style={{color: this.state.color}}>
|
||||||
{this.props.user.displayName}
|
{this.props.user.displayName}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
module.exports = User
|
module.exports = User;
|
||||||
|
@ -1,40 +1,35 @@
|
|||||||
'use strict'
|
'use strict';
|
||||||
const React = require('react')
|
const React = require('react');
|
||||||
const ReactDOM = require('react-dom')
|
const create = require('create-react-class');
|
||||||
const create = require('create-react-class')
|
|
||||||
const Promise = require('bluebird')
|
|
||||||
const defaultValue = require('default-value')
|
|
||||||
|
|
||||||
const mediaLib = require('../../lib/media.js')
|
const mediaLib = require('../../lib/media.js');
|
||||||
|
|
||||||
const Text = require('./text.js')
|
|
||||||
|
|
||||||
let Event = create({
|
let Event = create({
|
||||||
displayName: "m.video",
|
displayName: "m.video",
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
let event = this.props.event
|
let event = this.props.event;
|
||||||
if (event.content.url == undefined) {
|
if (event.content.url == undefined) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
return mediaLib.parseEvent(this.props.client, event, 1000, 1000)
|
return mediaLib.parseEvent(this.props.client, event, 1000, 1000);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
let event = this.props.event
|
let event = this.props.event;
|
||||||
|
|
||||||
if (this.state == null) {
|
if (this.state == null) {
|
||||||
return "malformed video event: " + event.content.body
|
return "malformed video event: " + event.content.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="body">
|
<div className="body">
|
||||||
<video controls poster={this.state.thumb} style={{maxHeight: this.state.size.h, maxWidth: this.state.size.w}}>
|
<video controls poster={this.state.thumb} style={{maxHeight: this.state.size.h, maxWidth: this.state.size.w}}>
|
||||||
<source src={this.state.full}></source>
|
<source src={this.state.full}></source>
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
module.exports = Event;
|
module.exports = Event;
|
||||||
|
@ -1,46 +1,44 @@
|
|||||||
'use strict'
|
'use strict';
|
||||||
const React = require('react')
|
|
||||||
const ReactDOM = require('react-dom')
|
|
||||||
const create = require('create-react-class')
|
|
||||||
|
|
||||||
let fileUpload = create({
|
const Promise = require("bluebird");
|
||||||
displayName: "FileUpload",
|
const React = require('react');
|
||||||
|
|
||||||
setFileRef: function(e) {
|
const withElement = require("../lib/with-element");
|
||||||
if (e != null) {
|
const fileToDataUrl = require("../lib/file-to-data-url");
|
||||||
e.addEventListener('change', this.startUpload)
|
|
||||||
this.setState({
|
|
||||||
fileRef: e
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
startUpload: function(e) {
|
module.exports = function FileUpload({ addUpload }) {
|
||||||
Array.from(e.target.files).forEach((file) => {
|
function handleChange(event) {
|
||||||
if (file.type.startsWith("image/")) {
|
return Promise.map(Array.from(event.target.files), (file) => {
|
||||||
let reader = new FileReader()
|
if (file.type.startsWith("image/")) {
|
||||||
reader.onloadend = () => {
|
return Promise.try(() => {
|
||||||
let fileObject = {
|
return fileToDataUrl(file);
|
||||||
file: file,
|
}).then((url) => {
|
||||||
preview: reader.result
|
return addUpload({
|
||||||
}
|
file: file,
|
||||||
this.props.addUpload(fileObject)
|
preview: url
|
||||||
}
|
});
|
||||||
reader.readAsDataURL(file)
|
});
|
||||||
} else {
|
} else {
|
||||||
this.props.addUpload({file: file, preview: "/icons/file.svg"})
|
return addUpload({
|
||||||
}
|
file: file,
|
||||||
})
|
preview: "/icons/file.svg"
|
||||||
},
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render: function() {
|
let setFileRef = withElement((element) => {
|
||||||
return (
|
element.addEventListener("change", handleChange);
|
||||||
<div className="fileUpload">
|
|
||||||
<input type="file" id="fileUpload" multiple ref={this.setFileRef} />
|
|
||||||
<label htmlFor="fileUpload"><img src="/icons/file.svg"/></label>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = fileUpload
|
return function() {
|
||||||
|
element.removeEventListener("change", handleChange);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fileUpload">
|
||||||
|
<input type="file" id="fileUpload" multiple ref={setFileRef} />
|
||||||
|
<label htmlFor="fileUpload"><img src="/icons/file.svg"/></label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -1,64 +1,62 @@
|
|||||||
'use strict'
|
'use strict';
|
||||||
const React = require('react')
|
const React = require('react');
|
||||||
const ReactDOM = require('react-dom')
|
const create = require('create-react-class');
|
||||||
const create = require('create-react-class')
|
const debounce = require('debounce');
|
||||||
const Promise = require('bluebird')
|
|
||||||
const debounce = require('debounce')
|
|
||||||
|
|
||||||
let FilterList = create({
|
let FilterList = create({
|
||||||
displayName: "FilterList",
|
displayName: "FilterList",
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
selection: "room0",
|
selection: "room0",
|
||||||
filter: ""
|
filter: ""
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
select: function(id) {
|
select: function(id) {
|
||||||
this.setState({selection: id, filter: ""})
|
this.setState({selection: id, filter: ""});
|
||||||
this.state.inputRef.value = ""
|
this.state.inputRef.value = "";
|
||||||
this.props.callback(id)
|
this.props.callback(id);
|
||||||
},
|
},
|
||||||
|
|
||||||
inputRef: function(ref) {
|
inputRef: function(ref) {
|
||||||
if (ref == null) {
|
if (ref == null) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
inputRef: ref
|
inputRef: ref
|
||||||
})
|
});
|
||||||
ref.addEventListener("keyup", debounce(this.input, 20))
|
ref.addEventListener("keyup", debounce(this.input, 20));
|
||||||
},
|
},
|
||||||
|
|
||||||
input: function(e) {
|
input: function(e) {
|
||||||
this.setState({
|
this.setState({
|
||||||
filter: e.target.value.toUpperCase()
|
filter: e.target.value.toUpperCase()
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
let keys = Object.keys(this.props.items)
|
let keys = Object.keys(this.props.items);
|
||||||
let items = keys.map((itemKey, id) => {
|
let items = keys.map((itemKey) => {
|
||||||
let item = this.props.items[itemKey]
|
let item = this.props.items[itemKey];
|
||||||
let props = {
|
let props = {
|
||||||
selected: this.state.selection == itemKey,
|
selected: this.state.selection == itemKey,
|
||||||
filter: this.state.filter,
|
filter: this.state.filter,
|
||||||
content: item,
|
content: item,
|
||||||
key: itemKey,
|
key: itemKey,
|
||||||
listId: itemKey,
|
listId: itemKey,
|
||||||
select: this.select,
|
select: this.select,
|
||||||
properties: this.props.properties
|
properties: this.props.properties
|
||||||
}
|
};
|
||||||
return React.createElement(this.props.element, props)
|
return React.createElement(this.props.element, props);
|
||||||
})
|
});
|
||||||
return <>
|
return <>
|
||||||
<input className="filter" ref={this.inputRef} placeholder="Search"/>
|
<input className="filter" ref={this.inputRef} placeholder="Search"/>
|
||||||
<div className="list">
|
<div className="list">
|
||||||
{items}
|
{items}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
module.exports = FilterList
|
module.exports = FilterList;
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
'use strict'
|
'use strict';
|
||||||
const React = require('react')
|
const React = require('react');
|
||||||
const ReactDOM = require('react-dom')
|
const create = require('create-react-class');
|
||||||
const create = require('create-react-class')
|
|
||||||
const Promise = require('bluebird')
|
|
||||||
|
|
||||||
let info = create({
|
let info = create({
|
||||||
displayName: "Info",
|
displayName: "Info",
|
||||||
render: function() {
|
render: function() {
|
||||||
let title = ""
|
let title = "";
|
||||||
if (this.props.room != undefined) {
|
if (this.props.room != undefined) {
|
||||||
title = this.props.room.name
|
title = this.props.room.name;
|
||||||
}
|
}
|
||||||
return <div className="info">{title}</div>
|
return <div className="info">{title}</div>;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
||||||
module.exports = info
|
module.exports = info;
|
||||||
|
@ -1,277 +1,281 @@
|
|||||||
'use strict'
|
'use strict';
|
||||||
const React = require('react')
|
const React = require('react');
|
||||||
const ReactDOM = require('react-dom')
|
const create = require('create-react-class');
|
||||||
const create = require('create-react-class')
|
const Promise = require('bluebird');
|
||||||
const Promise = require('bluebird')
|
const colorConvert = require('color-convert');
|
||||||
const colorConvert = require('color-convert')
|
const sanitize = require('sanitize-html');
|
||||||
const sanitize = require('sanitize-html')
|
|
||||||
|
|
||||||
const riot = require('../lib/riot-utils.js')
|
const riot = require('../lib/riot-utils.js');
|
||||||
const FileUpload = require('./fileUpload.js')
|
const FileUpload = require('./fileUpload.js');
|
||||||
|
|
||||||
let input = create({
|
let input = create({
|
||||||
displayName: "Input",
|
displayName: "Input",
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
uploads: []
|
uploads: []
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
setRef: function(ref) {
|
setRef: function(ref) {
|
||||||
if (ref !=null) {
|
if (ref !=null) {
|
||||||
ref.addEventListener("keydown", (e) => {
|
ref.addEventListener("keydown", (e) => {
|
||||||
// only send on plain 'enter'
|
// only send on plain 'enter'
|
||||||
if (e.key == "Enter") {
|
if (e.key == "Enter") {
|
||||||
if (!e.shiftKey && !e.altKey && !e.ctrlKey) {
|
if (!e.shiftKey && !e.altKey && !e.ctrlKey) {
|
||||||
this.send(e)
|
this.send(e);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
ref.addEventListener('change', this.resize_textarea)
|
ref.addEventListener('change', this.resize_textarea);
|
||||||
ref.addEventListener('cut', this.resize_textarea_delayed)
|
ref.addEventListener('cut', this.resize_textarea_delayed);
|
||||||
ref.addEventListener('paste', this.resize_textarea_delayed)
|
ref.addEventListener('paste', this.resize_textarea_delayed);
|
||||||
ref.addEventListener('drop', this.resize_textarea_delayed)
|
ref.addEventListener('drop', this.resize_textarea_delayed);
|
||||||
ref.addEventListener('keydown', this.resize_textarea_delayed)
|
ref.addEventListener('keydown', this.resize_textarea_delayed);
|
||||||
this.setState({ref: ref})
|
this.setState({ref: ref});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addUpload: function(upload) {
|
addUpload: function(upload) {
|
||||||
let uploads = this.state.uploads
|
let uploads = this.state.uploads;
|
||||||
uploads.push(upload)
|
uploads.push(upload);
|
||||||
this.setState({uploads: uploads})
|
this.setState({uploads: uploads});
|
||||||
},
|
},
|
||||||
|
|
||||||
removeUpload: function(index) {
|
removeUpload: function(index) {
|
||||||
let uploads = this.state.uploads
|
let uploads = this.state.uploads;
|
||||||
uploads.splice(index, 1)
|
uploads.splice(index, 1);
|
||||||
this.setState({uploads: uploads})
|
this.setState({uploads: uploads});
|
||||||
},
|
},
|
||||||
|
|
||||||
resize_textarea: function(element) {
|
resize_textarea: function(element) {
|
||||||
if (element == undefined) {
|
if (element == undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let ref = element.target;
|
let ref = element.target;
|
||||||
if (ref != undefined) {
|
if (ref != undefined) {
|
||||||
ref.style.height = 'auto';
|
ref.style.height = 'auto';
|
||||||
ref.style.height = ref.scrollHeight+'px';
|
ref.style.height = ref.scrollHeight+'px';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
resize_textarea_delayed: function(e) {
|
resize_textarea_delayed: function(e) {
|
||||||
setTimeout(() => this.resize_textarea(e), 5);
|
setTimeout(() => this.resize_textarea(e), 5);
|
||||||
},
|
},
|
||||||
|
|
||||||
send: function(e) {
|
send: function(e) {
|
||||||
let msg = e.target.value
|
let msg = e.target.value;
|
||||||
if (msg.trim().length != 0) {
|
if (msg.trim().length != 0) {
|
||||||
//TODO: parse markdown (commonmark?)
|
//TODO: parse markdown (commonmark?)
|
||||||
if (msg.startsWith('/')) {
|
if (msg.startsWith('/')) {
|
||||||
// Handle other commands
|
// Handle other commands
|
||||||
let parts = msg.split(' ')
|
let parts = msg.split(' ');
|
||||||
let command = parts[0]
|
let command = parts[0];
|
||||||
let result = handleCommands(command, parts)
|
let result = handleCommands(command, parts);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
if (result.type == "html") {
|
if (result.type == "html") {
|
||||||
this.sendHTML(result.content)
|
this.sendHTML(result.content);
|
||||||
} else {
|
} else {
|
||||||
this.sendPlain(result.content)
|
this.sendPlain(result.content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.sendPlain(msg)
|
this.sendPlain(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.uploads.length > 0) {
|
if (this.state.uploads.length > 0) {
|
||||||
this.uploadFiles(this.state.uploads)
|
this.uploadFiles(this.state.uploads);
|
||||||
this.setState({uploads: []})
|
this.setState({uploads: []});
|
||||||
}
|
}
|
||||||
e.target.value = ""
|
e.target.value = "";
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
this.resize_textarea_delayed(e)
|
this.resize_textarea_delayed(e);
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadFiles: function(uploads) {
|
uploadFiles: function(uploads) {
|
||||||
let client = this.props.client
|
let client = this.props.client;
|
||||||
Promise.map(uploads, (upload) => {
|
|
||||||
let fileUploadPromise = client.uploadContent(upload.file,
|
|
||||||
{onlyContentUri: false}).then((response) => {
|
|
||||||
return response.content_uri
|
|
||||||
})
|
|
||||||
|
|
||||||
let mimeType = upload.file.type
|
return Promise.map(uploads, (upload) => {
|
||||||
let eventType = "m.file"
|
let fileUploadPromise = client.uploadContent(upload.file,
|
||||||
let additionalPromise
|
{onlyContentUri: false}).then((response) => {
|
||||||
if (mimeType.startsWith("image/") || mimeType.startsWith("video/")) {
|
return response.content_uri;
|
||||||
function elementToThumbnail(element) {
|
});
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
riot.createThumbnail(element,
|
|
||||||
element.width,
|
|
||||||
element.height,
|
|
||||||
thumbnailType
|
|
||||||
)
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("neo: error getting thumbnail", error)
|
|
||||||
reject()
|
|
||||||
})
|
|
||||||
.then((thumbResult) => {
|
|
||||||
return client.uploadContent(thumbResult.thumbnail, {onlyContentUri: false})
|
|
||||||
}).then((response) => {
|
|
||||||
return resolve({
|
|
||||||
thumbnail_url: response.content_uri,
|
|
||||||
thumbnail_info: {
|
|
||||||
mimetype: thumbnailType
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
let mimeType = upload.file.type;
|
||||||
}
|
let eventType = "m.file";
|
||||||
if (mimeType.startsWith("image/")) {
|
let additionalPromise;
|
||||||
eventType = "m.image"
|
if (mimeType.startsWith("image/") || mimeType.startsWith("video/")) {
|
||||||
additionalPromise = riot.loadImageElement(upload.file)
|
function elementToThumbnail(element) {
|
||||||
.then((element) => {return elementToThumbnail(element)})
|
return Promise.try(() => {
|
||||||
} else if (mimeType.startsWith("video/")) {
|
return riot.createThumbnail(element,
|
||||||
eventType = "m.video"
|
element.width,
|
||||||
additionalPromise = riot.loadVideoElement(upload.file)
|
element.height,
|
||||||
.then((element) => {return elementToThumbnail(element)})
|
thumbnailType
|
||||||
}
|
);
|
||||||
// create and upload thumbnail
|
}).catch((error) => {
|
||||||
let thumbnailType = "image/png"
|
console.error("neo: error getting thumbnail", error);
|
||||||
if (mimeType == "image/jpeg") {
|
|
||||||
thumbnailType = mimeType
|
|
||||||
}
|
|
||||||
} else if (mimeType.startsWith("audio/")) {
|
|
||||||
eventType = "m.audio"
|
|
||||||
} else {
|
|
||||||
// m.file
|
|
||||||
}
|
|
||||||
Promise.all([fileUploadPromise, additionalPromise]).then((result) => {
|
|
||||||
console.log(result)
|
|
||||||
let info = {
|
|
||||||
mimetype: mimeType
|
|
||||||
}
|
|
||||||
if (result[1] != undefined) {
|
|
||||||
info = Object.assign(info, result[1])
|
|
||||||
}
|
|
||||||
client.sendEvent(this.props.roomId, "m.room.message", {
|
|
||||||
body: upload.file.name,
|
|
||||||
msgtype: eventType,
|
|
||||||
info: info,
|
|
||||||
url: result[0]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
sendPlain: function(string) {
|
throw error;
|
||||||
let content = {
|
}).then((thumbResult) => {
|
||||||
body: string,
|
return client.uploadContent(thumbResult.thumbnail, {onlyContentUri: false});
|
||||||
msgtype: "m.text"
|
}).then((response) => {
|
||||||
}
|
return {
|
||||||
content = this.sendReply(content)
|
thumbnail_url: response.content_uri,
|
||||||
this.props.client.sendEvent(this.props.roomId, "m.room.message", content, (err, res) => {
|
thumbnail_info: {
|
||||||
if (err != null) {
|
mimetype: thumbnailType
|
||||||
console.log(err)
|
}
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
},
|
}
|
||||||
|
if (mimeType.startsWith("image/")) {
|
||||||
|
eventType = "m.image";
|
||||||
|
additionalPromise = riot.loadImageElement(upload.file)
|
||||||
|
.then((element) => {return elementToThumbnail(element);});
|
||||||
|
} else if (mimeType.startsWith("video/")) {
|
||||||
|
eventType = "m.video";
|
||||||
|
additionalPromise = riot.loadVideoElement(upload.file)
|
||||||
|
.then((element) => {return elementToThumbnail(element);});
|
||||||
|
}
|
||||||
|
// create and upload thumbnail
|
||||||
|
let thumbnailType = "image/png";
|
||||||
|
|
||||||
sendHTML: function(html) {
|
if (mimeType == "image/jpeg") {
|
||||||
let content = {
|
thumbnailType = mimeType;
|
||||||
body: sanitize(html, {allowedTags: []}),
|
}
|
||||||
formatted_body: html,
|
} else if (mimeType.startsWith("audio/")) {
|
||||||
format: "org.matrix.custom.html",
|
eventType = "m.audio";
|
||||||
msgtype: "m.text"
|
} else {
|
||||||
}
|
// m.file
|
||||||
|
}
|
||||||
|
|
||||||
content = this.sendReply(content)
|
return Promise.all([fileUploadPromise, additionalPromise]).then((result) => {
|
||||||
|
console.log(result);
|
||||||
|
let info = {
|
||||||
|
mimetype: mimeType
|
||||||
|
};
|
||||||
|
|
||||||
this.props.client.sendEvent(this.props.roomId, "m.room.message", content, (err, res) => {
|
if (result[1] != undefined) {
|
||||||
console.log(err)
|
info = Object.assign(info, result[1]);
|
||||||
})
|
}
|
||||||
},
|
|
||||||
|
|
||||||
sendReply: function(content) {
|
return client.sendEvent(this.props.roomId, "m.room.message", {
|
||||||
if (this.props.replyEvent != undefined) {
|
body: upload.file.name,
|
||||||
content['m.relates_to'] = {
|
msgtype: eventType,
|
||||||
'm.in_reply_to': {
|
info: info,
|
||||||
event_id: this.props.replyEvent.event_id
|
url: result[0]
|
||||||
}
|
});
|
||||||
}
|
});
|
||||||
this.props.onReplyClick()
|
});
|
||||||
}
|
},
|
||||||
return content
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
sendPlain: function(string) {
|
||||||
return <div className="input">
|
let content = {
|
||||||
{this.props.replyEvent &&
|
body: string,
|
||||||
<div className="replyEvent" onClick={() => this.props.onReplyClick()}>
|
msgtype: "m.text"
|
||||||
{this.props.replyEvent.plaintext()}
|
};
|
||||||
</div>
|
content = this.sendReply(content);
|
||||||
}
|
/* FIXME: Promisify */
|
||||||
{this.state.uploads.length > 0 &&
|
this.props.client.sendEvent(this.props.roomId, "m.room.message", content, (err, _res) => {
|
||||||
<div className="imgPreview">
|
if (err != null) {
|
||||||
{this.state.uploads.map((upload, key) => {
|
console.log(err);
|
||||||
return (
|
}
|
||||||
<div key={key}>
|
});
|
||||||
<img src={upload.preview}/>
|
},
|
||||||
<span onClick={() => this.removeUpload(key)}>X</span>
|
|
||||||
</div>
|
sendHTML: function(html) {
|
||||||
)
|
let content = {
|
||||||
})}
|
body: sanitize(html, {allowedTags: []}),
|
||||||
</div>
|
formatted_body: html,
|
||||||
}
|
format: "org.matrix.custom.html",
|
||||||
<div className="content">
|
msgtype: "m.text"
|
||||||
<textarea ref={this.setRef} rows="1" spellCheck="false" placeholder="unencrypted message"></textarea>
|
};
|
||||||
<FileUpload addUpload={this.addUpload}/>
|
|
||||||
</div>
|
content = this.sendReply(content);
|
||||||
</div>
|
|
||||||
}
|
/* FIXME: Promisify */
|
||||||
})
|
this.props.client.sendEvent(this.props.roomId, "m.room.message", content, (err, _res) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
sendReply: function(content) {
|
||||||
|
if (this.props.replyEvent != undefined) {
|
||||||
|
content['m.relates_to'] = {
|
||||||
|
'm.in_reply_to': {
|
||||||
|
event_id: this.props.replyEvent.event_id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.props.onReplyClick();
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return <div className="input">
|
||||||
|
{this.props.replyEvent &&
|
||||||
|
<div className="replyEvent" onClick={() => this.props.onReplyClick()}>
|
||||||
|
{this.props.replyEvent.plaintext()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{this.state.uploads.length > 0 &&
|
||||||
|
<div className="imgPreview">
|
||||||
|
{this.state.uploads.map((upload, key) => {
|
||||||
|
return (
|
||||||
|
<div key={key}>
|
||||||
|
<img src={upload.preview}/>
|
||||||
|
<span onClick={() => this.removeUpload(key)}>X</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div className="content">
|
||||||
|
<textarea ref={this.setRef} rows="1" spellCheck="false" placeholder="unencrypted message"></textarea>
|
||||||
|
<FileUpload addUpload={this.addUpload}/>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function handleCommands(command, parts) {
|
function handleCommands(command, parts) {
|
||||||
if (command == "/rainbow") {
|
if (command == "/rainbow") {
|
||||||
if (parts.length < 2) {
|
if (parts.length < 2) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
let string = parts[1]
|
let string = parts[1];
|
||||||
for(let i=2; i < parts.length; i++) {
|
for(let i=2; i < parts.length; i++) {
|
||||||
string += " " + parts[i]
|
string += " " + parts[i];
|
||||||
}
|
}
|
||||||
let html = rainbowTransform(string)
|
let html = rainbowTransform(string);
|
||||||
return {
|
return {
|
||||||
type: 'html',
|
type: 'html',
|
||||||
content: html
|
content: html
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function rainbowTransform(text) {
|
function rainbowTransform(text) {
|
||||||
let array = text.split("");
|
let array = text.split("");
|
||||||
let delta = 360/text.length;
|
let delta = 360/text.length;
|
||||||
if (delta < 10) {
|
if (delta < 10) {
|
||||||
delta = 10;
|
delta = 10;
|
||||||
} else if (delta > 20) {
|
} else if (delta > 20) {
|
||||||
delta = 20;
|
delta = 20;
|
||||||
}
|
}
|
||||||
let h = -1 * delta; // start at beginning
|
let h = -1 * delta; // start at beginning
|
||||||
|
|
||||||
let rainbowArray = array.map((char) => {
|
let rainbowArray = array.map((char) => {
|
||||||
h = h + delta;
|
h = h + delta;
|
||||||
if (h > 360) {
|
if (h > 360) {
|
||||||
h = 0;
|
h = 0;
|
||||||
}
|
}
|
||||||
return `<font color="${colorConvert.hsl.hex(h, 100, 50)}">${char}</font>`;
|
return `<font color="${colorConvert.hsl.hex(h, 100, 50)}">${char}</font>`;
|
||||||
});
|
});
|
||||||
let rainbow = rainbowArray.join("");
|
let rainbow = rainbowArray.join("");
|
||||||
return rainbow;
|
return rainbow;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = input
|
module.exports = input;
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
'use strict'
|
'use strict';
|
||||||
const React = require('react')
|
const React = require('react');
|
||||||
const ReactDOM = require('react-dom')
|
const create = require('create-react-class');
|
||||||
const create = require('create-react-class')
|
|
||||||
|
|
||||||
let Loading = create({
|
let Loading = create({
|
||||||
displayName: "Loading",
|
displayName: "Loading",
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<div className="spinner">
|
<div className="spinner">
|
||||||
<div className="bounce1"/>
|
<div className="bounce1"/>
|
||||||
<div className="bounce2"/>
|
<div className="bounce2"/>
|
||||||
<div className="bounce3"/>
|
<div className="bounce3"/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
module.exports = Loading
|
module.exports = Loading;
|
||||||
|
@ -1,117 +1,114 @@
|
|||||||
'use strict'
|
'use strict';
|
||||||
const React = require('react')
|
const React = require('react');
|
||||||
const ReactDOM = require('react-dom')
|
const create = require('create-react-class');
|
||||||
const create = require('create-react-class')
|
const jdenticon = require('jdenticon');
|
||||||
const Promise = require('bluebird')
|
|
||||||
const debounce = require('debounce')
|
|
||||||
const jdenticon = require('jdenticon')
|
|
||||||
|
|
||||||
const FilterList = require('./filterList.js')
|
const FilterList = require('./filterList.js');
|
||||||
|
|
||||||
let RoomListItem = create({
|
let RoomListItem = create({
|
||||||
displayName: "RoomListItem",
|
displayName: "RoomListItem",
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
let room = this.props.content
|
let room = this.props.content;
|
||||||
let client = this.props.properties.client
|
let client = this.props.properties.client;
|
||||||
let jdenticon = <svg id="avatar" ref={this.jdenticonRef}/>
|
let jdenticon = <svg id="avatar" ref={this.jdenticonRef}/>;
|
||||||
let avatarUrl
|
let avatarUrl;
|
||||||
|
|
||||||
let roomState = room.getLiveTimeline().getState('f')
|
let roomState = room.getLiveTimeline().getState('f');
|
||||||
let avatarState = roomState.getStateEvents('m.room.avatar')
|
let avatarState = roomState.getStateEvents('m.room.avatar');
|
||||||
if (avatarState.length > 0) {
|
if (avatarState.length > 0) {
|
||||||
let event = avatarState[avatarState.length-1].event
|
let event = avatarState[avatarState.length-1].event;
|
||||||
let hs = client.baseUrl
|
let hs = client.baseUrl;
|
||||||
let media_mxc = event.content.url.slice(6)
|
let media_mxc = event.content.url.slice(6);
|
||||||
let path = `/_matrix/media/v1/thumbnail/${media_mxc}?width=128&height=128&method=scale`
|
let path = `/_matrix/media/v1/thumbnail/${media_mxc}?width=128&height=128&method=scale`;
|
||||||
avatarUrl = {
|
avatarUrl = {
|
||||||
hs: hs,
|
hs: hs,
|
||||||
path: path
|
path: path
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filterName: room.name.toUpperCase(),
|
filterName: room.name.toUpperCase(),
|
||||||
unread: Math.random() > 0.7,
|
unread: Math.random() > 0.7,
|
||||||
avatarUrl: avatarUrl,
|
avatarUrl: avatarUrl,
|
||||||
jdenticon: jdenticon,
|
jdenticon: jdenticon,
|
||||||
tries: 0
|
tries: 0
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
jdenticonRef: function(ref) {
|
jdenticonRef: function(ref) {
|
||||||
jdenticon.update(ref, this.props.content.roomId)
|
jdenticon.update(ref, this.props.content.roomId);
|
||||||
},
|
},
|
||||||
|
|
||||||
avatarFallback: function() {
|
avatarFallback: function() {
|
||||||
// instead of falling back on jdenticon immediately, we can try
|
// instead of falling back on jdenticon immediately, we can try
|
||||||
// a third-party homeserver's media repo
|
// a third-party homeserver's media repo
|
||||||
// this does come with trust issues, and is opt-in in settings
|
// this does come with trust issues, and is opt-in in settings
|
||||||
let fallbackMediaRepos = this.props.properties.options.fallbackMediaRepos
|
let fallbackMediaRepos = this.props.properties.options.fallbackMediaRepos;
|
||||||
|
|
||||||
if (this.state.tries < fallbackMediaRepos.length) {
|
if (this.state.tries < fallbackMediaRepos.length) {
|
||||||
let avatarUrl = this.state.avatarUrl
|
let avatarUrl = this.state.avatarUrl;
|
||||||
avatarUrl.hs = fallbackMediaRepos[this.state.tries]
|
avatarUrl.hs = fallbackMediaRepos[this.state.tries];
|
||||||
this.setState({
|
this.setState({
|
||||||
avatarUrl: avatarUrl,
|
avatarUrl: avatarUrl,
|
||||||
tries: this.state.tries + 1
|
tries: this.state.tries + 1
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({avatarUrl: null, avatar: jdenticon})
|
this.setState({avatarUrl: null, avatar: jdenticon});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setRef: function(ref) {
|
setRef: function(ref) {
|
||||||
if (ref == null) {
|
if (ref == null) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
this.setState({ref: ref})
|
this.setState({ref: ref});
|
||||||
ref.addEventListener("click", () => {this.props.select(this.props.listId)})
|
ref.addEventListener("click", () => {this.props.select(this.props.listId);});
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
if (this.state.filterName.indexOf(this.props.filter) == -1) {
|
if (this.state.filterName.indexOf(this.props.filter) == -1) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
let className = "roomListItem"
|
let className = "roomListItem";
|
||||||
if (this.props.selected) {
|
if (this.props.selected) {
|
||||||
className += " active"
|
className += " active";
|
||||||
}
|
}
|
||||||
if (this.state.unread) {
|
if (this.state.unread) {
|
||||||
className += " unread"
|
className += " unread";
|
||||||
}
|
}
|
||||||
return <div className={className} ref={this.setRef}>
|
return <div className={className} ref={this.setRef}>
|
||||||
{this.state.avatarUrl ?
|
{this.state.avatarUrl ?
|
||||||
<img id="avatar" src={`${this.state.avatarUrl.hs}${this.state.avatarUrl.path}`} onError={this.avatarFallback}></img>
|
<img id="avatar" src={`${this.state.avatarUrl.hs}${this.state.avatarUrl.path}`} onError={this.avatarFallback}></img>
|
||||||
:
|
:
|
||||||
this.state.jdenticon
|
this.state.jdenticon
|
||||||
}
|
}
|
||||||
<span id="name">{this.props.content.name}</span>
|
<span id="name">{this.props.content.name}</span>
|
||||||
</div>
|
</div>;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
let Sidebar = create({
|
let Sidebar = create({
|
||||||
displayName: "Sidebar",
|
displayName: "Sidebar",
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
filter: ""
|
filter: ""
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
setFilter: function(filter) {
|
setFilter: function(filter) {
|
||||||
this.setState({
|
this.setState({
|
||||||
filter: filter.toUpperCase()
|
filter: filter.toUpperCase()
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
return <div className="sidebar">
|
return <div className="sidebar">
|
||||||
<FilterList items={this.props.rooms} properties={{client: this.props.client, options: this.props.options}} element={RoomListItem} callback={(roomId) => {this.props.selectRoom(roomId)}}/>
|
<FilterList items={this.props.rooms} properties={{client: this.props.client, options: this.props.options}} element={RoomListItem} callback={(roomId) => {this.props.selectRoom(roomId);}}/>
|
||||||
</div>
|
</div>;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
||||||
module.exports = Sidebar
|
module.exports = Sidebar;
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const Promise = require("bluebird");
|
||||||
|
|
||||||
|
module.exports = function createApiRequester(apiUrl) {
|
||||||
|
return function apiRequest(path, body) {
|
||||||
|
return Promise.try(() => {
|
||||||
|
let targetUrl = apiUrl + path;
|
||||||
|
|
||||||
|
return Promise.try(() => {
|
||||||
|
return fetch(targetUrl, {
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json'
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status >= 200 && response.status < 400) {
|
||||||
|
return response.json();
|
||||||
|
} else {
|
||||||
|
throw new Error(`Non-200 response code for ${targetUrl}: ${response.status}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,19 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const Promise = require("bluebird");
|
||||||
|
|
||||||
|
module.exports = function readFileAsDataUrl(dataUrl) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = function(event) {
|
||||||
|
resolve(event.target.result);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = function(error) {
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(dataUrl);
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,49 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const jdenticon = require("jdenticon");
|
||||||
|
const defaultValue = require("default-value");
|
||||||
|
|
||||||
|
module.exports = function generateJdenticon(name) {
|
||||||
|
function setConfig() {
|
||||||
|
/* NOTE: This is a hack to ensure that any other code using the `jdenticon` library can't mess with our config, since it's set globally. This function gets called prior to *every* jdenticon-generating operation. */
|
||||||
|
jdenticon.config = {
|
||||||
|
lightness: {
|
||||||
|
color: [0.58, 0.66],
|
||||||
|
grayscale: [0.30, 0.90]
|
||||||
|
},
|
||||||
|
saturation: {
|
||||||
|
color: 0.66,
|
||||||
|
grayscale: 0.00
|
||||||
|
},
|
||||||
|
backColor: "#00000000"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toSvg: function (size) {
|
||||||
|
setConfig();
|
||||||
|
|
||||||
|
return jdenticon.toSvg(name, size);
|
||||||
|
},
|
||||||
|
primaryColor: function () {
|
||||||
|
let svg = this.toSvg();
|
||||||
|
|
||||||
|
let color = svg.match(/#([a-f0-9]{6})/g).find((candidate) => {
|
||||||
|
let r = candidate.substr(1, 2);
|
||||||
|
let g = candidate.substr(3, 2);
|
||||||
|
let b = candidate.substr(5, 2);
|
||||||
|
|
||||||
|
let isGrayScale = (r === g && g === b);
|
||||||
|
|
||||||
|
return !isGrayScale;
|
||||||
|
});
|
||||||
|
|
||||||
|
return defaultValue(color, "#ff0000");
|
||||||
|
},
|
||||||
|
update: function (element) {
|
||||||
|
setConfig();
|
||||||
|
|
||||||
|
jdenticon.update(element, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,15 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { validateOptions, required, isNumber, isString } = require("validatem");
|
||||||
|
const isMXC = require("./validate/is-mxc");
|
||||||
|
|
||||||
|
module.exports = function generateThumbnailUrl({homeserver, mxc, width, height }) {
|
||||||
|
validateOptions(arguments, {
|
||||||
|
homeserver: [ required, isString ],
|
||||||
|
mxc: [ required, isMXC ],
|
||||||
|
width: [ required, isNumber ],
|
||||||
|
height: [ required, isNumber ]
|
||||||
|
});
|
||||||
|
|
||||||
|
return `${homeserver}/_matrix/media/v1/thumbnail/${mxc.homeserver}/${mxc.id}?width=${width}&height=${height}&method=scale`;
|
||||||
|
};
|
@ -0,0 +1,34 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = function groupEvents(events) {
|
||||||
|
let currentSender = "";
|
||||||
|
let currentType = "";
|
||||||
|
let currentEvents = [];
|
||||||
|
let eventGroups = [];
|
||||||
|
|
||||||
|
function finalizeGroup() {
|
||||||
|
if (currentEvents.length > 0) {
|
||||||
|
eventGroups.push({
|
||||||
|
sender: currentSender,
|
||||||
|
events: currentEvents
|
||||||
|
});
|
||||||
|
|
||||||
|
currentEvents = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
events.forEach((event) => {
|
||||||
|
/* TODO: Eventually group multiple non-message events from a single user into a single event item as well, even when they are of different types */
|
||||||
|
if (event.sender !== currentSender || event.type !== currentType) {
|
||||||
|
finalizeGroup();
|
||||||
|
currentSender = event.sender;
|
||||||
|
currentType = event.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentEvents.push(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
finalizeGroup();
|
||||||
|
|
||||||
|
return eventGroups;
|
||||||
|
};
|
@ -1,56 +1,58 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
// should be able to handle images, stickers, and video
|
// should be able to handle images, stickers, and video
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
parseEvent: function(client, event, maxHeight, maxWidth) {
|
parseEvent: function(client, event, maxHeight, maxWidth) {
|
||||||
if (event.content.msgtype == "m.image") {
|
if (event.content.msgtype == "m.image") {
|
||||||
let h = maxHeight
|
let h = maxHeight;
|
||||||
let w = maxWidth
|
let w = maxWidth;
|
||||||
|
|
||||||
let media_url = client.mxcUrlToHttp(event.content.url)
|
let media_url = client.mxcUrlToHttp(event.content.url);
|
||||||
let thumb_url = event.content.url
|
let thumb_url = event.content.url;
|
||||||
|
|
||||||
if (event.content.info != null) {
|
if (event.content.info != null) {
|
||||||
if (event.content.info.thumbnail_url != null) {
|
if (event.content.info.thumbnail_url != null) {
|
||||||
thumb_url = event.content.info.thumbnail_url
|
thumb_url = event.content.info.thumbnail_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.content.info.thumbnail_info != null) {
|
if (event.content.info.thumbnail_info != null) {
|
||||||
h = (event.content.info.thumbnail_info.h < maxHeight) ? event.content.info.thumbnail_info.h : h
|
h = (event.content.info.thumbnail_info.h < maxHeight) ? event.content.info.thumbnail_info.h : h;
|
||||||
w = (event.content.info.thumbnail_info.w < maxWidth) ? event.content.info.thumbnail_info.w : w
|
w = (event.content.info.thumbnail_info.w < maxWidth) ? event.content.info.thumbnail_info.w : w;
|
||||||
} else {
|
} else {
|
||||||
h = (event.content.info.h < maxHeight) ? event.content.info.h : h
|
h = (event.content.info.h < maxHeight) ? event.content.info.h : h;
|
||||||
w = (event.content.info.w < maxWidth) ? event.content.info.w : w
|
w = (event.content.info.w < maxWidth) ? event.content.info.w : w;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
thumb_url = client.mxcUrlToHttp(thumb_url, w, h, 'scale', false)
|
thumb_url = client.mxcUrlToHttp(thumb_url, w, h, 'scale', false);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
full: media_url,
|
full: media_url,
|
||||||
thumb: thumb_url,
|
thumb: thumb_url,
|
||||||
size: {h, w}
|
size: {h, w}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
if (event.content.msgtype == "m.video") {
|
if (event.content.msgtype == "m.video") {
|
||||||
let thumb = null
|
let thumb = null;
|
||||||
let h = maxHeight
|
let h = maxHeight;
|
||||||
let w = maxWidth
|
let w = maxWidth;
|
||||||
|
|
||||||
if (event.content.info != null) {
|
if (event.content.info != null) {
|
||||||
if (event.content.info.thumbnail_url != null) {
|
if (event.content.info.thumbnail_url != null) {
|
||||||
if (event.content.info.thumbnail_info != null) {
|
if (event.content.info.thumbnail_info != null) {
|
||||||
h = (event.content.info.thumbnail_info.h < maxHeight) ? event.content.info.thumbnail_info.h : h
|
h = (event.content.info.thumbnail_info.h < maxHeight) ? event.content.info.thumbnail_info.h : h;
|
||||||
w = (event.content.info.thumbnail_info.w < maxWidth) ? event.content.info.thumbnail_info.w : w
|
w = (event.content.info.thumbnail_info.w < maxWidth) ? event.content.info.thumbnail_info.w : w;
|
||||||
}
|
}
|
||||||
|
|
||||||
thumb = client.mxcUrlToHttp(event.content.thumbnail, w, h, 'scale', false)
|
thumb = client.mxcUrlToHttp(event.content.thumbnail, w, h, 'scale', false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
full: client.mxcUrlToHttp(event.content.url),
|
full: client.mxcUrlToHttp(event.content.url),
|
||||||
thumb: thumb,
|
thumb: thumb,
|
||||||
size: {h, w}
|
size: {h, w}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const urlLib = require("url");
|
||||||
|
|
||||||
|
module.exports = function parseMXC(uri) {
|
||||||
|
let parsed = urlLib.parse(uri);
|
||||||
|
|
||||||
|
if (parsed.protocol === "mxc:" && parsed.slashes === true) {
|
||||||
|
return {
|
||||||
|
homeserver: parsed.host,
|
||||||
|
id: parsed.pathname.replace(/^\/+/, "")
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error("Specified URI is not an MXC URI");
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,7 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = function isMXC(value) {
|
||||||
|
if (value.homeserver == null || value.id == null) {
|
||||||
|
throw new Error("Must be an MXC object");
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,15 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
|
||||||
|
module.exports = function withElement(callback) {
|
||||||
|
let [ elementRef, setElementRef ] = React.useState();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (elementRef != null) {
|
||||||
|
callback(elementRef);
|
||||||
|
}
|
||||||
|
}, [ elementRef ]);
|
||||||
|
|
||||||
|
return setElementRef;
|
||||||
|
};
|
@ -1,89 +1,94 @@
|
|||||||
let assert = require('assert')
|
"use strict";
|
||||||
let urllib = require('url')
|
|
||||||
let querystring = require('querystring')
|
|
||||||
|
|
||||||
let mediaLib = require('../../lib/media.js')
|
/* global describe, it */
|
||||||
|
|
||||||
|
let assert = require('assert');
|
||||||
|
let urllib = require('url');
|
||||||
|
let querystring = require('querystring');
|
||||||
|
|
||||||
|
let mediaLib = require('../../lib/media.js');
|
||||||
|
|
||||||
let client = {
|
let client = {
|
||||||
mxcUrlToHttp: function(url, w, h, method, allowDirectLinks) {
|
/* FIXME: Verify whether allowDirectLinks is used / expected to do anything by other code */
|
||||||
let hs = "https://chat.privacytools.io"
|
mxcUrlToHttp: function(url, w, h, method, _allowDirectLinks) {
|
||||||
let mxc = url.slice(6)
|
let hs = "https://chat.privacytools.io";
|
||||||
if (w) {
|
let mxc = url.slice(6);
|
||||||
return `${hs}/_matrix/media/v1/thumbnail/${mxc}?w=${w}&h=${h}&method=${method}`
|
if (w) {
|
||||||
} else {
|
return `${hs}/_matrix/media/v1/thumbnail/${mxc}?w=${w}&h=${h}&method=${method}`;
|
||||||
return `${hs}/_matrix/media/v1/download/${mxc}`
|
} else {
|
||||||
}
|
return `${hs}/_matrix/media/v1/download/${mxc}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mockEventTemplate = {
|
let mockEventTemplate = {
|
||||||
type: "m.room.message",
|
type: "m.room.message",
|
||||||
sender: "@f0x:privacytools.io",
|
sender: "@f0x:privacytools.io",
|
||||||
content: {
|
content: {
|
||||||
body: "image.png",
|
body: "image.png",
|
||||||
info: {
|
info: {
|
||||||
size: 16692,
|
size: 16692,
|
||||||
mimetype: "image/png",
|
mimetype: "image/png",
|
||||||
thumbnail_info: {
|
thumbnail_info: {
|
||||||
w: 268,
|
w: 268,
|
||||||
h: 141,
|
h: 141,
|
||||||
mimetype: "image/png",
|
mimetype: "image/png",
|
||||||
size: 16896
|
size: 16896
|
||||||
},
|
},
|
||||||
w: 268,
|
w: 268,
|
||||||
h: 141,
|
h: 141,
|
||||||
thumbnail_url: "mxc://privacytools.io/zBSerdKMhaXSIxfjzCmOnhXH"
|
thumbnail_url: "mxc://privacytools.io/zBSerdKMhaXSIxfjzCmOnhXH"
|
||||||
},
|
},
|
||||||
msgtype: "m.image",
|
msgtype: "m.image",
|
||||||
url: "mxc://privacytools.io/khPaFfeRyNdzlSttZraeAUre"
|
url: "mxc://privacytools.io/khPaFfeRyNdzlSttZraeAUre"
|
||||||
},
|
},
|
||||||
event_id: "$aaa:matrix.org",
|
event_id: "$aaa:matrix.org",
|
||||||
origin_server_ts: 1558470168199,
|
origin_server_ts: 1558470168199,
|
||||||
unsigned: {
|
unsigned: {
|
||||||
age: 143237861
|
age: 143237861
|
||||||
},
|
},
|
||||||
room_id: "!aaa:matrix.org"
|
room_id: "!aaa:matrix.org"
|
||||||
}
|
};
|
||||||
|
|
||||||
describe('media', function() {
|
describe('media', function() {
|
||||||
describe('#parseEvent()', function() {
|
describe('#parseEvent()', function() {
|
||||||
it('event without info', function() {
|
it('event without info', function() {
|
||||||
let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate))
|
let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate));
|
||||||
mockEvent.content.info = null
|
mockEvent.content.info = null;
|
||||||
|
|
||||||
checkParsedEvent(mockEvent, {
|
checkParsedEvent(mockEvent, {
|
||||||
w: 1000,
|
w: 1000,
|
||||||
h: 1000,
|
h: 1000,
|
||||||
method: 'scale'
|
method: 'scale'
|
||||||
})
|
});
|
||||||
}),
|
});
|
||||||
it('event without thumbnail', function() {
|
it('event without thumbnail', function() {
|
||||||
let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate))
|
let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate));
|
||||||
mockEvent.content.info.thumbnail_url = null
|
mockEvent.content.info.thumbnail_url = null;
|
||||||
mockEvent.content.info.thumbnail_info = null
|
mockEvent.content.info.thumbnail_info = null;
|
||||||
checkParsedEvent(mockEvent, {
|
checkParsedEvent(mockEvent, {
|
||||||
w: 268,
|
w: 268,
|
||||||
h: 141,
|
h: 141,
|
||||||
method: 'scale'
|
method: 'scale'
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
it('event without thumbnail_info', function() {
|
it('event without thumbnail_info', function() {
|
||||||
let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate))
|
let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate));
|
||||||
mockEvent.content.info.thumbnail_url = null
|
mockEvent.content.info.thumbnail_url = null;
|
||||||
checkParsedEvent(mockEvent, {
|
checkParsedEvent(mockEvent, {
|
||||||
w: 268,
|
w: 268,
|
||||||
h: 141,
|
h: 141,
|
||||||
method: 'scale'
|
method: 'scale'
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
function checkParsedEvent(mockEvent, expected) {
|
function checkParsedEvent(mockEvent, expected) {
|
||||||
let media = mediaLib.parseEvent(client, mockEvent, 1000, 1000)
|
let media = mediaLib.parseEvent(client, mockEvent, 1000, 1000);
|
||||||
let params = querystring.decode(urllib.parse(media.thumb).query)
|
let params = querystring.decode(urllib.parse(media.thumb).query);
|
||||||
|
|
||||||
Object.keys(params).forEach((key) => {
|
Object.keys(params).forEach((key) => {
|
||||||
assert.equal(expected[key], params[key])
|
assert.equal(expected[key], params[key]);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue