ESLint auto-fix (spaces -> tabs etc.)

refactor
Sven Slootweg 5 years ago
parent b7f794ffa6
commit aca6f3768d

@ -1,88 +1,88 @@
const gulp = require('gulp') const gulp = require('gulp');
const sass = require('gulp-sass') const sass = require('gulp-sass');
const concat = require('gulp-concat') const concat = require('gulp-concat');
const gutil = require('gulp-util') const gutil = require('gulp-util');
const imagemin = require('gulp-imagemin') const imagemin = require('gulp-imagemin');
const cache = require('gulp-cache') const cache = require('gulp-cache');
const gulpIf = require('gulp-if') const gulpIf = require('gulp-if');
const browserify = require('browserify') const browserify = require('browserify');
const del = require('del') const del = require('del');
const source = require('vinyl-source-stream') const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer') const buffer = require('vinyl-buffer');
const sourcemaps = require('gulp-sourcemaps') const sourcemaps = require('gulp-sourcemaps');
const budo = require('budo') const budo = require('budo');
const babelify = require('babelify') const babelify = require('babelify');
const cssFiles = 'src/scss/**/*.?(s)css' const cssFiles = 'src/scss/**/*.?(s)css';
let css = gulp.src(cssFiles) let css = gulp.src(cssFiles)
.pipe(sass()) .pipe(sass())
.pipe(concat('style.css')) .pipe(concat('style.css'))
.pipe(gulp.dest('build')) .pipe(gulp.dest('build'));
gulp.task('watch', function(cb) { gulp.task('watch', function(cb) {
budo("src/app.js", { budo("src/app.js", {
live: true, live: true,
dir: "build", dir: "build",
port: 3000, port: 3000,
browserify: { browserify: {
transform: babelify transform: babelify
} }
}).on('exit', cb) }).on('exit', cb);
gulp.watch(cssFiles, gulp.series(["sass"])) gulp.watch(cssFiles, gulp.series(["sass"]));
}) });
gulp.task("clean", function(done) { gulp.task("clean", function(done) {
del.sync('build') del.sync('build');
done() done();
}) });
gulp.task("sass", function() { gulp.task("sass", function() {
return gulp.src(cssFiles) return gulp.src(cssFiles)
.pipe(sass()) .pipe(sass())
.pipe(concat('style.css')) .pipe(concat('style.css'))
.pipe(gulp.dest('./build')) .pipe(gulp.dest('./build'));
}) });
gulp.task("assets", function() { gulp.task("assets", function() {
return gulp.src(["src/assets/**/*"]) return gulp.src(["src/assets/**/*"])
.pipe(gulpIf('*.+(png|jpg|jpeg|gif|svg)', .pipe(gulpIf('*.+(png|jpg|jpeg|gif|svg)',
cache(imagemin({ cache(imagemin({
interlaced: true interlaced: true
})) }))
)) ))
.pipe(gulp.dest('build')) .pipe(gulp.dest('build'));
}) });
gulp.task('js', function() { gulp.task('js', function() {
return gulp.src(['src/app.js', "src/components/**/*"]) return gulp.src(['src/app.js', "src/components/**/*"])
.pipe(babel({ .pipe(babel({
presets: [ presets: [
['@babel/env', { ['@babel/env', {
modules: false modules: false
}] }]
] ]
})) }))
.pipe(gulp.dest('build')) .pipe(gulp.dest('build'));
}) });
gulp.task('js', function() { gulp.task('js', function() {
let b = browserify({ let b = browserify({
entries: 'src/app.js', entries: 'src/app.js',
debug: false, debug: false,
transform: [babelify.configure({ transform: [babelify.configure({
presets: ['@babel/preset-env', '@babel/preset-react'] presets: ['@babel/preset-env', '@babel/preset-react']
})] })]
}) });
return b.bundle() return b.bundle()
.pipe(source('src/app.js')) .pipe(source('src/app.js'))
.pipe(buffer()) .pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true })) .pipe(sourcemaps.init({ loadMaps: true }))
.pipe(gulp.dest('build')) .pipe(gulp.dest('build'));
}) });
gulp.task('build', gulp.parallel(['clean', 'assets', 'js', 'sass', function(done) { gulp.task('build', gulp.parallel(['clean', 'assets', 'js', 'sass', function(done) {
done() done();
}])) }]));

@ -1,14 +1,14 @@
'use strict' 'use strict';
const React = require('react') const React = require('react');
const ReactDOM = require('react-dom') const ReactDOM = require('react-dom');
const create = require('create-react-class') const create = require('create-react-class');
const Promise = require('bluebird') const Promise = require('bluebird');
const urllib = require('url') const urllib = require('url');
const sdk = require('matrix-js-sdk') const sdk = require('matrix-js-sdk');
const Sidebar = require('./components/sidebar.js') const Sidebar = require('./components/sidebar.js');
const Login = require('./components/Login.js') const Login = require('./components/Login.js');
const Chat = require('./components/chat.js') const Chat = require('./components/chat.js');
// Things that will get settings: // Things that will get settings:
// colorscheme // colorscheme
@ -16,86 +16,86 @@ const Chat = require('./components/chat.js')
// incoming/outgoing message alignment (split) // incoming/outgoing message alignment (split)
let App = create({ let App = create({
displayName: "App", displayName: "App",
getInitialState: function() { getInitialState: function() {
return { return {
rooms: [], rooms: [],
options: { options: {
fallbackMediaRepos: [] fallbackMediaRepos: []
} }
} };
}, },
componentDidMount: function() { componentDidMount: function() {
//check if accessToken is stored in localStorage //check if accessToken is stored in localStorage
let accessToken = localStorage.getItem('accessToken') let accessToken = localStorage.getItem('accessToken');
if (localStorage.accessToken != undefined) { if (localStorage.accessToken != undefined) {
let userId = localStorage.getItem('userId') let userId = localStorage.getItem('userId');
let apiUrl = localStorage.getItem('apiUrl') let apiUrl = localStorage.getItem('apiUrl');
this.loginCallback(userId, accessToken, apiUrl, true) this.loginCallback(userId, accessToken, apiUrl, true);
} }
}, },
loginCallback: function(userId, accessToken, apiUrl, restored) { loginCallback: function(userId, accessToken, apiUrl, restored) {
if (restored) { if (restored) {
console.log("Restoring from localStorage") console.log("Restoring from localStorage");
} else { } else {
userId = '@' + userId.replace('@', '') userId = '@' + userId.replace('@', '');
localStorage.setItem('userId', userId) localStorage.setItem('userId', userId);
localStorage.setItem('accessToken', accessToken) localStorage.setItem('accessToken', accessToken);
localStorage.setItem('apiUrl', apiUrl) localStorage.setItem('apiUrl', apiUrl);
} }
let client = sdk.createClient({ let client = sdk.createClient({
baseUrl: apiUrl, baseUrl: apiUrl,
accessToken: accessToken, accessToken: accessToken,
userId: userId userId: userId
}); });
this.setState({ this.setState({
client: client client: client
}) });
this.startClient(client) this.startClient(client);
}, },
startClient: function(client) { startClient: function(client) {
console.log(client) console.log(client);
client.on("sync", (state, prevState, data) => { client.on("sync", (state, prevState, data) => {
if (state == "ERROR") { if (state == "ERROR") {
} else if (state == "SYNCING") { } else if (state == "SYNCING") {
let rooms = {} let rooms = {};
client.getRooms().forEach((room) => { client.getRooms().forEach((room) => {
rooms[room.roomId] = room rooms[room.roomId] = room;
}) });
this.setState({rooms: rooms}) this.setState({rooms: rooms});
} else if (state == "PREPARED") { } else if (state == "PREPARED") {
} }
}) });
client.on("Room.localEchoUpdated", (event) => { client.on("Room.localEchoUpdated", (event) => {
let rooms = {} let rooms = {};
client.getRooms().forEach((room) => { client.getRooms().forEach((room) => {
rooms[room.roomId] = room rooms[room.roomId] = room;
}) });
this.setState({rooms: rooms}) this.setState({rooms: rooms});
}) });
client.startClient() client.startClient();
}, },
render: function() { render: function() {
if (this.state.client == undefined) { if (this.state.client == undefined) {
//Login screen //Login screen
return <Login callback={this.loginCallback}/> return <Login callback={this.loginCallback}/>;
} }
return ( return (
<> <>
<Sidebar options={this.state.options} client={this.state.client} rooms={this.state.rooms} selectRoom={(roomId) => {this.setState({roomId: roomId})}}/> <Sidebar options={this.state.options} client={this.state.client} rooms={this.state.rooms} selectRoom={(roomId) => {this.setState({roomId: roomId});}}/>
<Chat client={this.state.client} roomId={this.state.roomId}/> <Chat client={this.state.client} roomId={this.state.roomId}/>
</> </>
) );
} }
}) });
ReactDOM.render( ReactDOM.render(
<App />, <App />,
document.getElementById('root') document.getElementById('root')
) );

@ -1,228 +1,228 @@
'use strict' 'use strict';
const React = require('react') const React = require('react');
const ReactDOM = require('react-dom') const ReactDOM = require('react-dom');
const create = require('create-react-class') const create = require('create-react-class');
const Promise = require('bluebird') const Promise = require('bluebird');
const urllib = require('url') const urllib = require('url');
const debounce = require('debounce') const debounce = require('debounce');
const defaultValue = require('default-value') const defaultValue = require('default-value');
let login = create({ let login = create({
displayName: "Login", displayName: "Login",
getInitialState: function() { getInitialState: function() {
return { return {
error: null, error: null,
formState: { formState: {
user: "", user: "",
pass: "", pass: "",
hs: "" hs: ""
}, },
hs: { hs: {
prompt: false, prompt: false,
error: null, error: null,
valid: false valid: false
} }
} };
}, },
login: function() { login: function() {
this.setState({error: ""}) this.setState({error: ""});
if (this.state.hs.valid) { if (this.state.hs.valid) {
return this.doLogin() return this.doLogin();
} }
let parts = this.state.formState.user.split(':') let parts = this.state.formState.user.split(':');
if (parts.length != 2) { if (parts.length != 2) {
return this.setState({error: "Please enter a full mxid, like username:homeserver.tld"}) return this.setState({error: "Please enter a full mxid, like username:homeserver.tld"});
} }
let hostname = urllib.parse("https://" + parts[1]) let hostname = urllib.parse("https://" + parts[1]);
getApiServer(hostname).then((hs) => { getApiServer(hostname).then((hs) => {
console.log("Using API server", hs) console.log("Using API server", hs);
let formState = this.state.formState let formState = this.state.formState;
formState.user = parts[0] formState.user = parts[0];
formState.hs = hs formState.hs = hs;
let hsState = Object.assign(this.state.hs, {valid: true}) let hsState = Object.assign(this.state.hs, {valid: true});
this.setState({apiUrl: hs, formState: formState, hs: hsState}) this.setState({apiUrl: hs, formState: formState, hs: hsState});
this.doLogin() this.doLogin();
}).catch((error) => { }).catch((error) => {
console.log("ERROR fetching homeserver url", error) console.log("ERROR fetching homeserver url", error);
let hsState = Object.assign(this.state.hs, {error: error, valid: false, prompt: true}) let hsState = Object.assign(this.state.hs, {error: error, valid: false, prompt: true});
this.setState({hs: hsState}) this.setState({hs: hsState});
}) });
}, },
doLogin: function() { doLogin: function() {
console.log("Logging in") console.log("Logging in");
let user = this.state.formState.user.replace('@', '') let user = this.state.formState.user.replace('@', '');
let password = this.state.formState.pass let password = this.state.formState.pass;
let hs = this.state.apiUrl let hs = this.state.apiUrl;
let data = { let data = {
user: user, user: user,
password: password, password: password,
type: "m.login.password", type: "m.login.password",
initial_device_display_name: "Neo v4", initial_device_display_name: "Neo v4",
}; };
let url = hs + "/_matrix/client/r0/login" let url = hs + "/_matrix/client/r0/login";
fetch(url, { fetch(url, {
body: JSON.stringify(data), body: JSON.stringify(data),
headers: { headers: {
'content-type': 'application/json' 'content-type': 'application/json'
}, },
method: 'POST', method: 'POST',
}).then((response) => response.json()) }).then((response) => response.json())
.then((responseJson) => { .then((responseJson) => {
console.log("got access token", responseJson) console.log("got access token", responseJson);
this.setState({json: responseJson}) this.setState({json: responseJson});
if(responseJson.access_token != undefined) { if(responseJson.access_token != undefined) {
this.props.callback(responseJson.user_id, responseJson.access_token, hs) this.props.callback(responseJson.user_id, responseJson.access_token, hs);
} else { } else {
this.setState({error: responseJson.error}) this.setState({error: responseJson.error});
} }
}) })
.catch((error) => { .catch((error) => {
console.error(url, error); console.error(url, error);
}); });
}, },
handleUserChange: function(e) { handleUserChange: function(e) {
let formState = this.state.formState let formState = this.state.formState;
let user = e.target.value let user = e.target.value;
formState.user = e.target.value formState.user = e.target.value;
let parts = user.split(':') let parts = user.split(':');
if (parts.length == 2) { if (parts.length == 2) {
formState.hs = parts[1] formState.hs = parts[1];
let hsState = Object.assign(this.state.hs, {error: null, valid: false}) let hsState = Object.assign(this.state.hs, {error: null, valid: false});
this.setState({hs: hsState}) this.setState({hs: hsState});
} }
this.setState({formState: formState}) this.setState({formState: formState});
}, },
handlePassChange: function(e) { handlePassChange: function(e) {
let formState = this.state.formState let formState = this.state.formState;
formState.pass = e.target.value formState.pass = e.target.value;
this.setState({formState: formState}) this.setState({formState: formState});
}, },
handleHsChange: function(e) { handleHsChange: function(e) {
let formState = this.state.formState let formState = this.state.formState;
formState.hs = e.target.value formState.hs = e.target.value;
this.setState({formState: formState}) this.setState({formState: formState});
this.setState({hs: {error: null, valid: false, prompt: true, changed: true}}) this.setState({hs: {error: null, valid: false, prompt: true, changed: true}});
}, },
render: function() { render: function() {
let hsState = "inactive" let hsState = "inactive";
if (this.state.hs.prompt) { if (this.state.hs.prompt) {
hsState = "active" hsState = "active";
} }
if (this.state.hs.error != null) { if (this.state.hs.error != null) {
hsState = "error" hsState = "error";
} }
if (this.state.hs.valid) { if (this.state.hs.valid) {
hsState = "validated" hsState = "validated";
} }
return ( return (
<div className="loginwrapper"> <div className="loginwrapper">
<img src="./neo.png"/> <img src="./neo.png"/>
<div className="errorMessage">{this.state.error}</div> <div className="errorMessage">{this.state.error}</div>
<div className="login"> <div className="login">
<label htmlFor="user">Username: </label> <label htmlFor="user">Username: </label>
<input type="text" id="user" placeholder="@user:homeserver.tld" value={this.state.formState["user"]} onChange={this.handleUserChange}/> <input type="text" id="user" placeholder="@user:homeserver.tld" value={this.state.formState["user"]} onChange={this.handleUserChange}/>
<label htmlFor="pass">Password: </label> <label htmlFor="pass">Password: </label>
<input type="password" id="pass" placeholder="password" value={this.state.formState["pass"]} onChange={this.handlePassChange}/> <input type="password" id="pass" placeholder="password" value={this.state.formState["pass"]} onChange={this.handlePassChange}/>
<label htmlFor="hs" className={hsState}>Homeserver: </label> <label htmlFor="hs" className={hsState}>Homeserver: </label>
{this.state.hs.prompt ? ( {this.state.hs.prompt ? (
<> <>
<input type="text" id="hs" value={this.state.formState["hs"]} onChange={this.handleHsChange}/> <input type="text" id="hs" value={this.state.formState["hs"]} onChange={this.handleHsChange}/>
</> </>
) : ( ) : (
<span id="hs">{this.state.formState["hs"]}</span> <span id="hs">{this.state.formState["hs"]}</span>
)} )}
<button onClick={()=>this.login()}>Log in</button> <button onClick={()=>this.login()}>Log in</button>
</div> </div>
</div> </div>
) );
} }
}) });
function getApiServer(hostname) { function getApiServer(hostname) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
console.log("Checking for api server from mxid", urllib.format(hostname)) console.log("Checking for api server from mxid", urllib.format(hostname));
checkApi(hostname).then(() => { checkApi(hostname).then(() => {
// Hostname is a valid api server // Hostname is a valid api server
hostname.pathname = "" hostname.pathname = "";
resolve(urllib.format(hostname)) resolve(urllib.format(hostname));
}).catch(() => { }).catch(() => {
console.log("trying .well-known") console.log("trying .well-known");
tryWellKnown(hostname).then((hostname) => { tryWellKnown(hostname).then((hostname) => {
console.log("got .well-known host", hostname) console.log("got .well-known host", hostname);
resolve(hostname) resolve(hostname);
}).catch((err) => { }).catch((err) => {
reject("Fatal error trying to get API host") reject("Fatal error trying to get API host");
}) });
}) });
}) });
} }
function checkApi(host) { function checkApi(host) {
let versionUrl = buildUrl(host, "/_matrix/client/versions") let versionUrl = buildUrl(host, "/_matrix/client/versions");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fetch(versionUrl).then((response) => { fetch(versionUrl).then((response) => {
if (response.status != 200) { if (response.status != 200) {
console.log("Invalid homeserver url", versionUrl) console.log("Invalid homeserver url", versionUrl);
return reject() return reject();
} }
resolve() resolve();
}).catch((err) => { }).catch((err) => {
reject(err) reject(err);
}) });
}) });
} }
function tryWellKnown(host) { function tryWellKnown(host) {
let wellKnownUrl = urllib.format(Object.assign(host, { let wellKnownUrl = urllib.format(Object.assign(host, {
pathname: "/.well-known/matrix/client" pathname: "/.well-known/matrix/client"
})) }));
console.log("Trying", wellKnownUrl, "for .well-known") console.log("Trying", wellKnownUrl, "for .well-known");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
return fetch(wellKnownUrl) return fetch(wellKnownUrl)
.then((response) => { .then((response) => {
if (response.status != 200) { if (response.status != 200) {
console.log("no well-known in use") console.log("no well-known in use");
reject("No homeserver found") reject("No homeserver found");
} }
return response return response;
}).catch((error) => { }).catch((error) => {
reject("can't fetch .well-known") reject("can't fetch .well-known");
}) })
.then((response) => response.json()) .then((response) => response.json())
.then((json) => { .then((json) => {
console.log("Parsed json", json) console.log("Parsed json", json);
if (json['m.homeserver'] != undefined && json['m.homeserver'].base_url != undefined) { if (json['m.homeserver'] != undefined && json['m.homeserver'].base_url != undefined) {
resolve(json['m.homeserver'].base_url) resolve(json['m.homeserver'].base_url);
} }
}) })
.catch((err) => { .catch((err) => {
console.log("Error in json", err) console.log("Error in json", err);
reject("Error while parsing .well-known") reject("Error while parsing .well-known");
}) });
}) });
} }
function buildUrl(host, path) { function buildUrl(host, path) {
return urllib.format(Object.assign(host, { return urllib.format(Object.assign(host, {
pathname: path pathname: path
})) }));
} }
module.exports = login module.exports = login;

@ -1,227 +1,227 @@
'use strict' 'use strict';
const React = require('react') const React = require('react');
const ReactDOM = require('react-dom') const ReactDOM = require('react-dom');
const create = require('create-react-class') const create = require('create-react-class');
const Promise = require('bluebird') const Promise = require('bluebird');
const debounce = require('debounce') const debounce = require('debounce');
const jdenticon = require('jdenticon') const jdenticon = require('jdenticon');
const defaultValue = require('default-value') const defaultValue = require('default-value');
const sdk = require('matrix-js-sdk') const sdk = require('matrix-js-sdk');
const sanitize = require('sanitize-html') const sanitize = require('sanitize-html');
const Event = require('./events/Event.js') const Event = require('./events/Event.js');
const Info = require('./info.js') const Info = require('./info.js');
const Input = require('./input.js') const Input = require('./input.js');
const User = require('./events/user.js') const User = require('./events/user.js');
const Loading = require('./loading.js') const Loading = require('./loading.js');
jdenticon.config = { jdenticon.config = {
lightness: { lightness: {
color: [0.58, 0.66], color: [0.58, 0.66],
grayscale: [0.30, 0.90] grayscale: [0.30, 0.90]
}, },
saturation: { saturation: {
color: 0.66, color: 0.66,
grayscale: 0.00 grayscale: 0.00
}, },
backColor: "#00000000" backColor: "#00000000"
}; };
let eventFunctions = { let eventFunctions = {
plaintext: function() { plaintext: function() {
let plain = "unknown event" let plain = "unknown event";
if (this.type == "m.room.message") { if (this.type == "m.room.message") {
plain = this.content.body plain = this.content.body;
if (this.content.format == "org.matrix.custom.html") { if (this.content.format == "org.matrix.custom.html") {
plain = sanitize(this.content.formatted_body, {allowedTags: []}) plain = sanitize(this.content.formatted_body, {allowedTags: []});
} }
} }
if (this.type == "m.room.member") { if (this.type == "m.room.member") {
if (this.content.membership == "invite") { if (this.content.membership == "invite") {
plain = `${this.sender} invited ${this.state_key}` plain = `${this.sender} invited ${this.state_key}`;
} else if (this.content.membership == "join") { } else if (this.content.membership == "join") {
plain = `${this.state_key} joined the room` plain = `${this.state_key} joined the room`;
} else if (this.content.membership == "leave") { } else if (this.content.membership == "leave") {
plain = `${this.state_key} left the room` plain = `${this.state_key} left the room`;
} else if (this.content.membership == "kick") { } else if (this.content.membership == "kick") {
plain = `${this.sender} kicked ${this.state_key}` plain = `${this.sender} kicked ${this.state_key}`;
} else if (this.content.membership == "ban") { } else if (this.content.membership == "ban") {
plain = `${this.sender} banned ${this.state_key}` plain = `${this.sender} banned ${this.state_key}`;
} }
} }
if (this.type == "m.room.avatar") { if (this.type == "m.room.avatar") {
if (this.content.url.length > 0) { if (this.content.url.length > 0) {
plain = `${this.sender} changed the room avatar` plain = `${this.sender} changed the room avatar`;
} }
} }
if (this.type == "m.room.name") { if (this.type == "m.room.name") {
return `${this.sender} changed the room name to ${this.content.name}` return `${this.sender} changed the room name to ${this.content.name}`;
} }
return plain return plain;
} }
} };
let chat = create({ let chat = create({
displayName: "Chat", displayName: "Chat",
getInitialState: function() { getInitialState: function() {
return { return {
ref: null, ref: null,
loading: false loading: false
} };
}, },
getSnapshotBeforeUpdate: function(oldProps, oldState) { getSnapshotBeforeUpdate: function(oldProps, oldState) {
let ref = this.state.ref let ref = this.state.ref;
if (ref == null) {return null} if (ref == null) {return null;}
if ((ref.scrollHeight - ref.offsetHeight) - ref.scrollTop < 100) { // Less than 100px from bottom if ((ref.scrollHeight - ref.offsetHeight) - ref.scrollTop < 100) { // Less than 100px from bottom
return true return true;
} }
return null return null;
}, },
componentDidUpdate(prevProps, prevState, snapshot) { componentDidUpdate(prevProps, prevState, snapshot) {
let ref = this.state.ref let ref = this.state.ref;
if (ref == null) {return} if (ref == null) {return;}
if (snapshot) { // scroll to bottom if (snapshot) { // scroll to bottom
ref.scrollTop = (ref.scrollHeight - ref.offsetHeight) ref.scrollTop = (ref.scrollHeight - ref.offsetHeight);
} }
}, },
setRef: function(ref) { setRef: function(ref) {
if (ref != null) { if (ref != null) {
this.setState({ref: ref}) this.setState({ref: ref});
} }
}, },
onReplyClick: function(e) { onReplyClick: function(e) {
this.setState({replyEvent: e}) this.setState({replyEvent: e});
}, },
paginateBackwards: function() { paginateBackwards: function() {
if (this.state.loading) { if (this.state.loading) {
return return;
} }
let client = this.props.client let client = this.props.client;
client.paginateEventTimeline(client.getRoom(this.props.roomId).getLiveTimeline(), {backwards: true}).then(() => { client.paginateEventTimeline(client.getRoom(this.props.roomId).getLiveTimeline(), {backwards: true}).then(() => {
this.setState({loading: false}) this.setState({loading: false});
}) });
this.setState({loading: true}) this.setState({loading: true});
}, },
render: function() { render: function() {
let client = this.props.client let client = this.props.client;
let empty = ( let empty = (
<div className="main"> <div className="main">
</div> </div>
) );
if (this.props.roomId == undefined) { if (this.props.roomId == undefined) {
//empty screen //empty screen
return empty return empty;
} }
let room = client.getRoom(this.props.roomId) let room = client.getRoom(this.props.roomId);
if (room == null) { if (room == null) {
return empty return empty;
} }
let messageGroups = { let messageGroups = {
current: [], current: [],
groups: [], groups: [],
sender: "", sender: "",
type: "" type: ""
} };
// if the sender is the same, add it to the 'current' messageGroup, if not, // 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. // push the old one to 'groups' and start with a new array.
let liveTimeline = room.getLiveTimeline() let liveTimeline = room.getLiveTimeline();
let liveTimelineEvents = liveTimeline.getEvents() let liveTimelineEvents = liveTimeline.getEvents();
let events = [] let events = [];
if (liveTimelineEvents.length > 0) { if (liveTimelineEvents.length > 0) {
liveTimelineEvents.forEach((MatrixEvent) => { liveTimelineEvents.forEach((MatrixEvent) => {
let event = MatrixEvent.event; let event = MatrixEvent.event;
event = Object.assign(event, eventFunctions) event = Object.assign(event, eventFunctions);
if (event.sender == null) { // localecho messages if (event.sender == null) { // localecho messages
event.sender = event.user_id event.sender = event.user_id;
event.local = true event.local = true;
} }
if (event.sender != messageGroups.sender || event.type != messageGroups.type) { if (event.sender != messageGroups.sender || event.type != messageGroups.type) {
messageGroups.sender = event.sender messageGroups.sender = event.sender;
messageGroups.type = event.type messageGroups.type = event.type;
if (messageGroups.current.length != 0) { if (messageGroups.current.length != 0) {
messageGroups.groups.push(messageGroups.current) messageGroups.groups.push(messageGroups.current);
} }
messageGroups.current = [] messageGroups.current = [];
} }
messageGroups.current.push(event) messageGroups.current.push(event);
}) });
messageGroups.groups.push(messageGroups.current) messageGroups.groups.push(messageGroups.current);
events = messageGroups.groups.map((events, id) => { 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}/> 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 //TODO: replace with something that only renders events in view
return ( return (
<div className="main"> <div className="main">
<Info room={room} /> <Info room={room} />
<div className="chat" ref={this.setRef}> <div className="chat" ref={this.setRef}>
<div className="events"> <div className="events">
<div className="paginateBackwards" onClick={this.paginateBackwards}> <div className="paginateBackwards" onClick={this.paginateBackwards}>
{this.state.loading ? {this.state.loading ?
<Loading/> : <Loading/> :
<span>load older messages</span> <span>load older messages</span>
} }
</div> </div>
{events} {events}
</div> </div>
</div> </div>
<Input client={client} roomId={this.props.roomId} replyEvent={this.state.replyEvent} onReplyClick={this.onReplyClick}/> <Input client={client} roomId={this.props.roomId} replyEvent={this.state.replyEvent} onReplyClick={this.onReplyClick}/>
</div> </div>
) );
} }
}) });
let EventGroup = create({ let EventGroup = create({
displayName: "EventGroup", displayName: "EventGroup",
getInitialState: function() { getInitialState: function() {
let user = this.props.client.getUser(this.props.events[0].sender) let user = this.props.client.getUser(this.props.events[0].sender);
let avatar = <svg id="avatar" ref={this.avatarRef} /> let avatar = <svg id="avatar" ref={this.avatarRef} />;
if (user.avatarUrl != null) { if (user.avatarUrl != null) {
let hs = this.props.client.baseUrl let hs = this.props.client.baseUrl;
let media_mxc = user.avatarUrl.slice(6) let media_mxc = user.avatarUrl.slice(6);
let url = `${hs}/_matrix/media/v1/thumbnail/${media_mxc}?width=128&height=128&method=scale` let url = `${hs}/_matrix/media/v1/thumbnail/${media_mxc}?width=128&height=128&method=scale`;
avatar = <img id="avatar" src={url}/> avatar = <img id="avatar" src={url}/>;
} }
return { return {
user: user, user: user,
avatar: avatar avatar: avatar
} };
}, },
avatarRef: function(ref) { avatarRef: function(ref) {
jdenticon.update(ref, this.state.user.userId) jdenticon.update(ref, this.state.user.userId);
}, },
render: function() { render: function() {
let events = this.props.events.map((event, key) => { 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 <Event event={event} key={key} client={this.props.client} room={this.props.room} onReplyClick={this.props.onReplyClick}/>;
}) });
return <div className="eventGroup"> return <div className="eventGroup">
{this.state.avatar} {this.state.avatar}
<div className="col"> <div className="col">
<User user={this.state.user}/> <User user={this.state.user}/>
{events} {events}
</div> </div>
</div> </div>;
} }
}) });
module.exports = chat module.exports = chat;

@ -1,115 +1,115 @@
'use strict' 'use strict';
const React = require('react') const React = require('react');
const ReactDOM = require('react-dom') const ReactDOM = require('react-dom');
const create = require('create-react-class') const create = require('create-react-class');
const defaultValue = require('default-value') const defaultValue = require('default-value');
const riot = require('../../lib/riot-utils.js') const riot = require('../../lib/riot-utils.js');
const User = require('./user.js') const User = require('./user.js');
const stateElement = require('./state.js') const stateElement = require('./state.js');
const elements = { const elements = {
"m.text": require('./text.js'), "m.text": require('./text.js'),
"m.image": require('./image.js'), "m.image": require('./image.js'),
"m.video": require('./video.js') "m.video": require('./video.js')
} };
const mxReplyRegex = /^<mx-reply>[\s\S]+<\/mx-reply>/ const mxReplyRegex = /^<mx-reply>[\s\S]+<\/mx-reply>/;
let Event = create({ let Event = create({
displayName: "Event", displayName: "Event",
render: function() { render: function() {
let event = this.props.event let event = this.props.event;
let state = "" let state = "";
let reply = "" let reply = "";
let element = "unsupported event: " + event.type let element = "unsupported event: " + event.type;
if (event.local) { if (event.local) {
state = " local" state = " local";
} }
if (event.type == "m.room.message") { if (event.type == "m.room.message") {
let msgtype = event.content.msgtype; let msgtype = event.content.msgtype;
let formattedEvent = parseEvent(event) let formattedEvent = parseEvent(event);
let parsedReply = formattedEvent.parsedReply let parsedReply = formattedEvent.parsedReply;
if (parsedReply.isReply) { if (parsedReply.isReply) {
let repliedEvent = this.props.room.findEventById(parsedReply.to) let repliedEvent = this.props.room.findEventById(parsedReply.to);
let shortText, repliedUser let shortText, repliedUser;
if (repliedEvent == undefined) { if (repliedEvent == undefined) {
shortText = "Can't load this event" shortText = "Can't load this event";
repliedUser = {userId: "NEO_UNKNOWN", displayName: "Unknown User"} repliedUser = {userId: "NEO_UNKNOWN", displayName: "Unknown User"};
// fall back on <mx-reply> content? // fall back on <mx-reply> content?
} else { } else {
repliedUser = this.props.client.getUser(repliedEvent.event.sender) repliedUser = this.props.client.getUser(repliedEvent.event.sender);
shortText = parseEvent(repliedEvent.event) shortText = parseEvent(repliedEvent.event);
if (shortText.html) { if (shortText.html) {
shortText = <span dangerouslySetInnerHTML={{__html: shortText.body}}/> shortText = <span dangerouslySetInnerHTML={{__html: shortText.body}}/>;
} else { } else {
shortText = shortText.body shortText = shortText.body;
} }
} }
reply = ( reply = (
<div className="reply"> <div className="reply">
<User user={repliedUser}/> <User user={repliedUser}/>
{shortText} {shortText}
</div> </div>
) );
} }
element = React.createElement(defaultValue(elements[msgtype], elements["m.text"]), {formattedEvent: formattedEvent, event: event, client: this.props.client}) element = React.createElement(defaultValue(elements[msgtype], elements["m.text"]), {formattedEvent: formattedEvent, event: event, client: this.props.client});
} else if (["m.room.name", "m.room.member", "m.room.avatar"].includes(event.type)) { } else if (["m.room.name", "m.room.member", "m.room.avatar"].includes(event.type)) {
element = React.createElement(stateElement, {event: event}) element = React.createElement(stateElement, {event: event});
} }
return ( return (
<div className={"event" + state} onClick={() => { <div className={"event" + state} onClick={() => {
this.props.onReplyClick(event) this.props.onReplyClick(event);
console.log(event) console.log(event);
}}> }}>
{reply} {reply}
{element} {element}
</div> </div>
) );
} }
}) });
function parseEvent(event, context) { function parseEvent(event, context) {
// context can be either 'main' or 'reply' // context can be either 'main' or 'reply'
let body = event.content.body let body = event.content.body;
let html = false let html = false;
if (event.content.format == "org.matrix.custom.html") { if (event.content.format == "org.matrix.custom.html") {
body = riot.sanitize(event.content.formatted_body) body = riot.sanitize(event.content.formatted_body);
html = true html = true;
} }
if (body) { if (body) {
body = body.trim() body = body.trim();
} }
let parsedReply = parseReply(event, body) let parsedReply = parseReply(event, body);
if (parsedReply.isReply) { if (parsedReply.isReply) {
// body with fallback stripped // body with fallback stripped
body = parsedReply.body body = parsedReply.body;
} }
return {body: body, parsedReply: parsedReply, html: html} return {body: body, parsedReply: parsedReply, html: html};
} }
function parseReply(event, body) { function parseReply(event, body) {
let replyTo let replyTo;
try { try {
replyTo = event.content['m.relates_to']['m.in_reply_to'].event_id replyTo = event.content['m.relates_to']['m.in_reply_to'].event_id;
if (replyTo) { if (replyTo) {
// strip <mx-reply> from message if it exists // strip <mx-reply> from message if it exists
body = body.replace(mxReplyRegex, "") body = body.replace(mxReplyRegex, "");
} }
} catch(err) { } catch(err) {
// no reply // no reply
return {isReply: false} return {isReply: false};
} }
return {isReply: true, body: body, to: replyTo} return {isReply: true, body: body, to: replyTo};
} }
module.exports = Event module.exports = Event;

@ -1,44 +1,44 @@
'use strict' 'use strict';
const React = require('react') const React = require('react');
const ReactDOM = require('react-dom') const ReactDOM = require('react-dom');
const create = require('create-react-class') const create = require('create-react-class');
const Promise = require('bluebird') const Promise = require('bluebird');
const defaultValue = require('default-value') const defaultValue = require('default-value');
const mediaLib = require('../../lib/media.js') const mediaLib = require('../../lib/media.js');
const Text = require('./text.js') const Text = require('./text.js');
let Event = create({ let Event = create({
displayName: "m.image", displayName: "m.image",
getInitialState: function() { getInitialState: function() {
let event = this.props.event let event = this.props.event;
if (event.content.url == undefined) { if (event.content.url == undefined) {
return null return null;
} }
return mediaLib.parseEvent(this.props.client, event, 1000, 1000) return mediaLib.parseEvent(this.props.client, event, 1000, 1000);
}, },
updateSize: function(e) { updateSize: function(e) {
console.log("image was loaded") console.log("image was loaded");
}, },
render: function() { render: function() {
let event = this.props.event let event = this.props.event;
if (this.state == null) { if (this.state == null) {
return "malformed image event: " + event.content.body return "malformed image event: " + event.content.body;
} }
return ( return (
<div className="body"> <div className="body">
<a href={this.state.full} target="_blank"> <a href={this.state.full} target="_blank">
<img src={this.state.thumb} style={{maxHeight: this.state.size.h, maxWidth: this.state.size.w}}/> <img src={this.state.thumb} style={{maxHeight: this.state.size.h, maxWidth: this.state.size.w}}/>
</a> </a>
</div> </div>
) );
} }
}) });
module.exports = Event; module.exports = Event;

@ -1,19 +1,19 @@
'use strict' 'use strict';
const React = require('react') const React = require('react');
const ReactDOM = require('react-dom') const ReactDOM = require('react-dom');
const create = require('create-react-class') const create = require('create-react-class');
let Event = create({ let Event = create({
displayName: "genericStateEvent", displayName: "genericStateEvent",
render: function() { render: function() {
let event = this.props.event let event = this.props.event;
return ( return (
<div className="body"> <div className="body">
{event.plaintext()} {event.plaintext()}
</div> </div>
) );
} }
}) });
module.exports = Event module.exports = Event;

@ -1,43 +1,43 @@
'use strict' 'use strict';
const React = require('react') const React = require('react');
const ReactDOM = require('react-dom') const ReactDOM = require('react-dom');
const create = require('create-react-class') const create = require('create-react-class');
const Promise = require('bluebird') const Promise = require('bluebird');
const riot = require('../../lib/riot-utils.js') const riot = require('../../lib/riot-utils.js');
let Event = create({ let Event = create({
displayName: "m.text", displayName: "m.text",
render: function() { render: function() {
let event = this.props.event let event = this.props.event;
let formattedEvent = this.props.formattedEvent let formattedEvent = this.props.formattedEvent;
let eventBody let eventBody;
if (formattedEvent.html) { if (formattedEvent.html) {
eventBody = <div eventBody = <div
className="body" className="body"
dangerouslySetInnerHTML={{__html: formattedEvent.body}} dangerouslySetInnerHTML={{__html: formattedEvent.body}}
/> />;
} else { } else {
eventBody = eventBody =
<div className="body"> <div className="body">
{formattedEvent.body} {formattedEvent.body}
</div> </div>;
} }
let eventClass = "" let eventClass = "";
if (event.local) { if (event.local) {
eventClass += " local" eventClass += " local";
} }
return <div className={eventClass}> return <div className={eventClass}>
{eventBody} {eventBody}
</div> </div>;
} }
}) });
module.exports = Event module.exports = Event;

@ -1,49 +1,49 @@
'use strict' 'use strict';
const React = require('react') const React = require('react');
const ReactDOM = require('react-dom') const ReactDOM = require('react-dom');
const create = require('create-react-class') const create = require('create-react-class');
const jdenticon = require('jdenticon') const jdenticon = require('jdenticon');
jdenticon.config = { jdenticon.config = {
lightness: { lightness: {
color: [0.58, 0.66], color: [0.58, 0.66],
grayscale: [0.30, 0.90] grayscale: [0.30, 0.90]
}, },
saturation: { saturation: {
color: 0.66, color: 0.66,
grayscale: 0.00 grayscale: 0.00
}, },
backColor: "#00000000" backColor: "#00000000"
}; };
let User = create({ let User = create({
displayName: "user", displayName: "user",
getInitialState: function() { getInitialState: function() {
let icon = jdenticon.toSvg(this.props.user.userId, 200) let icon = jdenticon.toSvg(this.props.user.userId, 200);
let match = icon.match(/#([a-f0-9]{6})/g) let match = icon.match(/#([a-f0-9]{6})/g);
let color = '#ff0000' let color = '#ff0000';
for(let i=match.length-1; i>= 0; i--) { for(let i=match.length-1; i>= 0; i--) {
color = match[i] color = match[i];
let r = color.substr(1, 2) let r = color.substr(1, 2);
let g = color.substr(3, 2) let g = color.substr(3, 2);
let b = color.substr(5, 2) let b = color.substr(5, 2);
if (r != g && g != b) { // not greyscale if (r != g && g != b) { // not greyscale
break break;
} }
} }
return { return {
color: color color: color
} };
}, },
render: function() { render: function() {
return ( return (
<div className="user" style={{color: this.state.color}}> <div className="user" style={{color: this.state.color}}>
{this.props.user.displayName} {this.props.user.displayName}
</div> </div>
) );
} }
}) });
module.exports = User module.exports = User;

@ -1,40 +1,40 @@
'use strict' 'use strict';
const React = require('react') const React = require('react');
const ReactDOM = require('react-dom') const ReactDOM = require('react-dom');
const create = require('create-react-class') const create = require('create-react-class');
const Promise = require('bluebird') const Promise = require('bluebird');
const defaultValue = require('default-value') const defaultValue = require('default-value');
const mediaLib = require('../../lib/media.js') const mediaLib = require('../../lib/media.js');
const Text = require('./text.js') const Text = require('./text.js');
let Event = create({ let Event = create({
displayName: "m.video", displayName: "m.video",
getInitialState: function() { getInitialState: function() {
let event = this.props.event let event = this.props.event;
if (event.content.url == undefined) { if (event.content.url == undefined) {
return null return null;
} }
return mediaLib.parseEvent(this.props.client, event, 1000, 1000) return mediaLib.parseEvent(this.props.client, event, 1000, 1000);
}, },
render: function() { render: function() {
let event = this.props.event let event = this.props.event;
if (this.state == null) { if (this.state == null) {
return "malformed video event: " + event.content.body return "malformed video event: " + event.content.body;
} }
return ( return (
<div className="body"> <div className="body">
<video controls poster={this.state.thumb} style={{maxHeight: this.state.size.h, maxWidth: this.state.size.w}}> <video controls poster={this.state.thumb} style={{maxHeight: this.state.size.h, maxWidth: this.state.size.w}}>
<source src={this.state.full}></source> <source src={this.state.full}></source>
</video> </video>
</div> </div>
) );
} }
}) });
module.exports = Event; module.exports = Event;

@ -1,46 +1,46 @@
'use strict' 'use strict';
const React = require('react') const React = require('react');
const ReactDOM = require('react-dom') const ReactDOM = require('react-dom');
const create = require('create-react-class') const create = require('create-react-class');
let fileUpload = create({ let fileUpload = create({
displayName: "FileUpload", displayName: "FileUpload",
setFileRef: function(e) { setFileRef: function(e) {
if (e != null) { if (e != null) {
e.addEventListener('change', this.startUpload) e.addEventListener('change', this.startUpload);
this.setState({ this.setState({
fileRef: e fileRef: e
}) });
} }
}, },
startUpload: function(e) { startUpload: function(e) {
Array.from(e.target.files).forEach((file) => { Array.from(e.target.files).forEach((file) => {
if (file.type.startsWith("image/")) { if (file.type.startsWith("image/")) {
let reader = new FileReader() let reader = new FileReader();
reader.onloadend = () => { reader.onloadend = () => {
let fileObject = { let fileObject = {
file: file, file: file,
preview: reader.result preview: reader.result
} };
this.props.addUpload(fileObject) this.props.addUpload(fileObject);
} };
reader.readAsDataURL(file) reader.readAsDataURL(file);
} else { } else {
this.props.addUpload({file: file, preview: "/icons/file.svg"}) this.props.addUpload({file: file, preview: "/icons/file.svg"});
} }
}) });
}, },
render: function() { render: function() {
return ( return (
<div className="fileUpload"> <div className="fileUpload">
<input type="file" id="fileUpload" multiple ref={this.setFileRef} /> <input type="file" id="fileUpload" multiple ref={this.setFileRef} />
<label htmlFor="fileUpload"><img src="/icons/file.svg"/></label> <label htmlFor="fileUpload"><img src="/icons/file.svg"/></label>
</div> </div>
) );
} }
}) });
module.exports = fileUpload module.exports = fileUpload;

@ -1,64 +1,64 @@
'use strict' 'use strict';
const React = require('react') const React = require('react');
const ReactDOM = require('react-dom') const ReactDOM = require('react-dom');
const create = require('create-react-class') const create = require('create-react-class');
const Promise = require('bluebird') const Promise = require('bluebird');
const debounce = require('debounce') const debounce = require('debounce');
let FilterList = create({ let FilterList = create({
displayName: "FilterList", displayName: "FilterList",
getInitialState: function() { getInitialState: function() {
return { return {
selection: "room0", selection: "room0",
filter: "" filter: ""
} };
}, },
select: function(id) { select: function(id) {
this.setState({selection: id, filter: ""}) this.setState({selection: id, filter: ""});
this.state.inputRef.value = "" this.state.inputRef.value = "";
this.props.callback(id) this.props.callback(id);
}, },
inputRef: function(ref) { inputRef: function(ref) {
if (ref == null) { if (ref == null) {
return return;
} }
this.setState({ this.setState({
inputRef: ref inputRef: ref
}) });
ref.addEventListener("keyup", debounce(this.input, 20)) ref.addEventListener("keyup", debounce(this.input, 20));
}, },
input: function(e) { input: function(e) {
this.setState({ this.setState({
filter: e.target.value.toUpperCase() filter: e.target.value.toUpperCase()
}) });
}, },
render: function() { render: function() {
let keys = Object.keys(this.props.items) let keys = Object.keys(this.props.items);
let items = keys.map((itemKey, id) => { let items = keys.map((itemKey, id) => {
let item = this.props.items[itemKey] let item = this.props.items[itemKey];
let props = { let props = {
selected: this.state.selection == itemKey, selected: this.state.selection == itemKey,
filter: this.state.filter, filter: this.state.filter,
content: item, content: item,
key: itemKey, key: itemKey,
listId: itemKey, listId: itemKey,
select: this.select, select: this.select,
properties: this.props.properties properties: this.props.properties
} };
return React.createElement(this.props.element, props) return React.createElement(this.props.element, props);
}) });
return <> return <>
<input className="filter" ref={this.inputRef} placeholder="Search"/> <input className="filter" ref={this.inputRef} placeholder="Search"/>
<div className="list"> <div className="list">
{items} {items}
</div> </div>
</> </>;
} }
}) });
module.exports = FilterList module.exports = FilterList;

@ -1,19 +1,19 @@
'use strict' 'use strict';
const React = require('react') const React = require('react');
const ReactDOM = require('react-dom') const ReactDOM = require('react-dom');
const create = require('create-react-class') const create = require('create-react-class');
const Promise = require('bluebird') const Promise = require('bluebird');
let info = create({ let info = create({
displayName: "Info", displayName: "Info",
render: function() { render: function() {
let title = "" let title = "";
if (this.props.room != undefined) { if (this.props.room != undefined) {
title = this.props.room.name title = this.props.room.name;
} }
return <div className="info">{title}</div> return <div className="info">{title}</div>;
} }
}) });
module.exports = info module.exports = info;

@ -1,277 +1,277 @@
'use strict' 'use strict';
const React = require('react') const React = require('react');
const ReactDOM = require('react-dom') const ReactDOM = require('react-dom');
const create = require('create-react-class') const create = require('create-react-class');
const Promise = require('bluebird') const Promise = require('bluebird');
const colorConvert = require('color-convert') const colorConvert = require('color-convert');
const sanitize = require('sanitize-html') const sanitize = require('sanitize-html');
const riot = require('../lib/riot-utils.js') const riot = require('../lib/riot-utils.js');
const FileUpload = require('./fileUpload.js') const FileUpload = require('./fileUpload.js');
let input = create({ let input = create({
displayName: "Input", displayName: "Input",
getInitialState: function() { getInitialState: function() {
return { return {
uploads: [] uploads: []
} };
}, },
setRef: function(ref) { setRef: function(ref) {
if (ref !=null) { if (ref !=null) {
ref.addEventListener("keydown", (e) => { ref.addEventListener("keydown", (e) => {
// only send on plain 'enter' // only send on plain 'enter'
if (e.key == "Enter") { if (e.key == "Enter") {
if (!e.shiftKey && !e.altKey && !e.ctrlKey) { if (!e.shiftKey && !e.altKey && !e.ctrlKey) {
this.send(e) this.send(e);
return return;
} }
} }
}) });
ref.addEventListener('change', this.resize_textarea) ref.addEventListener('change', this.resize_textarea);
ref.addEventListener('cut', this.resize_textarea_delayed) ref.addEventListener('cut', this.resize_textarea_delayed);
ref.addEventListener('paste', this.resize_textarea_delayed) ref.addEventListener('paste', this.resize_textarea_delayed);
ref.addEventListener('drop', this.resize_textarea_delayed) ref.addEventListener('drop', this.resize_textarea_delayed);
ref.addEventListener('keydown', this.resize_textarea_delayed) ref.addEventListener('keydown', this.resize_textarea_delayed);
this.setState({ref: ref}) this.setState({ref: ref});
} }
}, },
addUpload: function(upload) { addUpload: function(upload) {
let uploads = this.state.uploads let uploads = this.state.uploads;
uploads.push(upload) uploads.push(upload);
this.setState({uploads: uploads}) this.setState({uploads: uploads});
}, },
removeUpload: function(index) { removeUpload: function(index) {
let uploads = this.state.uploads let uploads = this.state.uploads;
uploads.splice(index, 1) uploads.splice(index, 1);
this.setState({uploads: uploads}) this.setState({uploads: uploads});
}, },
resize_textarea: function(element) { resize_textarea: function(element) {
if (element == undefined) { if (element == undefined) {
return; return;
} }
let ref = element.target; let ref = element.target;
if (ref != undefined) { if (ref != undefined) {
ref.style.height = 'auto'; ref.style.height = 'auto';
ref.style.height = ref.scrollHeight+'px'; ref.style.height = ref.scrollHeight+'px';
} }
}, },
resize_textarea_delayed: function(e) { resize_textarea_delayed: function(e) {
setTimeout(() => this.resize_textarea(e), 5); setTimeout(() => this.resize_textarea(e), 5);
}, },
send: function(e) { send: function(e) {
let msg = e.target.value let msg = e.target.value;
if (msg.trim().length != 0) { if (msg.trim().length != 0) {
//TODO: parse markdown (commonmark?) //TODO: parse markdown (commonmark?)
if (msg.startsWith('/')) { if (msg.startsWith('/')) {
// Handle other commands // Handle other commands
let parts = msg.split(' ') let parts = msg.split(' ');
let command = parts[0] let command = parts[0];
let result = handleCommands(command, parts) let result = handleCommands(command, parts);
if (result != null) { if (result != null) {
if (result.type == "html") { if (result.type == "html") {
this.sendHTML(result.content) this.sendHTML(result.content);
} else { } else {
this.sendPlain(result.content) this.sendPlain(result.content);
} }
} }
} else { } else {
this.sendPlain(msg) this.sendPlain(msg);
} }
} }
if (this.state.uploads.length > 0) { if (this.state.uploads.length > 0) {
this.uploadFiles(this.state.uploads) this.uploadFiles(this.state.uploads);
this.setState({uploads: []}) this.setState({uploads: []});
} }
e.target.value = "" e.target.value = "";
e.preventDefault() e.preventDefault();
this.resize_textarea_delayed(e) this.resize_textarea_delayed(e);
}, },
uploadFiles: function(uploads) { uploadFiles: function(uploads) {
let client = this.props.client let client = this.props.client;
Promise.map(uploads, (upload) => { Promise.map(uploads, (upload) => {
let fileUploadPromise = client.uploadContent(upload.file, let fileUploadPromise = client.uploadContent(upload.file,
{onlyContentUri: false}).then((response) => { {onlyContentUri: false}).then((response) => {
return response.content_uri return response.content_uri;
}) });
let mimeType = upload.file.type let mimeType = upload.file.type;
let eventType = "m.file" let eventType = "m.file";
let additionalPromise let additionalPromise;
if (mimeType.startsWith("image/") || mimeType.startsWith("video/")) { if (mimeType.startsWith("image/") || mimeType.startsWith("video/")) {
function elementToThumbnail(element) { function elementToThumbnail(element) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
riot.createThumbnail(element, riot.createThumbnail(element,
element.width, element.width,
element.height, element.height,
thumbnailType thumbnailType
) )
.catch((error) => { .catch((error) => {
console.error("neo: error getting thumbnail", error) console.error("neo: error getting thumbnail", error);
reject() reject();
}) })
.then((thumbResult) => { .then((thumbResult) => {
return client.uploadContent(thumbResult.thumbnail, {onlyContentUri: false}) return client.uploadContent(thumbResult.thumbnail, {onlyContentUri: false});
}).then((response) => { }).then((response) => {
return resolve({ return resolve({
thumbnail_url: response.content_uri, thumbnail_url: response.content_uri,
thumbnail_info: { thumbnail_info: {
mimetype: thumbnailType mimetype: thumbnailType
} }
}) });
}) });
}) });
} }
if (mimeType.startsWith("image/")) { if (mimeType.startsWith("image/")) {
eventType = "m.image" eventType = "m.image";
additionalPromise = riot.loadImageElement(upload.file) additionalPromise = riot.loadImageElement(upload.file)
.then((element) => {return elementToThumbnail(element)}) .then((element) => {return elementToThumbnail(element);});
} else if (mimeType.startsWith("video/")) { } else if (mimeType.startsWith("video/")) {
eventType = "m.video" eventType = "m.video";
additionalPromise = riot.loadVideoElement(upload.file) additionalPromise = riot.loadVideoElement(upload.file)
.then((element) => {return elementToThumbnail(element)}) .then((element) => {return elementToThumbnail(element);});
} }
// create and upload thumbnail // create and upload thumbnail
let thumbnailType = "image/png" let thumbnailType = "image/png";
if (mimeType == "image/jpeg") { if (mimeType == "image/jpeg") {
thumbnailType = mimeType thumbnailType = mimeType;
} }
} else if (mimeType.startsWith("audio/")) { } else if (mimeType.startsWith("audio/")) {
eventType = "m.audio" eventType = "m.audio";
} else { } else {
// m.file // m.file
} }
Promise.all([fileUploadPromise, additionalPromise]).then((result) => { Promise.all([fileUploadPromise, additionalPromise]).then((result) => {
console.log(result) console.log(result);
let info = { let info = {
mimetype: mimeType mimetype: mimeType
} };
if (result[1] != undefined) { if (result[1] != undefined) {
info = Object.assign(info, result[1]) info = Object.assign(info, result[1]);
} }
client.sendEvent(this.props.roomId, "m.room.message", { client.sendEvent(this.props.roomId, "m.room.message", {
body: upload.file.name, body: upload.file.name,
msgtype: eventType, msgtype: eventType,
info: info, info: info,
url: result[0] url: result[0]
}) });
}) });
}) });
}, },
sendPlain: function(string) { sendPlain: function(string) {
let content = { let content = {
body: string, body: string,
msgtype: "m.text" msgtype: "m.text"
} };
content = this.sendReply(content) content = this.sendReply(content);
this.props.client.sendEvent(this.props.roomId, "m.room.message", content, (err, res) => { this.props.client.sendEvent(this.props.roomId, "m.room.message", content, (err, res) => {
if (err != null) { if (err != null) {
console.log(err) console.log(err);
} }
}) });
}, },
sendHTML: function(html) { sendHTML: function(html) {
let content = { let content = {
body: sanitize(html, {allowedTags: []}), body: sanitize(html, {allowedTags: []}),
formatted_body: html, formatted_body: html,
format: "org.matrix.custom.html", format: "org.matrix.custom.html",
msgtype: "m.text" msgtype: "m.text"
} };
content = this.sendReply(content) content = this.sendReply(content);
this.props.client.sendEvent(this.props.roomId, "m.room.message", content, (err, res) => { this.props.client.sendEvent(this.props.roomId, "m.room.message", content, (err, res) => {
console.log(err) console.log(err);
}) });
}, },
sendReply: function(content) { sendReply: function(content) {
if (this.props.replyEvent != undefined) { if (this.props.replyEvent != undefined) {
content['m.relates_to'] = { content['m.relates_to'] = {
'm.in_reply_to': { 'm.in_reply_to': {
event_id: this.props.replyEvent.event_id event_id: this.props.replyEvent.event_id
} }
} };
this.props.onReplyClick() this.props.onReplyClick();
} }
return content return content;
}, },
render: function() { render: function() {
return <div className="input"> return <div className="input">
{this.props.replyEvent && {this.props.replyEvent &&
<div className="replyEvent" onClick={() => this.props.onReplyClick()}> <div className="replyEvent" onClick={() => this.props.onReplyClick()}>
{this.props.replyEvent.plaintext()} {this.props.replyEvent.plaintext()}
</div> </div>
} }
{this.state.uploads.length > 0 && {this.state.uploads.length > 0 &&
<div className="imgPreview"> <div className="imgPreview">
{this.state.uploads.map((upload, key) => { {this.state.uploads.map((upload, key) => {
return ( return (
<div key={key}> <div key={key}>
<img src={upload.preview}/> <img src={upload.preview}/>
<span onClick={() => this.removeUpload(key)}>X</span> <span onClick={() => this.removeUpload(key)}>X</span>
</div> </div>
) );
})} })}
</div> </div>
} }
<div className="content"> <div className="content">
<textarea ref={this.setRef} rows="1" spellCheck="false" placeholder="unencrypted message"></textarea> <textarea ref={this.setRef} rows="1" spellCheck="false" placeholder="unencrypted message"></textarea>
<FileUpload addUpload={this.addUpload}/> <FileUpload addUpload={this.addUpload}/>
</div> </div>
</div> </div>;
} }
}) });
function handleCommands(command, parts) { function handleCommands(command, parts) {
if (command == "/rainbow") { if (command == "/rainbow") {
if (parts.length < 2) { if (parts.length < 2) {
return return;
} }
let string = parts[1] let string = parts[1];
for(let i=2; i < parts.length; i++) { for(let i=2; i < parts.length; i++) {
string += " " + parts[i] string += " " + parts[i];
} }
let html = rainbowTransform(string) let html = rainbowTransform(string);
return { return {
type: 'html', type: 'html',
content: html content: html
} };
} }
return null return null;
} }
function rainbowTransform(text) { function rainbowTransform(text) {
let array = text.split(""); let array = text.split("");
let delta = 360/text.length; let delta = 360/text.length;
if (delta < 10) { if (delta < 10) {
delta = 10; delta = 10;
} else if (delta > 20) { } else if (delta > 20) {
delta = 20; delta = 20;
} }
let h = -1 * delta; // start at beginning let h = -1 * delta; // start at beginning
let rainbowArray = array.map((char) => { let rainbowArray = array.map((char) => {
h = h + delta; h = h + delta;
if (h > 360) { if (h > 360) {
h = 0; h = 0;
} }
return `<font color="${colorConvert.hsl.hex(h, 100, 50)}">${char}</font>`; return `<font color="${colorConvert.hsl.hex(h, 100, 50)}">${char}</font>`;
}); });
let rainbow = rainbowArray.join(""); let rainbow = rainbowArray.join("");
return rainbow; return rainbow;
} }
module.exports = input module.exports = input;

@ -1,20 +1,20 @@
'use strict' 'use strict';
const React = require('react') const React = require('react');
const ReactDOM = require('react-dom') const ReactDOM = require('react-dom');
const create = require('create-react-class') const create = require('create-react-class');
let Loading = create({ let Loading = create({
displayName: "Loading", displayName: "Loading",
render: function() { render: function() {
return ( return (
<div className="spinner"> <div className="spinner">
<div className="bounce1"/> <div className="bounce1"/>
<div className="bounce2"/> <div className="bounce2"/>
<div className="bounce3"/> <div className="bounce3"/>
</div> </div>
) );
} }
}) });
module.exports = Loading module.exports = Loading;

@ -1,117 +1,117 @@
'use strict' 'use strict';
const React = require('react') const React = require('react');
const ReactDOM = require('react-dom') const ReactDOM = require('react-dom');
const create = require('create-react-class') const create = require('create-react-class');
const Promise = require('bluebird') const Promise = require('bluebird');
const debounce = require('debounce') const debounce = require('debounce');
const jdenticon = require('jdenticon') const jdenticon = require('jdenticon');
const FilterList = require('./filterList.js') const FilterList = require('./filterList.js');
let RoomListItem = create({ let RoomListItem = create({
displayName: "RoomListItem", displayName: "RoomListItem",
getInitialState: function() { getInitialState: function() {
let room = this.props.content let room = this.props.content;
let client = this.props.properties.client let client = this.props.properties.client;
let jdenticon = <svg id="avatar" ref={this.jdenticonRef}/> let jdenticon = <svg id="avatar" ref={this.jdenticonRef}/>;
let avatarUrl let avatarUrl;
let roomState = room.getLiveTimeline().getState('f') let roomState = room.getLiveTimeline().getState('f');
let avatarState = roomState.getStateEvents('m.room.avatar') let avatarState = roomState.getStateEvents('m.room.avatar');
if (avatarState.length > 0) { if (avatarState.length > 0) {
let event = avatarState[avatarState.length-1].event let event = avatarState[avatarState.length-1].event;
let hs = client.baseUrl let hs = client.baseUrl;
let media_mxc = event.content.url.slice(6) let media_mxc = event.content.url.slice(6);
let path = `/_matrix/media/v1/thumbnail/${media_mxc}?width=128&height=128&method=scale` let path = `/_matrix/media/v1/thumbnail/${media_mxc}?width=128&height=128&method=scale`;
avatarUrl = { avatarUrl = {
hs: hs, hs: hs,
path: path path: path
} };
} }
return { return {
filterName: room.name.toUpperCase(), filterName: room.name.toUpperCase(),
unread: Math.random() > 0.7, unread: Math.random() > 0.7,
avatarUrl: avatarUrl, avatarUrl: avatarUrl,
jdenticon: jdenticon, jdenticon: jdenticon,
tries: 0 tries: 0
} };
}, },
jdenticonRef: function(ref) { jdenticonRef: function(ref) {
jdenticon.update(ref, this.props.content.roomId) jdenticon.update(ref, this.props.content.roomId);
}, },
avatarFallback: function() { avatarFallback: function() {
// instead of falling back on jdenticon immediately, we can try // instead of falling back on jdenticon immediately, we can try
// a third-party homeserver's media repo // a third-party homeserver's media repo
// this does come with trust issues, and is opt-in in settings // this does come with trust issues, and is opt-in in settings
let fallbackMediaRepos = this.props.properties.options.fallbackMediaRepos let fallbackMediaRepos = this.props.properties.options.fallbackMediaRepos;
if (this.state.tries < fallbackMediaRepos.length) { if (this.state.tries < fallbackMediaRepos.length) {
let avatarUrl = this.state.avatarUrl let avatarUrl = this.state.avatarUrl;
avatarUrl.hs = fallbackMediaRepos[this.state.tries] avatarUrl.hs = fallbackMediaRepos[this.state.tries];
this.setState({ this.setState({
avatarUrl: avatarUrl, avatarUrl: avatarUrl,
tries: this.state.tries + 1 tries: this.state.tries + 1
}) });
} else { } else {
this.setState({avatarUrl: null, avatar: jdenticon}) this.setState({avatarUrl: null, avatar: jdenticon});
} }
}, },
setRef: function(ref) { setRef: function(ref) {
if (ref == null) { if (ref == null) {
return return;
} }
this.setState({ref: ref}) this.setState({ref: ref});
ref.addEventListener("click", () => {this.props.select(this.props.listId)}) ref.addEventListener("click", () => {this.props.select(this.props.listId);});
}, },
render: function() { render: function() {
if (this.state.filterName.indexOf(this.props.filter) == -1) { if (this.state.filterName.indexOf(this.props.filter) == -1) {
return null return null;
} }
let className = "roomListItem" let className = "roomListItem";
if (this.props.selected) { if (this.props.selected) {
className += " active" className += " active";
} }
if (this.state.unread) { if (this.state.unread) {
className += " unread" className += " unread";
} }
return <div className={className} ref={this.setRef}> return <div className={className} ref={this.setRef}>
{this.state.avatarUrl ? {this.state.avatarUrl ?
<img id="avatar" src={`${this.state.avatarUrl.hs}${this.state.avatarUrl.path}`} onError={this.avatarFallback}></img> <img id="avatar" src={`${this.state.avatarUrl.hs}${this.state.avatarUrl.path}`} onError={this.avatarFallback}></img>
: :
this.state.jdenticon this.state.jdenticon
} }
<span id="name">{this.props.content.name}</span> <span id="name">{this.props.content.name}</span>
</div> </div>;
} }
}) });
let Sidebar = create({ let Sidebar = create({
displayName: "Sidebar", displayName: "Sidebar",
getInitialState: function() { getInitialState: function() {
return { return {
filter: "" filter: ""
} };
}, },
setFilter: function(filter) { setFilter: function(filter) {
this.setState({ this.setState({
filter: filter.toUpperCase() filter: filter.toUpperCase()
}) });
}, },
render: function() { render: function() {
return <div className="sidebar"> return <div className="sidebar">
<FilterList items={this.props.rooms} properties={{client: this.props.client, options: this.props.options}} element={RoomListItem} callback={(roomId) => {this.props.selectRoom(roomId)}}/> <FilterList items={this.props.rooms} properties={{client: this.props.client, options: this.props.options}} element={RoomListItem} callback={(roomId) => {this.props.selectRoom(roomId);}}/>
</div> </div>;
} }
}) });
module.exports = Sidebar module.exports = Sidebar;

@ -1,56 +1,56 @@
// should be able to handle images, stickers, and video // should be able to handle images, stickers, and video
module.exports = { module.exports = {
parseEvent: function(client, event, maxHeight, maxWidth) { parseEvent: function(client, event, maxHeight, maxWidth) {
if (event.content.msgtype == "m.image") { if (event.content.msgtype == "m.image") {
let h = maxHeight let h = maxHeight;
let w = maxWidth let w = maxWidth;
let media_url = client.mxcUrlToHttp(event.content.url) let media_url = client.mxcUrlToHttp(event.content.url);
let thumb_url = event.content.url let thumb_url = event.content.url;
if (event.content.info != null) { if (event.content.info != null) {
if (event.content.info.thumbnail_url != null) { if (event.content.info.thumbnail_url != null) {
thumb_url = event.content.info.thumbnail_url thumb_url = event.content.info.thumbnail_url;
} }
if (event.content.info.thumbnail_info != null) { if (event.content.info.thumbnail_info != null) {
h = (event.content.info.thumbnail_info.h < maxHeight) ? event.content.info.thumbnail_info.h : h h = (event.content.info.thumbnail_info.h < maxHeight) ? event.content.info.thumbnail_info.h : h;
w = (event.content.info.thumbnail_info.w < maxWidth) ? event.content.info.thumbnail_info.w : w w = (event.content.info.thumbnail_info.w < maxWidth) ? event.content.info.thumbnail_info.w : w;
} else { } else {
h = (event.content.info.h < maxHeight) ? event.content.info.h : h h = (event.content.info.h < maxHeight) ? event.content.info.h : h;
w = (event.content.info.w < maxWidth) ? event.content.info.w : w w = (event.content.info.w < maxWidth) ? event.content.info.w : w;
} }
} }
thumb_url = client.mxcUrlToHttp(thumb_url, w, h, 'scale', false) thumb_url = client.mxcUrlToHttp(thumb_url, w, h, 'scale', false);
return { return {
full: media_url, full: media_url,
thumb: thumb_url, thumb: thumb_url,
size: {h, w} size: {h, w}
} };
} }
if (event.content.msgtype == "m.video") { if (event.content.msgtype == "m.video") {
let thumb = null let thumb = null;
let h = maxHeight let h = maxHeight;
let w = maxWidth let w = maxWidth;
if (event.content.info != null) { if (event.content.info != null) {
if (event.content.info.thumbnail_url != null) { if (event.content.info.thumbnail_url != null) {
if (event.content.info.thumbnail_info != null) { if (event.content.info.thumbnail_info != null) {
h = (event.content.info.thumbnail_info.h < maxHeight) ? event.content.info.thumbnail_info.h : h h = (event.content.info.thumbnail_info.h < maxHeight) ? event.content.info.thumbnail_info.h : h;
w = (event.content.info.thumbnail_info.w < maxWidth) ? event.content.info.thumbnail_info.w : w w = (event.content.info.thumbnail_info.w < maxWidth) ? event.content.info.thumbnail_info.w : w;
} }
thumb = client.mxcUrlToHttp(event.content.thumbnail, w, h, 'scale', false) thumb = client.mxcUrlToHttp(event.content.thumbnail, w, h, 'scale', false);
} }
} }
return { return {
full: client.mxcUrlToHttp(event.content.url), full: client.mxcUrlToHttp(event.content.url),
thumb: thumb, thumb: thumb,
size: {h, w} size: {h, w}
} };
} }
} }
} };

@ -37,193 +37,193 @@ const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
*/ */
module.exports = { module.exports = {
createThumbnail: function(element, inputWidth, inputHeight, mimeType) { createThumbnail: function(element, inputWidth, inputHeight, mimeType) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
const MAX_WIDTH = 800; const MAX_WIDTH = 800;
const MAX_HEIGHT = 600; const MAX_HEIGHT = 600;
let targetWidth = inputWidth; let targetWidth = inputWidth;
let targetHeight = inputHeight; let targetHeight = inputHeight;
if (targetHeight > MAX_HEIGHT) { if (targetHeight > MAX_HEIGHT) {
targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
targetHeight = MAX_HEIGHT; targetHeight = MAX_HEIGHT;
} }
if (targetWidth > MAX_WIDTH) { if (targetWidth > MAX_WIDTH) {
targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth));
targetWidth = MAX_WIDTH; targetWidth = MAX_WIDTH;
} }
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.width = targetWidth; canvas.width = targetWidth;
canvas.height = targetHeight; canvas.height = targetHeight;
canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight);
canvas.toBlob(function(thumbnail) { canvas.toBlob(function(thumbnail) {
resolve({ resolve({
info: { info: {
thumbnail_info: { thumbnail_info: {
w: targetWidth, w: targetWidth,
h: targetHeight, h: targetHeight,
mimetype: thumbnail.type, mimetype: thumbnail.type,
size: thumbnail.size, size: thumbnail.size,
}, },
w: inputWidth, w: inputWidth,
h: inputHeight, h: inputHeight,
}, },
thumbnail: thumbnail, thumbnail: thumbnail,
}); });
}, mimeType); }, mimeType);
}); });
}, },
/** /**
* Load a file into a newly created image element. * Load a file into a newly created image element.
* *
* @param {File} file The file to load in an image element. * @param {File} file The file to load in an image element.
* @return {Promise} A promise that resolves with the html image element. * @return {Promise} A promise that resolves with the html image element.
*/ */
loadImageElement: function(imageFile) { loadImageElement: function(imageFile) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
// Load the file into an html element // Load the file into an html element
const img = document.createElement("img"); const img = document.createElement("img");
const objectUrl = URL.createObjectURL(imageFile); const objectUrl = URL.createObjectURL(imageFile);
img.src = objectUrl; img.src = objectUrl;
// Once ready, create a thumbnail // Once ready, create a thumbnail
img.onload = function() { img.onload = function() {
URL.revokeObjectURL(objectUrl); URL.revokeObjectURL(objectUrl);
resolve(img); resolve(img);
}; };
img.onerror = function(e) { img.onerror = function(e) {
reject(e); reject(e);
}; };
}); });
}, },
/** /**
* Load a file into a newly created video element. * Load a file into a newly created video element.
* *
* @param {File} file The file to load in an video element. * @param {File} file The file to load in an video element.
* @return {Promise} A promise that resolves with the video image element. * @return {Promise} A promise that resolves with the video image element.
*/ */
loadVideoElement: function(videoFile) { loadVideoElement: function(videoFile) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
// Load the file into an html element // Load the file into an html element
const video = document.createElement("video"); const video = document.createElement("video");
const reader = new FileReader(); const reader = new FileReader();
reader.onload = function(e) { reader.onload = function(e) {
video.src = e.target.result; video.src = e.target.result;
// Once ready, returns its size // Once ready, returns its size
// Wait until we have enough data to thumbnail the first frame. // Wait until we have enough data to thumbnail the first frame.
video.onloadeddata = function() { video.onloadeddata = function() {
video.width = video.videoWidth video.width = video.videoWidth;
video.height = video.videoHeight video.height = video.videoHeight;
resolve(video); resolve(video);
}; };
video.onerror = function(e) { video.onerror = function(e) {
reject(e); reject(e);
}; };
}; };
reader.onerror = function(e) { reader.onerror = function(e) {
reject(e); reject(e);
}; };
reader.readAsDataURL(videoFile); reader.readAsDataURL(videoFile);
}); });
}, },
sanitize: function(html) { sanitize: function(html) {
return sanitize(html, this.sanitizeHtmlParams); return sanitize(html, this.sanitizeHtmlParams);
}, },
sanitizeHtmlParams: { sanitizeHtmlParams: {
allowedTags: [ allowedTags: [
'font', // custom to matrix for IRC-style font coloring 'font', // custom to matrix for IRC-style font coloring
'del', // for markdown 'del', // for markdown
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'a', 'ul', 'ol', 'sup', 'sub', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'a', 'ul', 'ol', 'sup', 'sub',
'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', 'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img', 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img',
'mx-reply', 'mx-rainbow' 'mx-reply', 'mx-rainbow'
], ],
allowedAttributes: { allowedAttributes: {
// custom ones first: // custom ones first:
font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
img: ['src', 'width', 'height', 'alt', 'title'], img: ['src', 'width', 'height', 'alt', 'title'],
ol: ['start'], ol: ['start'],
code: ['class'], // We don't actually allow all classes, we filter them in transformTags code: ['class'], // We don't actually allow all classes, we filter them in transformTags
}, },
// Lots of these won't come up by default because we don't allow them // Lots of these won't come up by default because we don't allow them
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'], selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
// URL schemes we permit // URL schemes we permit
allowedSchemes: ['http', 'https', 'ftp', 'mailto', 'magnet'], allowedSchemes: ['http', 'https', 'ftp', 'mailto', 'magnet'],
allowProtocolRelative: false, allowProtocolRelative: false,
transformTags: { // custom to matrix transformTags: { // custom to matrix
// add blank targets to all hyperlinks except vector URLs // add blank targets to all hyperlinks except vector URLs
'img': function(tagName, attribs) { 'img': function(tagName, attribs) {
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag // Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
// because transformTags is used _before_ we filter by allowedSchemesByTag and // because transformTags is used _before_ we filter by allowedSchemesByTag and
// we don't want to allow images with `https?` `src`s. // we don't want to allow images with `https?` `src`s.
//if (!attribs.src || !attribs.src.startsWith('mxc://')) { //if (!attribs.src || !attribs.src.startsWith('mxc://')) {
return { tagName, attribs: {}}; return { tagName, attribs: {}};
//} //}
//attribs.src = MatrixClientPeg.get().mxcUrlToHttp( //attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
// attribs.src, // attribs.src,
// attribs.width || 800, // attribs.width || 800,
// attribs.height || 600 // attribs.height || 600
//); //);
//return { tagName: tagName, attribs: attribs }; //return { tagName: tagName, attribs: attribs };
}, },
'code': function(tagName, attribs) { 'code': function(tagName, attribs) {
if (typeof attribs.class !== 'undefined') { if (typeof attribs.class !== 'undefined') {
// Filter out all classes other than ones starting with language- for syntax highlighting. // Filter out all classes other than ones starting with language- for syntax highlighting.
const classes = attribs.class.split(/\s+/).filter(function(cl) { const classes = attribs.class.split(/\s+/).filter(function(cl) {
return cl.startsWith('language-'); return cl.startsWith('language-');
}); });
attribs.class = classes.join(' '); attribs.class = classes.join(' ');
} }
return { return {
tagName: tagName, tagName: tagName,
attribs: attribs, attribs: attribs,
}; };
}, },
'*': function(tagName, attribs) { '*': function(tagName, attribs) {
// Delete any style previously assigned, style is an allowedTag for font and span // Delete any style previously assigned, style is an allowedTag for font and span
// because attributes are stripped after transforming // because attributes are stripped after transforming
delete attribs.style; delete attribs.style;
// Sanitise and transform data-mx-color and data-mx-bg-color to their CSS // Sanitise and transform data-mx-color and data-mx-bg-color to their CSS
// equivalents // equivalents
const customCSSMapper = { const customCSSMapper = {
'data-mx-color': 'color', 'data-mx-color': 'color',
'data-mx-bg-color': 'background-color', 'data-mx-bg-color': 'background-color',
// $customAttributeKey: $cssAttributeKey // $customAttributeKey: $cssAttributeKey
}; };
let style = ""; let style = "";
Object.keys(customCSSMapper).forEach((customAttributeKey) => { Object.keys(customCSSMapper).forEach((customAttributeKey) => {
const cssAttributeKey = customCSSMapper[customAttributeKey]; const cssAttributeKey = customCSSMapper[customAttributeKey];
const customAttributeValue = attribs[customAttributeKey]; const customAttributeValue = attribs[customAttributeKey];
if (customAttributeValue && if (customAttributeValue &&
typeof customAttributeValue === 'string' && typeof customAttributeValue === 'string' &&
COLOR_REGEX.test(customAttributeValue) COLOR_REGEX.test(customAttributeValue)
) { ) {
style += cssAttributeKey + ":" + customAttributeValue + ";"; style += cssAttributeKey + ":" + customAttributeValue + ";";
delete attribs[customAttributeKey]; delete attribs[customAttributeKey];
} }
}); });
if (style) { if (style) {
attribs.style = style; attribs.style = style;
} }
return { tagName: tagName, attribs: attribs }; return { tagName: tagName, attribs: attribs };
}, },
}, },
} }
}; };

@ -1,89 +1,89 @@
let assert = require('assert') let assert = require('assert');
let urllib = require('url') let urllib = require('url');
let querystring = require('querystring') let querystring = require('querystring');
let mediaLib = require('../../lib/media.js') let mediaLib = require('../../lib/media.js');
let client = { let client = {
mxcUrlToHttp: function(url, w, h, method, allowDirectLinks) { mxcUrlToHttp: function(url, w, h, method, allowDirectLinks) {
let hs = "https://chat.privacytools.io" let hs = "https://chat.privacytools.io";
let mxc = url.slice(6) let mxc = url.slice(6);
if (w) { if (w) {
return `${hs}/_matrix/media/v1/thumbnail/${mxc}?w=${w}&h=${h}&method=${method}` return `${hs}/_matrix/media/v1/thumbnail/${mxc}?w=${w}&h=${h}&method=${method}`;
} else { } else {
return `${hs}/_matrix/media/v1/download/${mxc}` return `${hs}/_matrix/media/v1/download/${mxc}`;
} }
} }
} };
let mockEventTemplate = { let mockEventTemplate = {
type: "m.room.message", type: "m.room.message",
sender: "@f0x:privacytools.io", sender: "@f0x:privacytools.io",
content: { content: {
body: "image.png", body: "image.png",
info: { info: {
size: 16692, size: 16692,
mimetype: "image/png", mimetype: "image/png",
thumbnail_info: { thumbnail_info: {
w: 268, w: 268,
h: 141, h: 141,
mimetype: "image/png", mimetype: "image/png",
size: 16896 size: 16896
}, },
w: 268, w: 268,
h: 141, h: 141,
thumbnail_url: "mxc://privacytools.io/zBSerdKMhaXSIxfjzCmOnhXH" thumbnail_url: "mxc://privacytools.io/zBSerdKMhaXSIxfjzCmOnhXH"
}, },
msgtype: "m.image", msgtype: "m.image",
url: "mxc://privacytools.io/khPaFfeRyNdzlSttZraeAUre" url: "mxc://privacytools.io/khPaFfeRyNdzlSttZraeAUre"
}, },
event_id: "$aaa:matrix.org", event_id: "$aaa:matrix.org",
origin_server_ts: 1558470168199, origin_server_ts: 1558470168199,
unsigned: { unsigned: {
age: 143237861 age: 143237861
}, },
room_id: "!aaa:matrix.org" room_id: "!aaa:matrix.org"
} };
describe('media', function() { describe('media', function() {
describe('#parseEvent()', function() { describe('#parseEvent()', function() {
it('event without info', function() { it('event without info', function() {
let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate)) let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate));
mockEvent.content.info = null mockEvent.content.info = null;
checkParsedEvent(mockEvent, { checkParsedEvent(mockEvent, {
w: 1000, w: 1000,
h: 1000, h: 1000,
method: 'scale' method: 'scale'
}) });
}), }),
it('event without thumbnail', function() { it('event without thumbnail', function() {
let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate)) let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate));
mockEvent.content.info.thumbnail_url = null mockEvent.content.info.thumbnail_url = null;
mockEvent.content.info.thumbnail_info = null mockEvent.content.info.thumbnail_info = null;
checkParsedEvent(mockEvent, { checkParsedEvent(mockEvent, {
w: 268, w: 268,
h: 141, h: 141,
method: 'scale' method: 'scale'
}) });
}) });
it('event without thumbnail_info', function() { it('event without thumbnail_info', function() {
let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate)) let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate));
mockEvent.content.info.thumbnail_url = null mockEvent.content.info.thumbnail_url = null;
checkParsedEvent(mockEvent, { checkParsedEvent(mockEvent, {
w: 268, w: 268,
h: 141, h: 141,
method: 'scale' method: 'scale'
}) });
}) });
}) });
}) });
function checkParsedEvent(mockEvent, expected) { function checkParsedEvent(mockEvent, expected) {
let media = mediaLib.parseEvent(client, mockEvent, 1000, 1000) let media = mediaLib.parseEvent(client, mockEvent, 1000, 1000);
let params = querystring.decode(urllib.parse(media.thumb).query) let params = querystring.decode(urllib.parse(media.thumb).query);
Object.keys(params).forEach((key) => { Object.keys(params).forEach((key) => {
assert.equal(expected[key], params[key]) assert.equal(expected[key], params[key]);
}) });
} }

Loading…
Cancel
Save