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')
|
||||
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')
|
||||
"use strict";
|
||||
|
||||
const source = require('vinyl-source-stream')
|
||||
const buffer = require('vinyl-buffer')
|
||||
const sourcemaps = require('gulp-sourcemaps')
|
||||
const gulp = require('gulp');
|
||||
const sass = require('gulp-sass');
|
||||
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 babelify = require('babelify')
|
||||
const source = require('vinyl-source-stream');
|
||||
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)
|
||||
.pipe(sass())
|
||||
.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"]))
|
||||
})
|
||||
const cssFiles = 'src/scss/**/*.?(s)css';
|
||||
const assetsFiles = [ "src/assets/**/*" ];
|
||||
|
||||
gulp.task("clean", function(done) {
|
||||
del.sync('build')
|
||||
done()
|
||||
})
|
||||
del.sync('build');
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("sass", function() {
|
||||
return gulp.src(cssFiles)
|
||||
.pipe(sass())
|
||||
.pipe(concat('style.css'))
|
||||
.pipe(gulp.dest('./build'))
|
||||
})
|
||||
return gulp.src(cssFiles)
|
||||
.pipe(sass())
|
||||
.pipe(concat('style.css'))
|
||||
.pipe(gulp.dest('./build'));
|
||||
});
|
||||
|
||||
gulp.task("assets", function() {
|
||||
return gulp.src(["src/assets/**/*"])
|
||||
.pipe(gulpIf('*.+(png|jpg|jpeg|gif|svg)',
|
||||
cache(imagemin({
|
||||
interlaced: true
|
||||
}))
|
||||
))
|
||||
.pipe(gulp.dest('build'))
|
||||
})
|
||||
return gulp.src(assetsFiles)
|
||||
/* NOTE: Currently disabled, causes an error:
|
||||
[19:47:33] Error: Callback called multiple times
|
||||
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)
|
||||
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() {
|
||||
return gulp.src(['src/app.js', "src/components/**/*"])
|
||||
.pipe(babel({
|
||||
presets: [
|
||||
['@babel/env', {
|
||||
modules: false
|
||||
}]
|
||||
]
|
||||
}))
|
||||
.pipe(gulp.dest('build'))
|
||||
})
|
||||
let b = browserify({
|
||||
entries: 'src/app.js',
|
||||
debug: false,
|
||||
transform: [babelify.configure({
|
||||
presets: ['@babel/preset-env', '@babel/preset-react']
|
||||
})]
|
||||
});
|
||||
return b.bundle()
|
||||
.pipe(source('src/app.js'))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({ loadMaps: true }))
|
||||
.pipe(gulp.dest('build'));
|
||||
});
|
||||
|
||||
gulp.task('js', function() {
|
||||
let b = browserify({
|
||||
entries: 'src/app.js',
|
||||
debug: false,
|
||||
transform: [babelify.configure({
|
||||
presets: ['@babel/preset-env', '@babel/preset-react']
|
||||
})]
|
||||
})
|
||||
return b.bundle()
|
||||
.pipe(source('src/app.js'))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({ loadMaps: true }))
|
||||
.pipe(gulp.dest('build'))
|
||||
})
|
||||
gulp.task('watch', gulp.series(["clean", "assets", "sass", 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.watch(assetsFiles, gulp.series(["assets"]));
|
||||
}]));
|
||||
|
||||
gulp.task('build', gulp.parallel(['clean', 'assets', 'js', 'sass', function(done) {
|
||||
done()
|
||||
}]))
|
||||
done();
|
||||
}]));
|
||||
|
@ -1,228 +1,260 @@
|
||||
'use strict'
|
||||
const React = require('react')
|
||||
const ReactDOM = require('react-dom')
|
||||
const create = require('create-react-class')
|
||||
const Promise = require('bluebird')
|
||||
const urllib = require('url')
|
||||
const debounce = require('debounce')
|
||||
const defaultValue = require('default-value')
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
const create = require('create-react-class');
|
||||
const Promise = require('bluebird');
|
||||
const urllib = require('url');
|
||||
|
||||
const createApiRequester = require("../lib/api-request");
|
||||
|
||||
let login = create({
|
||||
displayName: "Login",
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
error: null,
|
||||
formState: {
|
||||
user: "",
|
||||
pass: "",
|
||||
hs: ""
|
||||
},
|
||||
hs: {
|
||||
prompt: false,
|
||||
error: null,
|
||||
valid: false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
login: function() {
|
||||
this.setState({error: ""})
|
||||
|
||||
if (this.state.hs.valid) {
|
||||
return this.doLogin()
|
||||
}
|
||||
|
||||
let parts = this.state.formState.user.split(':')
|
||||
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) => {
|
||||
console.log("Using API server", hs)
|
||||
let formState = this.state.formState
|
||||
formState.user = parts[0]
|
||||
formState.hs = hs
|
||||
let hsState = Object.assign(this.state.hs, {valid: true})
|
||||
this.setState({apiUrl: hs, formState: formState, hs: hsState})
|
||||
this.doLogin()
|
||||
}).catch((error) => {
|
||||
console.log("ERROR fetching homeserver url", error)
|
||||
let hsState = Object.assign(this.state.hs, {error: error, valid: false, prompt: true})
|
||||
this.setState({hs: hsState})
|
||||
})
|
||||
},
|
||||
|
||||
doLogin: function() {
|
||||
console.log("Logging in")
|
||||
let user = this.state.formState.user.replace('@', '')
|
||||
let password = this.state.formState.pass
|
||||
let hs = this.state.apiUrl
|
||||
|
||||
let data = {
|
||||
user: user,
|
||||
password: password,
|
||||
type: "m.login.password",
|
||||
initial_device_display_name: "Neo v4",
|
||||
};
|
||||
|
||||
let url = hs + "/_matrix/client/r0/login"
|
||||
|
||||
fetch(url, {
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
method: 'POST',
|
||||
}).then((response) => response.json())
|
||||
.then((responseJson) => {
|
||||
console.log("got access token", responseJson)
|
||||
this.setState({json: responseJson})
|
||||
if(responseJson.access_token != undefined) {
|
||||
this.props.callback(responseJson.user_id, responseJson.access_token, hs)
|
||||
} else {
|
||||
this.setState({error: responseJson.error})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(url, error);
|
||||
});
|
||||
},
|
||||
|
||||
handleUserChange: function(e) {
|
||||
let formState = this.state.formState
|
||||
let user = e.target.value
|
||||
formState.user = e.target.value
|
||||
let parts = user.split(':')
|
||||
if (parts.length == 2) {
|
||||
formState.hs = parts[1]
|
||||
let hsState = Object.assign(this.state.hs, {error: null, valid: false})
|
||||
this.setState({hs: hsState})
|
||||
}
|
||||
this.setState({formState: formState})
|
||||
},
|
||||
|
||||
handlePassChange: function(e) {
|
||||
let formState = this.state.formState
|
||||
formState.pass = e.target.value
|
||||
this.setState({formState: formState})
|
||||
},
|
||||
|
||||
handleHsChange: function(e) {
|
||||
let formState = this.state.formState
|
||||
formState.hs = e.target.value
|
||||
this.setState({formState: formState})
|
||||
this.setState({hs: {error: null, valid: false, prompt: true, changed: true}})
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let hsState = "inactive"
|
||||
if (this.state.hs.prompt) {
|
||||
hsState = "active"
|
||||
}
|
||||
if (this.state.hs.error != null) {
|
||||
hsState = "error"
|
||||
}
|
||||
if (this.state.hs.valid) {
|
||||
hsState = "validated"
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="loginwrapper">
|
||||
<img src="./neo.png"/>
|
||||
<div className="errorMessage">{this.state.error}</div>
|
||||
<div className="login">
|
||||
<label htmlFor="user">Username: </label>
|
||||
<input type="text" id="user" placeholder="@user:homeserver.tld" value={this.state.formState["user"]} onChange={this.handleUserChange}/>
|
||||
|
||||
<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>
|
||||
{this.state.hs.prompt ? (
|
||||
<>
|
||||
<input type="text" id="hs" value={this.state.formState["hs"]} onChange={this.handleHsChange}/>
|
||||
</>
|
||||
) : (
|
||||
<span id="hs">{this.state.formState["hs"]}</span>
|
||||
)}
|
||||
|
||||
<button onClick={()=>this.login()}>Log in</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function getApiServer(hostname) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log("Checking for api server from mxid", urllib.format(hostname))
|
||||
checkApi(hostname).then(() => {
|
||||
// Hostname is a valid api server
|
||||
hostname.pathname = ""
|
||||
resolve(urllib.format(hostname))
|
||||
}).catch(() => {
|
||||
console.log("trying .well-known")
|
||||
tryWellKnown(hostname).then((hostname) => {
|
||||
console.log("got .well-known host", hostname)
|
||||
resolve(hostname)
|
||||
}).catch((err) => {
|
||||
reject("Fatal error trying to get API host")
|
||||
})
|
||||
})
|
||||
})
|
||||
displayName: "Login",
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
error: null,
|
||||
formState: {
|
||||
user: "",
|
||||
pass: "",
|
||||
hs: ""
|
||||
},
|
||||
hs: {
|
||||
prompt: false,
|
||||
error: null,
|
||||
valid: false
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
login: function() {
|
||||
return Promise.try(() => {
|
||||
this.setState({error: ""});
|
||||
|
||||
if (this.state.hs.valid) {
|
||||
return this.doLogin();
|
||||
}
|
||||
|
||||
let parts = this.state.formState.user.split(':');
|
||||
if (parts.length != 2) {
|
||||
return this.setState({error: "Please enter a full mxid, like username:homeserver.tld"});
|
||||
}
|
||||
|
||||
let hostname = urllib.parse("https://" + parts[1]);
|
||||
|
||||
return Promise.try(() => {
|
||||
return getApiServer(hostname);
|
||||
}).then((homeserverUrl) => {
|
||||
console.log("Using API server", homeserverUrl);
|
||||
|
||||
this.setState({
|
||||
apiUrl: homeserverUrl,
|
||||
apiRequest: createApiRequester(homeserverUrl),
|
||||
formState: Object.assign(this.state.formState, {
|
||||
user: parts[0],
|
||||
hs: homeserverUrl
|
||||
}),
|
||||
hs: Object.assign(this.state.hs, {
|
||||
valid: true
|
||||
})
|
||||
});
|
||||
|
||||
return this.doLogin();
|
||||
}).catch((error) => {
|
||||
/* FIXME: Error filtering */
|
||||
console.log("ERROR fetching homeserver url", error);
|
||||
|
||||
this.setState({
|
||||
hs: Object.assign(this.state.hs, {
|
||||
error: error,
|
||||
valid: false,
|
||||
prompt: true
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
doLogin: function() {
|
||||
return Promise.try(() => {
|
||||
console.log("Logging in");
|
||||
let user = this.state.formState.user.replace('@', '');
|
||||
let password = this.state.formState.pass;
|
||||
let homeserverUrl = this.state.apiUrl;
|
||||
|
||||
return Promise.try(() => {
|
||||
return this.state.apiRequest("/_matrix/client/r0/login", {
|
||||
user: user,
|
||||
password: password,
|
||||
type: "m.login.password",
|
||||
initial_device_display_name: "Neo v4",
|
||||
});
|
||||
}).then((responseJson) => {
|
||||
console.log("got access token", responseJson);
|
||||
|
||||
this.setState({ json: responseJson });
|
||||
|
||||
if(responseJson.access_token != undefined) {
|
||||
this.props.callback(responseJson.user_id, responseJson.access_token, homeserverUrl);
|
||||
} else {
|
||||
this.setState({ error: responseJson.error });
|
||||
}
|
||||
}).catch((error) => {
|
||||
/* FIXME: Why are errors being swallowed here? */
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
handleUserChange: function(e) {
|
||||
let formState = this.state.formState;
|
||||
let user = e.target.value;
|
||||
formState.user = e.target.value;
|
||||
let parts = user.split(':');
|
||||
if (parts.length == 2) {
|
||||
formState.hs = parts[1];
|
||||
let hsState = Object.assign(this.state.hs, {error: null, valid: false});
|
||||
this.setState({hs: hsState});
|
||||
}
|
||||
this.setState({formState: formState});
|
||||
},
|
||||
|
||||
handlePassChange: function(e) {
|
||||
let formState = this.state.formState;
|
||||
formState.pass = e.target.value;
|
||||
this.setState({formState: formState});
|
||||
},
|
||||
|
||||
handleHomeserverChange: function(e) {
|
||||
let formState = this.state.formState;
|
||||
formState.hs = e.target.value;
|
||||
this.setState({formState: formState});
|
||||
this.setState({hs: {error: null, valid: false, prompt: true, changed: true}});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let hsState = "inactive";
|
||||
if (this.state.hs.prompt) {
|
||||
hsState = "active";
|
||||
}
|
||||
if (this.state.hs.error != null) {
|
||||
hsState = "error";
|
||||
}
|
||||
if (this.state.hs.valid) {
|
||||
hsState = "validated";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="loginwrapper">
|
||||
<img src="./neo.png"/>
|
||||
<div className="errorMessage">{this.state.error}</div>
|
||||
<div className="login">
|
||||
<label htmlFor="user">Username: </label>
|
||||
<input type="text" id="user" placeholder="@user:homeserver.tld" value={this.state.formState["user"]} onChange={this.handleUserChange}/>
|
||||
|
||||
<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>
|
||||
{this.state.hs.prompt ? (
|
||||
<>
|
||||
<input type="text" id="hs" value={this.state.formState["hs"]} onChange={this.handleHomeserverChange}/>
|
||||
</>
|
||||
) : (
|
||||
<span id="hs">{this.state.formState["hs"]}</span>
|
||||
)}
|
||||
|
||||
<button onClick={()=>this.login()}>Log in</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
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) {
|
||||
let versionUrl = buildUrl(host, "/_matrix/client/versions")
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(versionUrl).then((response) => {
|
||||
if (response.status != 200) {
|
||||
console.log("Invalid homeserver url", versionUrl)
|
||||
return reject()
|
||||
}
|
||||
resolve()
|
||||
}).catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
return Promise.try(() => {
|
||||
let versionUrl = buildUrl(host, "/_matrix/client/versions");
|
||||
|
||||
return Promise.try(() => {
|
||||
return fetch(versionUrl);
|
||||
}).then((response) => {
|
||||
if (response.status != 200) {
|
||||
console.log("Invalid homeserver url", versionUrl);
|
||||
|
||||
/* FIXME: Error types */
|
||||
throw new Error("Invalid homeserver URL");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function tryWellKnown(host) {
|
||||
let wellKnownUrl = urllib.format(Object.assign(host, {
|
||||
pathname: "/.well-known/matrix/client"
|
||||
}))
|
||||
console.log("Trying", wellKnownUrl, "for .well-known")
|
||||
return new Promise((resolve, reject) => {
|
||||
return fetch(wellKnownUrl)
|
||||
.then((response) => {
|
||||
if (response.status != 200) {
|
||||
console.log("no well-known in use")
|
||||
reject("No homeserver found")
|
||||
}
|
||||
return response
|
||||
}).catch((error) => {
|
||||
reject("can't fetch .well-known")
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
console.log("Parsed json", json)
|
||||
if (json['m.homeserver'] != undefined && json['m.homeserver'].base_url != undefined) {
|
||||
resolve(json['m.homeserver'].base_url)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Error in json", err)
|
||||
reject("Error while parsing .well-known")
|
||||
})
|
||||
})
|
||||
let wellKnownUrl = urllib.format(Object.assign(host, {
|
||||
pathname: "/.well-known/matrix/client"
|
||||
}));
|
||||
|
||||
console.log("Trying", wellKnownUrl, "for .well-known");
|
||||
|
||||
return Promise.try(() => {
|
||||
return fetch(wellKnownUrl);
|
||||
}).tap((response) => {
|
||||
if (response.status != 200) {
|
||||
console.log("no well-known in use");
|
||||
|
||||
/* FIXME: Error type */
|
||||
throw new Error("No homeserver found");
|
||||
}
|
||||
}).catch((_error) => {
|
||||
/* FIXME: Error chaining */
|
||||
throw new Error("can't fetch .well-known");
|
||||
}).then((response) => {
|
||||
return response.json();
|
||||
}).then((json) => {
|
||||
console.log("Parsed json", json);
|
||||
|
||||
if (json['m.homeserver'] != null && json['m.homeserver'].base_url != null) {
|
||||
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) {
|
||||
return urllib.format(Object.assign(host, {
|
||||
pathname: path
|
||||
}))
|
||||
function buildUrl(parsedUrl, path) {
|
||||
return urllib.format(Object.assign(parsedUrl, {
|
||||
pathname: path
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = login
|
||||
module.exports = login;
|
||||
|
@ -1,227 +1,201 @@
|
||||
'use strict'
|
||||
const React = require('react')
|
||||
const ReactDOM = require('react-dom')
|
||||
const create = require('create-react-class')
|
||||
const Promise = require('bluebird')
|
||||
const debounce = require('debounce')
|
||||
const jdenticon = require('jdenticon')
|
||||
const defaultValue = require('default-value')
|
||||
const sdk = require('matrix-js-sdk')
|
||||
const sanitize = require('sanitize-html')
|
||||
|
||||
const Event = require('./events/Event.js')
|
||||
const Info = require('./info.js')
|
||||
const Input = require('./input.js')
|
||||
const User = require('./events/user.js')
|
||||
const Loading = require('./loading.js')
|
||||
|
||||
jdenticon.config = {
|
||||
lightness: {
|
||||
color: [0.58, 0.66],
|
||||
grayscale: [0.30, 0.90]
|
||||
},
|
||||
saturation: {
|
||||
color: 0.66,
|
||||
grayscale: 0.00
|
||||
},
|
||||
backColor: "#00000000"
|
||||
};
|
||||
"use strict";
|
||||
|
||||
const Promise = require("bluebird");
|
||||
const React = require("react");
|
||||
const create = require("create-react-class");
|
||||
const sanitize = require("sanitize-html");
|
||||
const { expression } = require("dataprog");
|
||||
|
||||
const Event = require("./events/Event.js");
|
||||
const Info = require("./info.js");
|
||||
const Input = require("./input.js");
|
||||
const User = require("./events/user.js");
|
||||
const Loading = require("./loading.js");
|
||||
|
||||
const generateJdenticon = require("../lib/generate-jdenticon");
|
||||
const generateThumbnailUrl = require("../lib/generate-thumbnail-url");
|
||||
const groupEvents = require("../lib/group-events");
|
||||
const parseMXC = require("../lib/parse-mxc");
|
||||
const withElement = require("../lib/with-element");
|
||||
|
||||
let eventFunctions = {
|
||||
plaintext: function() {
|
||||
let plain = "unknown event"
|
||||
|
||||
if (this.type == "m.room.message") {
|
||||
plain = this.content.body
|
||||
|
||||
if (this.content.format == "org.matrix.custom.html") {
|
||||
plain = sanitize(this.content.formatted_body, {allowedTags: []})
|
||||
}
|
||||
}
|
||||
if (this.type == "m.room.member") {
|
||||
if (this.content.membership == "invite") {
|
||||
plain = `${this.sender} invited ${this.state_key}`
|
||||
} else if (this.content.membership == "join") {
|
||||
plain = `${this.state_key} joined the room`
|
||||
} else if (this.content.membership == "leave") {
|
||||
plain = `${this.state_key} left the room`
|
||||
} else if (this.content.membership == "kick") {
|
||||
plain = `${this.sender} kicked ${this.state_key}`
|
||||
} else if (this.content.membership == "ban") {
|
||||
plain = `${this.sender} banned ${this.state_key}`
|
||||
}
|
||||
}
|
||||
if (this.type == "m.room.avatar") {
|
||||
if (this.content.url.length > 0) {
|
||||
plain = `${this.sender} changed the room avatar`
|
||||
}
|
||||
}
|
||||
if (this.type == "m.room.name") {
|
||||
return `${this.sender} changed the room name to ${this.content.name}`
|
||||
}
|
||||
return plain
|
||||
}
|
||||
}
|
||||
plaintext: function() {
|
||||
if (this.type == "m.room.message") {
|
||||
if (this.content.format == "org.matrix.custom.html") {
|
||||
return sanitize(this.content.formatted_body, {allowedTags: []});
|
||||
} else {
|
||||
return this.content.body;
|
||||
}
|
||||
} else if (this.type == "m.room.member") {
|
||||
if (this.content.membership == "invite") {
|
||||
return `${this.sender} invited ${this.state_key}`;
|
||||
} else if (this.content.membership == "join") {
|
||||
return `${this.state_key} joined the room`;
|
||||
} else if (this.content.membership == "leave") {
|
||||
return `${this.state_key} left the room`;
|
||||
} else if (this.content.membership == "kick") {
|
||||
return `${this.sender} kicked ${this.state_key}`;
|
||||
} else if (this.content.membership == "ban") {
|
||||
return `${this.sender} banned ${this.state_key}`;
|
||||
} else {
|
||||
return "unknown member event";
|
||||
}
|
||||
} else if (this.type == "m.room.avatar") {
|
||||
if (this.content.url.length > 0) {
|
||||
return `${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 {
|
||||
return "unknown event";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let chat = create({
|
||||
displayName: "Chat",
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
ref: null,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
|
||||
getSnapshotBeforeUpdate: function(oldProps, oldState) {
|
||||
let ref = this.state.ref
|
||||
if (ref == null) {return null}
|
||||
if ((ref.scrollHeight - ref.offsetHeight) - ref.scrollTop < 100) { // Less than 100px from bottom
|
||||
return true
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
let ref = this.state.ref
|
||||
if (ref == null) {return}
|
||||
if (snapshot) { // scroll to bottom
|
||||
ref.scrollTop = (ref.scrollHeight - ref.offsetHeight)
|
||||
}
|
||||
},
|
||||
|
||||
setRef: function(ref) {
|
||||
if (ref != null) {
|
||||
this.setState({ref: ref})
|
||||
}
|
||||
},
|
||||
|
||||
onReplyClick: function(e) {
|
||||
this.setState({replyEvent: e})
|
||||
},
|
||||
|
||||
paginateBackwards: function() {
|
||||
if (this.state.loading) {
|
||||
return
|
||||
}
|
||||
let client = this.props.client
|
||||
client.paginateEventTimeline(client.getRoom(this.props.roomId).getLiveTimeline(), {backwards: true}).then(() => {
|
||||
this.setState({loading: false})
|
||||
})
|
||||
this.setState({loading: true})
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let client = this.props.client
|
||||
let empty = (
|
||||
<div className="main">
|
||||
</div>
|
||||
)
|
||||
if (this.props.roomId == undefined) {
|
||||
//empty screen
|
||||
return empty
|
||||
}
|
||||
|
||||
let room = client.getRoom(this.props.roomId)
|
||||
if (room == null) {
|
||||
return empty
|
||||
}
|
||||
|
||||
let messageGroups = {
|
||||
current: [],
|
||||
groups: [],
|
||||
sender: "",
|
||||
type: ""
|
||||
}
|
||||
|
||||
// if the sender is the same, add it to the 'current' messageGroup, if not,
|
||||
// push the old one to 'groups' and start with a new array.
|
||||
|
||||
let liveTimeline = room.getLiveTimeline()
|
||||
let liveTimelineEvents = liveTimeline.getEvents()
|
||||
|
||||
let events = []
|
||||
if (liveTimelineEvents.length > 0) {
|
||||
liveTimelineEvents.forEach((MatrixEvent) => {
|
||||
let event = MatrixEvent.event;
|
||||
event = Object.assign(event, eventFunctions)
|
||||
if (event.sender == null) { // localecho messages
|
||||
event.sender = event.user_id
|
||||
event.local = true
|
||||
}
|
||||
if (event.sender != messageGroups.sender || event.type != messageGroups.type) {
|
||||
messageGroups.sender = event.sender
|
||||
messageGroups.type = event.type
|
||||
if (messageGroups.current.length != 0) {
|
||||
messageGroups.groups.push(messageGroups.current)
|
||||
}
|
||||
messageGroups.current = []
|
||||
}
|
||||
messageGroups.current.push(event)
|
||||
})
|
||||
messageGroups.groups.push(messageGroups.current)
|
||||
|
||||
events = messageGroups.groups.map((events, id) => {
|
||||
return <EventGroup key={`${this.props.roomId}-${events[0].event_id}`} events={events} client={this.props.client} room={room} onReplyClick={this.onReplyClick}/>
|
||||
})
|
||||
}
|
||||
//TODO: replace with something that only renders events in view
|
||||
return (
|
||||
<div className="main">
|
||||
<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>
|
||||
}
|
||||
</div>
|
||||
{events}
|
||||
</div>
|
||||
</div>
|
||||
<Input client={client} roomId={this.props.roomId} replyEvent={this.state.replyEvent} onReplyClick={this.onReplyClick}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
let EventGroup = create({
|
||||
displayName: "EventGroup",
|
||||
|
||||
getInitialState: function() {
|
||||
let user = this.props.client.getUser(this.props.events[0].sender)
|
||||
let avatar = <svg id="avatar" ref={this.avatarRef} />
|
||||
|
||||
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`
|
||||
avatar = <img id="avatar" src={url}/>
|
||||
}
|
||||
|
||||
return {
|
||||
user: user,
|
||||
avatar: avatar
|
||||
}
|
||||
},
|
||||
|
||||
avatarRef: function(ref) {
|
||||
jdenticon.update(ref, this.state.user.userId)
|
||||
},
|
||||
|
||||
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
|
||||
displayName: "Chat",
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
ref: null,
|
||||
loading: false
|
||||
};
|
||||
},
|
||||
|
||||
getSnapshotBeforeUpdate: function(_oldProps, _oldState) {
|
||||
let ref = this.state.ref;
|
||||
|
||||
if (ref != null && (ref.scrollHeight - ref.offsetHeight) - ref.scrollTop < 100) {
|
||||
// Less than 100px from bottom
|
||||
return true;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
let ref = this.state.ref;
|
||||
|
||||
if (ref != null && snapshot) {
|
||||
// scroll to bottom
|
||||
ref.scrollTop = (ref.scrollHeight - ref.offsetHeight);
|
||||
}
|
||||
},
|
||||
|
||||
setRef: function(ref) {
|
||||
if (ref != null) {
|
||||
this.setState({ ref: ref });
|
||||
}
|
||||
},
|
||||
|
||||
onReplyClick: function(e) {
|
||||
this.setState({ replyEvent: e });
|
||||
},
|
||||
|
||||
paginateBackwards: function() {
|
||||
if (!this.state.loading) {
|
||||
let client = this.props.client;
|
||||
let timeline = client.getRoom(this.props.roomId).getLiveTimeline();
|
||||
|
||||
this.setState({loading: true});
|
||||
|
||||
return Promise.try(() => {
|
||||
return client.paginateEventTimeline(timeline, {backwards: true});
|
||||
}).then(() => {
|
||||
this.setState({loading: false});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let client = this.props.client;
|
||||
|
||||
let empty = <div className="main" />;
|
||||
|
||||
if (this.props.roomId == null) {
|
||||
//empty screen
|
||||
return empty;
|
||||
} else {
|
||||
let room = client.getRoom(this.props.roomId);
|
||||
if (room == null) {
|
||||
return empty;
|
||||
} else {
|
||||
let liveTimeline = room.getLiveTimeline();
|
||||
let liveTimelineEvents = liveTimeline.getEvents();
|
||||
|
||||
let events = liveTimelineEvents.map((item) => {
|
||||
let event = item.event;
|
||||
|
||||
return Object.assign(
|
||||
event,
|
||||
eventFunctions,
|
||||
(event.sender == null)
|
||||
/* Whether this event is a local echo */
|
||||
? { local: true, sender: event.user_id }
|
||||
: null
|
||||
);
|
||||
});
|
||||
|
||||
let eventGroups = groupEvents(events);
|
||||
|
||||
//TODO: replace with something that only renders events in view
|
||||
return (
|
||||
<div className="main">
|
||||
<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>
|
||||
}
|
||||
</div>
|
||||
{(eventGroups.map((group) => {
|
||||
return <EventGroup key={`${this.props.roomId}-${group.events[0].event_id}`} events={group.events} client={this.props.client} room={room} onReplyClick={this.onReplyClick}/>;
|
||||
}))}
|
||||
</div>
|
||||
</div>
|
||||
<Input client={client} roomId={this.props.roomId} replyEvent={this.state.replyEvent} onReplyClick={this.onReplyClick}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function EventGroup({ events, room, client, onReplyClick }) {
|
||||
let setAvatarRef = withElement((element) => {
|
||||
generateJdenticon(user.userId).update(element);
|
||||
});
|
||||
|
||||
let user = client.getUser(events[0].sender);
|
||||
|
||||
let avatar = expression(() => {
|
||||
if (user.avatarUrl != null) {
|
||||
let url = generateThumbnailUrl({
|
||||
homeserver: client.baseUrl,
|
||||
mxc: parseMXC(user.avatarUrl),
|
||||
width: 128,
|
||||
height: 128
|
||||
});
|
||||
|
||||
return <img id="avatar" src={url} />;
|
||||
} else {
|
||||
return <svg id="avatar" ref={setAvatarRef} />;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="eventGroup">
|
||||
{avatar}
|
||||
<div className="col">
|
||||
<User user={user}/>
|
||||
{events.map((event, key) => {
|
||||
return <Event event={event} key={key} client={client} room={room} onReplyClick={onReplyClick}/>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = chat;
|
||||
|
@ -1,115 +1,114 @@
|
||||
'use strict'
|
||||
const React = require('react')
|
||||
const ReactDOM = require('react-dom')
|
||||
const create = require('create-react-class')
|
||||
const defaultValue = require('default-value')
|
||||
'use strict';
|
||||
const React = require('react');
|
||||
const create = require('create-react-class');
|
||||
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 stateElement = require('./state.js')
|
||||
const User = require('./user.js');
|
||||
const stateElement = require('./state.js');
|
||||
|
||||
const elements = {
|
||||
"m.text": require('./text.js'),
|
||||
"m.image": require('./image.js'),
|
||||
"m.video": require('./video.js')
|
||||
}
|
||||
"m.text": require('./text.js'),
|
||||
"m.image": require('./image.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({
|
||||
displayName: "Event",
|
||||
|
||||
render: function() {
|
||||
let event = this.props.event
|
||||
let state = ""
|
||||
let reply = ""
|
||||
let element = "unsupported event: " + event.type
|
||||
|
||||
if (event.local) {
|
||||
state = " local"
|
||||
}
|
||||
|
||||
if (event.type == "m.room.message") {
|
||||
let msgtype = event.content.msgtype;
|
||||
let formattedEvent = parseEvent(event)
|
||||
|
||||
let parsedReply = formattedEvent.parsedReply
|
||||
if (parsedReply.isReply) {
|
||||
let repliedEvent = this.props.room.findEventById(parsedReply.to)
|
||||
let shortText, repliedUser
|
||||
if (repliedEvent == undefined) {
|
||||
shortText = "Can't load this event"
|
||||
repliedUser = {userId: "NEO_UNKNOWN", displayName: "Unknown User"}
|
||||
// fall back on <mx-reply> content?
|
||||
} else {
|
||||
repliedUser = this.props.client.getUser(repliedEvent.event.sender)
|
||||
shortText = parseEvent(repliedEvent.event)
|
||||
if (shortText.html) {
|
||||
shortText = <span dangerouslySetInnerHTML={{__html: shortText.body}}/>
|
||||
} else {
|
||||
shortText = shortText.body
|
||||
}
|
||||
}
|
||||
reply = (
|
||||
<div className="reply">
|
||||
<User user={repliedUser}/>
|
||||
{shortText}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
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)) {
|
||||
element = React.createElement(stateElement, {event: event})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"event" + state} onClick={() => {
|
||||
this.props.onReplyClick(event)
|
||||
console.log(event)
|
||||
}}>
|
||||
{reply}
|
||||
{element}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function parseEvent(event, context) {
|
||||
// context can be either 'main' or 'reply'
|
||||
let body = event.content.body
|
||||
let html = false
|
||||
|
||||
if (event.content.format == "org.matrix.custom.html") {
|
||||
body = riot.sanitize(event.content.formatted_body)
|
||||
html = true
|
||||
}
|
||||
|
||||
if (body) {
|
||||
body = body.trim()
|
||||
}
|
||||
|
||||
let parsedReply = parseReply(event, body)
|
||||
if (parsedReply.isReply) {
|
||||
// body with fallback stripped
|
||||
body = parsedReply.body
|
||||
}
|
||||
return {body: body, parsedReply: parsedReply, html: html}
|
||||
displayName: "Event",
|
||||
|
||||
render: function() {
|
||||
let event = this.props.event;
|
||||
let state = "";
|
||||
let reply = "";
|
||||
let element = "unsupported event: " + event.type;
|
||||
|
||||
if (event.local) {
|
||||
state = " local";
|
||||
}
|
||||
|
||||
if (event.type == "m.room.message") {
|
||||
let msgtype = event.content.msgtype;
|
||||
let formattedEvent = parseEvent(event); /* FIXME: Specify context */
|
||||
|
||||
let parsedReply = formattedEvent.parsedReply;
|
||||
if (parsedReply.isReply) {
|
||||
let repliedEvent = this.props.room.findEventById(parsedReply.to);
|
||||
let shortText, repliedUser;
|
||||
if (repliedEvent == undefined) {
|
||||
shortText = "Can't load this event";
|
||||
repliedUser = {userId: "NEO_UNKNOWN", displayName: "Unknown User"};
|
||||
// fall back on <mx-reply> content?
|
||||
} else {
|
||||
repliedUser = this.props.client.getUser(repliedEvent.event.sender);
|
||||
shortText = parseEvent(repliedEvent.event); /* FIXME: Specify context */
|
||||
if (shortText.html) {
|
||||
shortText = <span dangerouslySetInnerHTML={{__html: shortText.body}}/>;
|
||||
} else {
|
||||
shortText = shortText.body;
|
||||
}
|
||||
}
|
||||
reply = (
|
||||
<div className="reply">
|
||||
<User user={repliedUser}/>
|
||||
{shortText}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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)) {
|
||||
element = React.createElement(stateElement, {event: event});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"event" + state} onClick={() => {
|
||||
this.props.onReplyClick(event);
|
||||
console.log(event);
|
||||
}}>
|
||||
{reply}
|
||||
{element}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function parseEvent(event, _context) {
|
||||
// context can be either 'main' or 'reply'
|
||||
let body = event.content.body;
|
||||
let html = false;
|
||||
|
||||
if (event.content.format == "org.matrix.custom.html") {
|
||||
body = riot.sanitize(event.content.formatted_body);
|
||||
html = true;
|
||||
}
|
||||
|
||||
if (body) {
|
||||
body = body.trim();
|
||||
}
|
||||
|
||||
let parsedReply = parseReply(event, body);
|
||||
if (parsedReply.isReply) {
|
||||
// body with fallback stripped
|
||||
body = parsedReply.body;
|
||||
}
|
||||
return {body: body, parsedReply: parsedReply, html: html};
|
||||
}
|
||||
|
||||
function parseReply(event, body) {
|
||||
let replyTo
|
||||
try {
|
||||
replyTo = event.content['m.relates_to']['m.in_reply_to'].event_id
|
||||
if (replyTo) {
|
||||
// strip <mx-reply> from message if it exists
|
||||
body = body.replace(mxReplyRegex, "")
|
||||
}
|
||||
} catch(err) {
|
||||
// no reply
|
||||
return {isReply: false}
|
||||
}
|
||||
return {isReply: true, body: body, to: replyTo}
|
||||
let replyTo;
|
||||
try {
|
||||
replyTo = event.content['m.relates_to']['m.in_reply_to'].event_id;
|
||||
if (replyTo) {
|
||||
// strip <mx-reply> from message if it exists
|
||||
body = body.replace(mxReplyRegex, "");
|
||||
}
|
||||
} catch(err) {
|
||||
// no reply
|
||||
return {isReply: false};
|
||||
}
|
||||
return {isReply: true, body: body, to: replyTo};
|
||||
}
|
||||
|
||||
module.exports = Event
|
||||
module.exports = Event;
|
||||
|
@ -1,44 +1,35 @@
|
||||
'use strict'
|
||||
const React = require('react')
|
||||
const ReactDOM = require('react-dom')
|
||||
const create = require('create-react-class')
|
||||
const Promise = require('bluebird')
|
||||
const defaultValue = require('default-value')
|
||||
'use strict';
|
||||
const React = require('react');
|
||||
const create = require('create-react-class');
|
||||
|
||||
const mediaLib = require('../../lib/media.js')
|
||||
|
||||
const Text = require('./text.js')
|
||||
const mediaLib = require('../../lib/media.js');
|
||||
|
||||
let Event = create({
|
||||
displayName: "m.image",
|
||||
|
||||
getInitialState: function() {
|
||||
let event = this.props.event
|
||||
if (event.content.url == undefined) {
|
||||
return null
|
||||
}
|
||||
return mediaLib.parseEvent(this.props.client, event, 1000, 1000)
|
||||
},
|
||||
|
||||
updateSize: function(e) {
|
||||
console.log("image was loaded")
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let event = this.props.event
|
||||
|
||||
if (this.state == null) {
|
||||
return "malformed image event: " + event.content.body
|
||||
}
|
||||
|
||||
return (
|
||||
<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}}/>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
displayName: "m.image",
|
||||
|
||||
getInitialState: function() {
|
||||
let event = this.props.event;
|
||||
if (event.content.url == undefined) {
|
||||
return null;
|
||||
}
|
||||
return mediaLib.parseEvent(this.props.client, event, 1000, 1000);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let event = this.props.event;
|
||||
|
||||
if (this.state == null) {
|
||||
return "malformed image event: " + event.content.body;
|
||||
}
|
||||
|
||||
return (
|
||||
<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}}/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Event;
|
||||
|
@ -1,19 +1,18 @@
|
||||
'use strict'
|
||||
const React = require('react')
|
||||
const ReactDOM = require('react-dom')
|
||||
const create = require('create-react-class')
|
||||
'use strict';
|
||||
const React = require('react');
|
||||
const create = require('create-react-class');
|
||||
|
||||
let Event = create({
|
||||
displayName: "genericStateEvent",
|
||||
displayName: "genericStateEvent",
|
||||
|
||||
render: function() {
|
||||
let event = this.props.event
|
||||
return (
|
||||
<div className="body">
|
||||
{event.plaintext()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
render: function() {
|
||||
let event = this.props.event;
|
||||
return (
|
||||
<div className="body">
|
||||
{event.plaintext()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Event
|
||||
module.exports = Event;
|
||||
|
@ -1,43 +1,39 @@
|
||||
'use strict'
|
||||
const React = require('react')
|
||||
const ReactDOM = require('react-dom')
|
||||
const create = require('create-react-class')
|
||||
const Promise = require('bluebird')
|
||||
|
||||
const riot = require('../../lib/riot-utils.js')
|
||||
'use strict';
|
||||
const React = require('react');
|
||||
const create = require('create-react-class');
|
||||
|
||||
let Event = create({
|
||||
displayName: "m.text",
|
||||
displayName: "m.text",
|
||||
|
||||
render: function() {
|
||||
let event = this.props.event
|
||||
let formattedEvent = this.props.formattedEvent
|
||||
render: function() {
|
||||
let event = this.props.event;
|
||||
let formattedEvent = this.props.formattedEvent;
|
||||
|
||||
let eventBody
|
||||
let eventBody;
|
||||
|
||||
if (formattedEvent.html) {
|
||||
eventBody = <div
|
||||
className="body"
|
||||
dangerouslySetInnerHTML={{__html: formattedEvent.body}}
|
||||
/>
|
||||
} else {
|
||||
eventBody =
|
||||
if (formattedEvent.html) {
|
||||
eventBody = <div
|
||||
className="body"
|
||||
dangerouslySetInnerHTML={{__html: formattedEvent.body}}
|
||||
/>;
|
||||
} else {
|
||||
eventBody =
|
||||
<div className="body">
|
||||
{formattedEvent.body}
|
||||
</div>
|
||||
}
|
||||
{formattedEvent.body}
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
||||
let eventClass = ""
|
||||
if (event.local) {
|
||||
eventClass += " local"
|
||||
}
|
||||
let eventClass = "";
|
||||
if (event.local) {
|
||||
eventClass += " local";
|
||||
}
|
||||
|
||||
return <div className={eventClass}>
|
||||
{eventBody}
|
||||
</div>
|
||||
}
|
||||
})
|
||||
return <div className={eventClass}>
|
||||
{eventBody}
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = Event
|
||||
module.exports = Event;
|
||||
|
@ -1,49 +1,27 @@
|
||||
'use strict'
|
||||
const React = require('react')
|
||||
const ReactDOM = require('react-dom')
|
||||
const create = require('create-react-class')
|
||||
const jdenticon = require('jdenticon')
|
||||
"use strict";
|
||||
|
||||
jdenticon.config = {
|
||||
lightness: {
|
||||
color: [0.58, 0.66],
|
||||
grayscale: [0.30, 0.90]
|
||||
},
|
||||
saturation: {
|
||||
color: 0.66,
|
||||
grayscale: 0.00
|
||||
},
|
||||
backColor: "#00000000"
|
||||
};
|
||||
const React = require("react");
|
||||
const create = require("create-react-class");
|
||||
|
||||
const generateJdenticon = require("../../lib/generate-jdenticon");
|
||||
|
||||
let User = create({
|
||||
displayName: "user",
|
||||
displayName: "user",
|
||||
|
||||
getInitialState: function() {
|
||||
let icon = jdenticon.toSvg(this.props.user.userId, 200)
|
||||
let match = icon.match(/#([a-f0-9]{6})/g)
|
||||
let color = '#ff0000'
|
||||
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
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
/* FIXME: Cache this to speed it up */
|
||||
color: generateJdenticon(this.props.user.userId).primaryColor()
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="user" style={{color: this.state.color}}>
|
||||
{this.props.user.displayName}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
render: function() {
|
||||
return (
|
||||
<div className="user" style={{color: this.state.color}}>
|
||||
{this.props.user.displayName}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = User
|
||||
module.exports = User;
|
||||
|
@ -1,40 +1,35 @@
|
||||
'use strict'
|
||||
const React = require('react')
|
||||
const ReactDOM = require('react-dom')
|
||||
const create = require('create-react-class')
|
||||
const Promise = require('bluebird')
|
||||
const defaultValue = require('default-value')
|
||||
'use strict';
|
||||
const React = require('react');
|
||||
const create = require('create-react-class');
|
||||
|
||||
const mediaLib = require('../../lib/media.js')
|
||||
|
||||
const Text = require('./text.js')
|
||||
const mediaLib = require('../../lib/media.js');
|
||||
|
||||
let Event = create({
|
||||
displayName: "m.video",
|
||||
|
||||
getInitialState: function() {
|
||||
let event = this.props.event
|
||||
if (event.content.url == undefined) {
|
||||
return null
|
||||
}
|
||||
return mediaLib.parseEvent(this.props.client, event, 1000, 1000)
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let event = this.props.event
|
||||
|
||||
if (this.state == null) {
|
||||
return "malformed video event: " + event.content.body
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="body">
|
||||
<video controls poster={this.state.thumb} style={{maxHeight: this.state.size.h, maxWidth: this.state.size.w}}>
|
||||
<source src={this.state.full}></source>
|
||||
</video>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
displayName: "m.video",
|
||||
|
||||
getInitialState: function() {
|
||||
let event = this.props.event;
|
||||
if (event.content.url == undefined) {
|
||||
return null;
|
||||
}
|
||||
return mediaLib.parseEvent(this.props.client, event, 1000, 1000);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let event = this.props.event;
|
||||
|
||||
if (this.state == null) {
|
||||
return "malformed video event: " + event.content.body;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="body">
|
||||
<video controls poster={this.state.thumb} style={{maxHeight: this.state.size.h, maxWidth: this.state.size.w}}>
|
||||
<source src={this.state.full}></source>
|
||||
</video>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Event;
|
||||
|
@ -1,46 +1,44 @@
|
||||
'use strict'
|
||||
const React = require('react')
|
||||
const ReactDOM = require('react-dom')
|
||||
const create = require('create-react-class')
|
||||
'use strict';
|
||||
|
||||
let fileUpload = create({
|
||||
displayName: "FileUpload",
|
||||
const Promise = require("bluebird");
|
||||
const React = require('react');
|
||||
|
||||
setFileRef: function(e) {
|
||||
if (e != null) {
|
||||
e.addEventListener('change', this.startUpload)
|
||||
this.setState({
|
||||
fileRef: e
|
||||
})
|
||||
}
|
||||
},
|
||||
const withElement = require("../lib/with-element");
|
||||
const fileToDataUrl = require("../lib/file-to-data-url");
|
||||
|
||||
startUpload: function(e) {
|
||||
Array.from(e.target.files).forEach((file) => {
|
||||
if (file.type.startsWith("image/")) {
|
||||
let reader = new FileReader()
|
||||
reader.onloadend = () => {
|
||||
let fileObject = {
|
||||
file: file,
|
||||
preview: reader.result
|
||||
}
|
||||
this.props.addUpload(fileObject)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
} else {
|
||||
this.props.addUpload({file: file, preview: "/icons/file.svg"})
|
||||
}
|
||||
})
|
||||
},
|
||||
module.exports = function FileUpload({ addUpload }) {
|
||||
function handleChange(event) {
|
||||
return Promise.map(Array.from(event.target.files), (file) => {
|
||||
if (file.type.startsWith("image/")) {
|
||||
return Promise.try(() => {
|
||||
return fileToDataUrl(file);
|
||||
}).then((url) => {
|
||||
return addUpload({
|
||||
file: file,
|
||||
preview: url
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return addUpload({
|
||||
file: file,
|
||||
preview: "/icons/file.svg"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="fileUpload">
|
||||
<input type="file" id="fileUpload" multiple ref={this.setFileRef} />
|
||||
<label htmlFor="fileUpload"><img src="/icons/file.svg"/></label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
let setFileRef = withElement((element) => {
|
||||
element.addEventListener("change", handleChange);
|
||||
|
||||
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'
|
||||
const React = require('react')
|
||||
const ReactDOM = require('react-dom')
|
||||
const create = require('create-react-class')
|
||||
const Promise = require('bluebird')
|
||||
const debounce = require('debounce')
|
||||
'use strict';
|
||||
const React = require('react');
|
||||
const create = require('create-react-class');
|
||||
const debounce = require('debounce');
|
||||
|
||||
let FilterList = create({
|
||||
displayName: "FilterList",
|
||||
displayName: "FilterList",
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
selection: "room0",
|
||||
filter: ""
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
selection: "room0",
|
||||
filter: ""
|
||||
};
|
||||
},
|
||||
|
||||
select: function(id) {
|
||||
this.setState({selection: id, filter: ""})
|
||||
this.state.inputRef.value = ""
|
||||
this.props.callback(id)
|
||||
},
|
||||
select: function(id) {
|
||||
this.setState({selection: id, filter: ""});
|
||||
this.state.inputRef.value = "";
|
||||
this.props.callback(id);
|
||||
},
|
||||
|
||||
inputRef: function(ref) {
|
||||
if (ref == null) {
|
||||
return
|
||||
}
|
||||
this.setState({
|
||||
inputRef: ref
|
||||
})
|
||||
ref.addEventListener("keyup", debounce(this.input, 20))
|
||||
},
|
||||
inputRef: function(ref) {
|
||||
if (ref == null) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
inputRef: ref
|
||||
});
|
||||
ref.addEventListener("keyup", debounce(this.input, 20));
|
||||
},
|
||||
|
||||
input: function(e) {
|
||||
this.setState({
|
||||
filter: e.target.value.toUpperCase()
|
||||
})
|
||||
},
|
||||
input: function(e) {
|
||||
this.setState({
|
||||
filter: e.target.value.toUpperCase()
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let keys = Object.keys(this.props.items)
|
||||
let items = keys.map((itemKey, id) => {
|
||||
let item = this.props.items[itemKey]
|
||||
let props = {
|
||||
selected: this.state.selection == itemKey,
|
||||
filter: this.state.filter,
|
||||
content: item,
|
||||
key: itemKey,
|
||||
listId: itemKey,
|
||||
select: this.select,
|
||||
properties: this.props.properties
|
||||
}
|
||||
return React.createElement(this.props.element, props)
|
||||
})
|
||||
return <>
|
||||
render: function() {
|
||||
let keys = Object.keys(this.props.items);
|
||||
let items = keys.map((itemKey) => {
|
||||
let item = this.props.items[itemKey];
|
||||
let props = {
|
||||
selected: this.state.selection == itemKey,
|
||||
filter: this.state.filter,
|
||||
content: item,
|
||||
key: itemKey,
|
||||
listId: itemKey,
|
||||
select: this.select,
|
||||
properties: this.props.properties
|
||||
};
|
||||
return React.createElement(this.props.element, props);
|
||||
});
|
||||
return <>
|
||||
<input className="filter" ref={this.inputRef} placeholder="Search"/>
|
||||
<div className="list">
|
||||
{items}
|
||||
{items}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
})
|
||||
</>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = FilterList
|
||||
module.exports = FilterList;
|
||||
|
@ -1,19 +1,17 @@
|
||||
'use strict'
|
||||
const React = require('react')
|
||||
const ReactDOM = require('react-dom')
|
||||
const create = require('create-react-class')
|
||||
const Promise = require('bluebird')
|
||||
'use strict';
|
||||
const React = require('react');
|
||||
const create = require('create-react-class');
|
||||
|
||||
let info = create({
|
||||
displayName: "Info",
|
||||
render: function() {
|
||||
let title = ""
|
||||
if (this.props.room != undefined) {
|
||||
title = this.props.room.name
|
||||
}
|
||||
return <div className="info">{title}</div>
|
||||
}
|
||||
})
|
||||
displayName: "Info",
|
||||
render: function() {
|
||||
let title = "";
|
||||
if (this.props.room != undefined) {
|
||||
title = this.props.room.name;
|
||||
}
|
||||
return <div className="info">{title}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = info
|
||||
module.exports = info;
|
||||
|
@ -1,277 +1,281 @@
|
||||
'use strict'
|
||||
const React = require('react')
|
||||
const ReactDOM = require('react-dom')
|
||||
const create = require('create-react-class')
|
||||
const Promise = require('bluebird')
|
||||
const colorConvert = require('color-convert')
|
||||
const sanitize = require('sanitize-html')
|
||||
'use strict';
|
||||
const React = require('react');
|
||||
const create = require('create-react-class');
|
||||
const Promise = require('bluebird');
|
||||
const colorConvert = require('color-convert');
|
||||
const sanitize = require('sanitize-html');
|
||||
|
||||
const riot = require('../lib/riot-utils.js')
|
||||
const FileUpload = require('./fileUpload.js')
|
||||
const riot = require('../lib/riot-utils.js');
|
||||
const FileUpload = require('./fileUpload.js');
|
||||
|
||||
let input = create({
|
||||
displayName: "Input",
|
||||
displayName: "Input",
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
uploads: []
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
uploads: []
|
||||
};
|
||||
},
|
||||
|
||||
setRef: function(ref) {
|
||||
if (ref !=null) {
|
||||
ref.addEventListener("keydown", (e) => {
|
||||
// only send on plain 'enter'
|
||||
if (e.key == "Enter") {
|
||||
if (!e.shiftKey && !e.altKey && !e.ctrlKey) {
|
||||
this.send(e)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
ref.addEventListener('change', this.resize_textarea)
|
||||
ref.addEventListener('cut', this.resize_textarea_delayed)
|
||||
ref.addEventListener('paste', this.resize_textarea_delayed)
|
||||
ref.addEventListener('drop', this.resize_textarea_delayed)
|
||||
ref.addEventListener('keydown', this.resize_textarea_delayed)
|
||||
this.setState({ref: ref})
|
||||
}
|
||||
},
|
||||
setRef: function(ref) {
|
||||
if (ref !=null) {
|
||||
ref.addEventListener("keydown", (e) => {
|
||||
// only send on plain 'enter'
|
||||
if (e.key == "Enter") {
|
||||
if (!e.shiftKey && !e.altKey && !e.ctrlKey) {
|
||||
this.send(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
ref.addEventListener('change', this.resize_textarea);
|
||||
ref.addEventListener('cut', this.resize_textarea_delayed);
|
||||
ref.addEventListener('paste', this.resize_textarea_delayed);
|
||||
ref.addEventListener('drop', this.resize_textarea_delayed);
|
||||
ref.addEventListener('keydown', this.resize_textarea_delayed);
|
||||
this.setState({ref: ref});
|
||||
}
|
||||
},
|
||||
|
||||
addUpload: function(upload) {
|
||||
let uploads = this.state.uploads
|
||||
uploads.push(upload)
|
||||
this.setState({uploads: uploads})
|
||||
},
|
||||
addUpload: function(upload) {
|
||||
let uploads = this.state.uploads;
|
||||
uploads.push(upload);
|
||||
this.setState({uploads: uploads});
|
||||
},
|
||||
|
||||
removeUpload: function(index) {
|
||||
let uploads = this.state.uploads
|
||||
uploads.splice(index, 1)
|
||||
this.setState({uploads: uploads})
|
||||
},
|
||||
removeUpload: function(index) {
|
||||
let uploads = this.state.uploads;
|
||||
uploads.splice(index, 1);
|
||||
this.setState({uploads: uploads});
|
||||
},
|
||||
|
||||
resize_textarea: function(element) {
|
||||
if (element == undefined) {
|
||||
return;
|
||||
}
|
||||
let ref = element.target;
|
||||
if (ref != undefined) {
|
||||
ref.style.height = 'auto';
|
||||
ref.style.height = ref.scrollHeight+'px';
|
||||
}
|
||||
},
|
||||
resize_textarea: function(element) {
|
||||
if (element == undefined) {
|
||||
return;
|
||||
}
|
||||
let ref = element.target;
|
||||
if (ref != undefined) {
|
||||
ref.style.height = 'auto';
|
||||
ref.style.height = ref.scrollHeight+'px';
|
||||
}
|
||||
},
|
||||
|
||||
resize_textarea_delayed: function(e) {
|
||||
setTimeout(() => this.resize_textarea(e), 5);
|
||||
},
|
||||
resize_textarea_delayed: function(e) {
|
||||
setTimeout(() => this.resize_textarea(e), 5);
|
||||
},
|
||||
|
||||
send: function(e) {
|
||||
let msg = e.target.value
|
||||
if (msg.trim().length != 0) {
|
||||
//TODO: parse markdown (commonmark?)
|
||||
if (msg.startsWith('/')) {
|
||||
// Handle other commands
|
||||
let parts = msg.split(' ')
|
||||
let command = parts[0]
|
||||
let result = handleCommands(command, parts)
|
||||
if (result != null) {
|
||||
if (result.type == "html") {
|
||||
this.sendHTML(result.content)
|
||||
} else {
|
||||
this.sendPlain(result.content)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.sendPlain(msg)
|
||||
}
|
||||
}
|
||||
send: function(e) {
|
||||
let msg = e.target.value;
|
||||
if (msg.trim().length != 0) {
|
||||
//TODO: parse markdown (commonmark?)
|
||||
if (msg.startsWith('/')) {
|
||||
// Handle other commands
|
||||
let parts = msg.split(' ');
|
||||
let command = parts[0];
|
||||
let result = handleCommands(command, parts);
|
||||
if (result != null) {
|
||||
if (result.type == "html") {
|
||||
this.sendHTML(result.content);
|
||||
} else {
|
||||
this.sendPlain(result.content);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.sendPlain(msg);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.uploads.length > 0) {
|
||||
this.uploadFiles(this.state.uploads)
|
||||
this.setState({uploads: []})
|
||||
}
|
||||
e.target.value = ""
|
||||
e.preventDefault()
|
||||
this.resize_textarea_delayed(e)
|
||||
},
|
||||
if (this.state.uploads.length > 0) {
|
||||
this.uploadFiles(this.state.uploads);
|
||||
this.setState({uploads: []});
|
||||
}
|
||||
e.target.value = "";
|
||||
e.preventDefault();
|
||||
this.resize_textarea_delayed(e);
|
||||
},
|
||||
|
||||
uploadFiles: function(uploads) {
|
||||
let client = this.props.client
|
||||
Promise.map(uploads, (upload) => {
|
||||
let fileUploadPromise = client.uploadContent(upload.file,
|
||||
{onlyContentUri: false}).then((response) => {
|
||||
return response.content_uri
|
||||
})
|
||||
uploadFiles: function(uploads) {
|
||||
let client = this.props.client;
|
||||
|
||||
let mimeType = upload.file.type
|
||||
let eventType = "m.file"
|
||||
let additionalPromise
|
||||
if (mimeType.startsWith("image/") || mimeType.startsWith("video/")) {
|
||||
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
|
||||
}
|
||||
})
|
||||
})
|
||||
return Promise.map(uploads, (upload) => {
|
||||
let fileUploadPromise = client.uploadContent(upload.file,
|
||||
{onlyContentUri: false}).then((response) => {
|
||||
return response.content_uri;
|
||||
});
|
||||
|
||||
})
|
||||
}
|
||||
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"
|
||||
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]
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
let mimeType = upload.file.type;
|
||||
let eventType = "m.file";
|
||||
let additionalPromise;
|
||||
if (mimeType.startsWith("image/") || mimeType.startsWith("video/")) {
|
||||
function elementToThumbnail(element) {
|
||||
return Promise.try(() => {
|
||||
return riot.createThumbnail(element,
|
||||
element.width,
|
||||
element.height,
|
||||
thumbnailType
|
||||
);
|
||||
}).catch((error) => {
|
||||
console.error("neo: error getting thumbnail", error);
|
||||
|
||||
sendPlain: function(string) {
|
||||
let content = {
|
||||
body: string,
|
||||
msgtype: "m.text"
|
||||
}
|
||||
content = this.sendReply(content)
|
||||
this.props.client.sendEvent(this.props.roomId, "m.room.message", content, (err, res) => {
|
||||
if (err != null) {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
},
|
||||
throw error;
|
||||
}).then((thumbResult) => {
|
||||
return client.uploadContent(thumbResult.thumbnail, {onlyContentUri: false});
|
||||
}).then((response) => {
|
||||
return {
|
||||
thumbnail_url: response.content_uri,
|
||||
thumbnail_info: {
|
||||
mimetype: thumbnailType
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
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) {
|
||||
let content = {
|
||||
body: sanitize(html, {allowedTags: []}),
|
||||
formatted_body: html,
|
||||
format: "org.matrix.custom.html",
|
||||
msgtype: "m.text"
|
||||
}
|
||||
if (mimeType == "image/jpeg") {
|
||||
thumbnailType = mimeType;
|
||||
}
|
||||
} else if (mimeType.startsWith("audio/")) {
|
||||
eventType = "m.audio";
|
||||
} 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) => {
|
||||
console.log(err)
|
||||
})
|
||||
},
|
||||
if (result[1] != undefined) {
|
||||
info = Object.assign(info, result[1]);
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
return client.sendEvent(this.props.roomId, "m.room.message", {
|
||||
body: upload.file.name,
|
||||
msgtype: eventType,
|
||||
info: info,
|
||||
url: result[0]
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
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>
|
||||
}
|
||||
})
|
||||
sendPlain: function(string) {
|
||||
let content = {
|
||||
body: string,
|
||||
msgtype: "m.text"
|
||||
};
|
||||
content = this.sendReply(content);
|
||||
/* FIXME: Promisify */
|
||||
this.props.client.sendEvent(this.props.roomId, "m.room.message", content, (err, _res) => {
|
||||
if (err != null) {
|
||||
console.log(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
sendHTML: function(html) {
|
||||
let content = {
|
||||
body: sanitize(html, {allowedTags: []}),
|
||||
formatted_body: html,
|
||||
format: "org.matrix.custom.html",
|
||||
msgtype: "m.text"
|
||||
};
|
||||
|
||||
content = this.sendReply(content);
|
||||
|
||||
/* 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) {
|
||||
if (command == "/rainbow") {
|
||||
if (parts.length < 2) {
|
||||
return
|
||||
}
|
||||
let string = parts[1]
|
||||
for(let i=2; i < parts.length; i++) {
|
||||
string += " " + parts[i]
|
||||
}
|
||||
let html = rainbowTransform(string)
|
||||
return {
|
||||
type: 'html',
|
||||
content: html
|
||||
}
|
||||
}
|
||||
return null
|
||||
if (command == "/rainbow") {
|
||||
if (parts.length < 2) {
|
||||
return;
|
||||
}
|
||||
let string = parts[1];
|
||||
for(let i=2; i < parts.length; i++) {
|
||||
string += " " + parts[i];
|
||||
}
|
||||
let html = rainbowTransform(string);
|
||||
return {
|
||||
type: 'html',
|
||||
content: html
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function rainbowTransform(text) {
|
||||
let array = text.split("");
|
||||
let delta = 360/text.length;
|
||||
if (delta < 10) {
|
||||
delta = 10;
|
||||
} else if (delta > 20) {
|
||||
delta = 20;
|
||||
}
|
||||
let h = -1 * delta; // start at beginning
|
||||
let array = text.split("");
|
||||
let delta = 360/text.length;
|
||||
if (delta < 10) {
|
||||
delta = 10;
|
||||
} else if (delta > 20) {
|
||||
delta = 20;
|
||||
}
|
||||
let h = -1 * delta; // start at beginning
|
||||
|
||||
let rainbowArray = array.map((char) => {
|
||||
h = h + delta;
|
||||
if (h > 360) {
|
||||
h = 0;
|
||||
}
|
||||
return `<font color="${colorConvert.hsl.hex(h, 100, 50)}">${char}</font>`;
|
||||
});
|
||||
let rainbow = rainbowArray.join("");
|
||||
return rainbow;
|
||||
let rainbowArray = array.map((char) => {
|
||||
h = h + delta;
|
||||
if (h > 360) {
|
||||
h = 0;
|
||||
}
|
||||
return `<font color="${colorConvert.hsl.hex(h, 100, 50)}">${char}</font>`;
|
||||
});
|
||||
let rainbow = rainbowArray.join("");
|
||||
return rainbow;
|
||||
}
|
||||
|
||||
module.exports = input
|
||||
module.exports = input;
|
||||
|
@ -1,20 +1,19 @@
|
||||
'use strict'
|
||||
const React = require('react')
|
||||
const ReactDOM = require('react-dom')
|
||||
const create = require('create-react-class')
|
||||
'use strict';
|
||||
const React = require('react');
|
||||
const create = require('create-react-class');
|
||||
|
||||
let Loading = create({
|
||||
displayName: "Loading",
|
||||
displayName: "Loading",
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="spinner">
|
||||
<div className="bounce1"/>
|
||||
<div className="bounce2"/>
|
||||
<div className="bounce3"/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
render: function() {
|
||||
return (
|
||||
<div className="spinner">
|
||||
<div className="bounce1"/>
|
||||
<div className="bounce2"/>
|
||||
<div className="bounce3"/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Loading
|
||||
module.exports = Loading;
|
||||
|
@ -1,117 +1,114 @@
|
||||
'use strict'
|
||||
const React = require('react')
|
||||
const ReactDOM = require('react-dom')
|
||||
const create = require('create-react-class')
|
||||
const Promise = require('bluebird')
|
||||
const debounce = require('debounce')
|
||||
const jdenticon = require('jdenticon')
|
||||
'use strict';
|
||||
const React = require('react');
|
||||
const create = require('create-react-class');
|
||||
const jdenticon = require('jdenticon');
|
||||
|
||||
const FilterList = require('./filterList.js')
|
||||
const FilterList = require('./filterList.js');
|
||||
|
||||
let RoomListItem = create({
|
||||
displayName: "RoomListItem",
|
||||
|
||||
getInitialState: function() {
|
||||
let room = this.props.content
|
||||
let client = this.props.properties.client
|
||||
let jdenticon = <svg id="avatar" ref={this.jdenticonRef}/>
|
||||
let avatarUrl
|
||||
|
||||
let roomState = room.getLiveTimeline().getState('f')
|
||||
let avatarState = roomState.getStateEvents('m.room.avatar')
|
||||
if (avatarState.length > 0) {
|
||||
let event = avatarState[avatarState.length-1].event
|
||||
let hs = client.baseUrl
|
||||
let media_mxc = event.content.url.slice(6)
|
||||
let path = `/_matrix/media/v1/thumbnail/${media_mxc}?width=128&height=128&method=scale`
|
||||
avatarUrl = {
|
||||
hs: hs,
|
||||
path: path
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
filterName: room.name.toUpperCase(),
|
||||
unread: Math.random() > 0.7,
|
||||
avatarUrl: avatarUrl,
|
||||
jdenticon: jdenticon,
|
||||
tries: 0
|
||||
}
|
||||
},
|
||||
|
||||
jdenticonRef: function(ref) {
|
||||
jdenticon.update(ref, this.props.content.roomId)
|
||||
},
|
||||
|
||||
avatarFallback: function() {
|
||||
// instead of falling back on jdenticon immediately, we can try
|
||||
// a third-party homeserver's media repo
|
||||
// this does come with trust issues, and is opt-in in settings
|
||||
let fallbackMediaRepos = this.props.properties.options.fallbackMediaRepos
|
||||
|
||||
if (this.state.tries < fallbackMediaRepos.length) {
|
||||
let avatarUrl = this.state.avatarUrl
|
||||
avatarUrl.hs = fallbackMediaRepos[this.state.tries]
|
||||
this.setState({
|
||||
avatarUrl: avatarUrl,
|
||||
tries: this.state.tries + 1
|
||||
})
|
||||
} else {
|
||||
this.setState({avatarUrl: null, avatar: jdenticon})
|
||||
}
|
||||
},
|
||||
|
||||
setRef: function(ref) {
|
||||
if (ref == null) {
|
||||
return
|
||||
}
|
||||
this.setState({ref: ref})
|
||||
ref.addEventListener("click", () => {this.props.select(this.props.listId)})
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.filterName.indexOf(this.props.filter) == -1) {
|
||||
return null
|
||||
}
|
||||
let className = "roomListItem"
|
||||
if (this.props.selected) {
|
||||
className += " active"
|
||||
}
|
||||
if (this.state.unread) {
|
||||
className += " unread"
|
||||
}
|
||||
return <div className={className} ref={this.setRef}>
|
||||
{this.state.avatarUrl ?
|
||||
<img id="avatar" src={`${this.state.avatarUrl.hs}${this.state.avatarUrl.path}`} onError={this.avatarFallback}></img>
|
||||
:
|
||||
this.state.jdenticon
|
||||
}
|
||||
<span id="name">{this.props.content.name}</span>
|
||||
</div>
|
||||
}
|
||||
})
|
||||
displayName: "RoomListItem",
|
||||
|
||||
getInitialState: function() {
|
||||
let room = this.props.content;
|
||||
let client = this.props.properties.client;
|
||||
let jdenticon = <svg id="avatar" ref={this.jdenticonRef}/>;
|
||||
let avatarUrl;
|
||||
|
||||
let roomState = room.getLiveTimeline().getState('f');
|
||||
let avatarState = roomState.getStateEvents('m.room.avatar');
|
||||
if (avatarState.length > 0) {
|
||||
let event = avatarState[avatarState.length-1].event;
|
||||
let hs = client.baseUrl;
|
||||
let media_mxc = event.content.url.slice(6);
|
||||
let path = `/_matrix/media/v1/thumbnail/${media_mxc}?width=128&height=128&method=scale`;
|
||||
avatarUrl = {
|
||||
hs: hs,
|
||||
path: path
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
filterName: room.name.toUpperCase(),
|
||||
unread: Math.random() > 0.7,
|
||||
avatarUrl: avatarUrl,
|
||||
jdenticon: jdenticon,
|
||||
tries: 0
|
||||
};
|
||||
},
|
||||
|
||||
jdenticonRef: function(ref) {
|
||||
jdenticon.update(ref, this.props.content.roomId);
|
||||
},
|
||||
|
||||
avatarFallback: function() {
|
||||
// instead of falling back on jdenticon immediately, we can try
|
||||
// a third-party homeserver's media repo
|
||||
// this does come with trust issues, and is opt-in in settings
|
||||
let fallbackMediaRepos = this.props.properties.options.fallbackMediaRepos;
|
||||
|
||||
if (this.state.tries < fallbackMediaRepos.length) {
|
||||
let avatarUrl = this.state.avatarUrl;
|
||||
avatarUrl.hs = fallbackMediaRepos[this.state.tries];
|
||||
this.setState({
|
||||
avatarUrl: avatarUrl,
|
||||
tries: this.state.tries + 1
|
||||
});
|
||||
} else {
|
||||
this.setState({avatarUrl: null, avatar: jdenticon});
|
||||
}
|
||||
},
|
||||
|
||||
setRef: function(ref) {
|
||||
if (ref == null) {
|
||||
return;
|
||||
}
|
||||
this.setState({ref: ref});
|
||||
ref.addEventListener("click", () => {this.props.select(this.props.listId);});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.filterName.indexOf(this.props.filter) == -1) {
|
||||
return null;
|
||||
}
|
||||
let className = "roomListItem";
|
||||
if (this.props.selected) {
|
||||
className += " active";
|
||||
}
|
||||
if (this.state.unread) {
|
||||
className += " unread";
|
||||
}
|
||||
return <div className={className} ref={this.setRef}>
|
||||
{this.state.avatarUrl ?
|
||||
<img id="avatar" src={`${this.state.avatarUrl.hs}${this.state.avatarUrl.path}`} onError={this.avatarFallback}></img>
|
||||
:
|
||||
this.state.jdenticon
|
||||
}
|
||||
<span id="name">{this.props.content.name}</span>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
let Sidebar = create({
|
||||
displayName: "Sidebar",
|
||||
displayName: "Sidebar",
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
filter: ""
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
filter: ""
|
||||
};
|
||||
},
|
||||
|
||||
setFilter: function(filter) {
|
||||
this.setState({
|
||||
filter: filter.toUpperCase()
|
||||
})
|
||||
},
|
||||
setFilter: function(filter) {
|
||||
this.setState({
|
||||
filter: filter.toUpperCase()
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
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)}}/>
|
||||
</div>
|
||||
}
|
||||
})
|
||||
render: function() {
|
||||
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);}}/>
|
||||
</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
|
||||
|
||||
module.exports = {
|
||||
parseEvent: function(client, event, maxHeight, maxWidth) {
|
||||
if (event.content.msgtype == "m.image") {
|
||||
let h = maxHeight
|
||||
let w = maxWidth
|
||||
|
||||
let media_url = client.mxcUrlToHttp(event.content.url)
|
||||
let thumb_url = event.content.url
|
||||
|
||||
if (event.content.info != null) {
|
||||
if (event.content.info.thumbnail_url != null) {
|
||||
thumb_url = event.content.info.thumbnail_url
|
||||
}
|
||||
|
||||
if (event.content.info.thumbnail_info != null) {
|
||||
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
|
||||
} else {
|
||||
h = (event.content.info.h < maxHeight) ? event.content.info.h : h
|
||||
w = (event.content.info.w < maxWidth) ? event.content.info.w : w
|
||||
}
|
||||
}
|
||||
|
||||
thumb_url = client.mxcUrlToHttp(thumb_url, w, h, 'scale', false)
|
||||
|
||||
return {
|
||||
full: media_url,
|
||||
thumb: thumb_url,
|
||||
size: {h, w}
|
||||
}
|
||||
}
|
||||
if (event.content.msgtype == "m.video") {
|
||||
let thumb = null
|
||||
let h = maxHeight
|
||||
let w = maxWidth
|
||||
|
||||
if (event.content.info != null) {
|
||||
if (event.content.info.thumbnail_url != null) {
|
||||
if (event.content.info.thumbnail_info != null) {
|
||||
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
|
||||
}
|
||||
|
||||
thumb = client.mxcUrlToHttp(event.content.thumbnail, w, h, 'scale', false)
|
||||
}
|
||||
}
|
||||
return {
|
||||
full: client.mxcUrlToHttp(event.content.url),
|
||||
thumb: thumb,
|
||||
size: {h, w}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
parseEvent: function(client, event, maxHeight, maxWidth) {
|
||||
if (event.content.msgtype == "m.image") {
|
||||
let h = maxHeight;
|
||||
let w = maxWidth;
|
||||
|
||||
let media_url = client.mxcUrlToHttp(event.content.url);
|
||||
let thumb_url = event.content.url;
|
||||
|
||||
if (event.content.info != null) {
|
||||
if (event.content.info.thumbnail_url != null) {
|
||||
thumb_url = event.content.info.thumbnail_url;
|
||||
}
|
||||
|
||||
if (event.content.info.thumbnail_info != null) {
|
||||
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;
|
||||
} else {
|
||||
h = (event.content.info.h < maxHeight) ? event.content.info.h : h;
|
||||
w = (event.content.info.w < maxWidth) ? event.content.info.w : w;
|
||||
}
|
||||
}
|
||||
|
||||
thumb_url = client.mxcUrlToHttp(thumb_url, w, h, 'scale', false);
|
||||
|
||||
return {
|
||||
full: media_url,
|
||||
thumb: thumb_url,
|
||||
size: {h, w}
|
||||
};
|
||||
}
|
||||
if (event.content.msgtype == "m.video") {
|
||||
let thumb = null;
|
||||
let h = maxHeight;
|
||||
let w = maxWidth;
|
||||
|
||||
if (event.content.info != null) {
|
||||
if (event.content.info.thumbnail_url != null) {
|
||||
if (event.content.info.thumbnail_info != null) {
|
||||
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;
|
||||
}
|
||||
|
||||
thumb = client.mxcUrlToHttp(event.content.thumbnail, w, h, 'scale', false);
|
||||
}
|
||||
}
|
||||
return {
|
||||
full: client.mxcUrlToHttp(event.content.url),
|
||||
thumb: thumb,
|
||||
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')
|
||||
let urllib = require('url')
|
||||
let querystring = require('querystring')
|
||||
"use strict";
|
||||
|
||||
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 = {
|
||||
mxcUrlToHttp: function(url, w, h, method, allowDirectLinks) {
|
||||
let hs = "https://chat.privacytools.io"
|
||||
let mxc = url.slice(6)
|
||||
if (w) {
|
||||
return `${hs}/_matrix/media/v1/thumbnail/${mxc}?w=${w}&h=${h}&method=${method}`
|
||||
} else {
|
||||
return `${hs}/_matrix/media/v1/download/${mxc}`
|
||||
}
|
||||
}
|
||||
}
|
||||
/* FIXME: Verify whether allowDirectLinks is used / expected to do anything by other code */
|
||||
mxcUrlToHttp: function(url, w, h, method, _allowDirectLinks) {
|
||||
let hs = "https://chat.privacytools.io";
|
||||
let mxc = url.slice(6);
|
||||
if (w) {
|
||||
return `${hs}/_matrix/media/v1/thumbnail/${mxc}?w=${w}&h=${h}&method=${method}`;
|
||||
} else {
|
||||
return `${hs}/_matrix/media/v1/download/${mxc}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mockEventTemplate = {
|
||||
type: "m.room.message",
|
||||
sender: "@f0x:privacytools.io",
|
||||
content: {
|
||||
body: "image.png",
|
||||
info: {
|
||||
size: 16692,
|
||||
mimetype: "image/png",
|
||||
thumbnail_info: {
|
||||
w: 268,
|
||||
h: 141,
|
||||
mimetype: "image/png",
|
||||
size: 16896
|
||||
},
|
||||
w: 268,
|
||||
h: 141,
|
||||
thumbnail_url: "mxc://privacytools.io/zBSerdKMhaXSIxfjzCmOnhXH"
|
||||
},
|
||||
msgtype: "m.image",
|
||||
url: "mxc://privacytools.io/khPaFfeRyNdzlSttZraeAUre"
|
||||
},
|
||||
event_id: "$aaa:matrix.org",
|
||||
origin_server_ts: 1558470168199,
|
||||
unsigned: {
|
||||
age: 143237861
|
||||
},
|
||||
room_id: "!aaa:matrix.org"
|
||||
}
|
||||
type: "m.room.message",
|
||||
sender: "@f0x:privacytools.io",
|
||||
content: {
|
||||
body: "image.png",
|
||||
info: {
|
||||
size: 16692,
|
||||
mimetype: "image/png",
|
||||
thumbnail_info: {
|
||||
w: 268,
|
||||
h: 141,
|
||||
mimetype: "image/png",
|
||||
size: 16896
|
||||
},
|
||||
w: 268,
|
||||
h: 141,
|
||||
thumbnail_url: "mxc://privacytools.io/zBSerdKMhaXSIxfjzCmOnhXH"
|
||||
},
|
||||
msgtype: "m.image",
|
||||
url: "mxc://privacytools.io/khPaFfeRyNdzlSttZraeAUre"
|
||||
},
|
||||
event_id: "$aaa:matrix.org",
|
||||
origin_server_ts: 1558470168199,
|
||||
unsigned: {
|
||||
age: 143237861
|
||||
},
|
||||
room_id: "!aaa:matrix.org"
|
||||
};
|
||||
|
||||
describe('media', function() {
|
||||
describe('#parseEvent()', function() {
|
||||
it('event without info', function() {
|
||||
let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate))
|
||||
mockEvent.content.info = null
|
||||
describe('#parseEvent()', function() {
|
||||
it('event without info', function() {
|
||||
let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate));
|
||||
mockEvent.content.info = null;
|
||||
|
||||
checkParsedEvent(mockEvent, {
|
||||
w: 1000,
|
||||
h: 1000,
|
||||
method: 'scale'
|
||||
})
|
||||
}),
|
||||
it('event without thumbnail', function() {
|
||||
let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate))
|
||||
mockEvent.content.info.thumbnail_url = null
|
||||
mockEvent.content.info.thumbnail_info = null
|
||||
checkParsedEvent(mockEvent, {
|
||||
w: 268,
|
||||
h: 141,
|
||||
method: 'scale'
|
||||
})
|
||||
})
|
||||
it('event without thumbnail_info', function() {
|
||||
let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate))
|
||||
mockEvent.content.info.thumbnail_url = null
|
||||
checkParsedEvent(mockEvent, {
|
||||
w: 268,
|
||||
h: 141,
|
||||
method: 'scale'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
checkParsedEvent(mockEvent, {
|
||||
w: 1000,
|
||||
h: 1000,
|
||||
method: 'scale'
|
||||
});
|
||||
});
|
||||
it('event without thumbnail', function() {
|
||||
let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate));
|
||||
mockEvent.content.info.thumbnail_url = null;
|
||||
mockEvent.content.info.thumbnail_info = null;
|
||||
checkParsedEvent(mockEvent, {
|
||||
w: 268,
|
||||
h: 141,
|
||||
method: 'scale'
|
||||
});
|
||||
});
|
||||
it('event without thumbnail_info', function() {
|
||||
let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate));
|
||||
mockEvent.content.info.thumbnail_url = null;
|
||||
checkParsedEvent(mockEvent, {
|
||||
w: 268,
|
||||
h: 141,
|
||||
method: 'scale'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function checkParsedEvent(mockEvent, expected) {
|
||||
let media = mediaLib.parseEvent(client, mockEvent, 1000, 1000)
|
||||
let params = querystring.decode(urllib.parse(media.thumb).query)
|
||||
let media = mediaLib.parseEvent(client, mockEvent, 1000, 1000);
|
||||
let params = querystring.decode(urllib.parse(media.thumb).query);
|
||||
|
||||
Object.keys(params).forEach((key) => {
|
||||
assert.equal(expected[key], params[key])
|
||||
})
|
||||
Object.keys(params).forEach((key) => {
|
||||
assert.equal(expected[key], params[key]);
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue