Compare commits

...

14 Commits

@ -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,72 +1,58 @@
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'))
})
.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'))
})
gulp.task('js', function() {
return gulp.src(['src/app.js', "src/components/**/*"])
.pipe(babel({
presets: [
['@babel/env', {
modules: false
}]
]
}))
.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() {
let b = browserify({
@ -75,14 +61,28 @@ gulp.task('js', function() {
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'))
})
.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();
}]));

@ -19,6 +19,7 @@
"budo": "^11.5.0",
"color-convert": "^2.0.0",
"create-react-class": "^15.6.3",
"dataprog": "^0.1.0",
"debounce": "^1.2.0",
"default-value": "^1.0.0",
"del": "^3.0.0",
@ -40,13 +41,15 @@
"react-dom": "^16.6.3",
"sanitize-html": "^1.20.0",
"sourceify": "^0.1.0",
"validatem": "^0.2.0",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"webpack": "^4.27.1"
},
"devDependencies": {
"eslint": "^6.0.1",
"eslint-plugin-react": "^7.14.2",
"gulp-watch": "^5.0.1",
"mocha-reporter-remote": "^1.7.1",
"livereactload": "^4.0.0-beta.2",
"mocha": "^6.1.4"
}

@ -1,14 +1,12 @@
'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 sdk = require('matrix-js-sdk')
const Sidebar = require('./components/sidebar.js')
const Login = require('./components/Login.js')
const Chat = require('./components/chat.js')
'use strict';
const React = require('react');
const ReactDOM = require('react-dom');
const create = require('create-react-class');
const sdk = require('matrix-js-sdk');
const Sidebar = require('./components/sidebar.js');
const Login = require('./components/Login.js');
const Chat = require('./components/chat.js');
// Things that will get settings:
// colorscheme
@ -24,27 +22,27 @@ let App = create({
options: {
fallbackMediaRepos: []
}
}
};
},
componentDidMount: function() {
//check if accessToken is stored in localStorage
let accessToken = localStorage.getItem('accessToken')
let accessToken = localStorage.getItem('accessToken');
if (localStorage.accessToken != undefined) {
let userId = localStorage.getItem('userId')
let apiUrl = localStorage.getItem('apiUrl')
this.loginCallback(userId, accessToken, apiUrl, true)
let userId = localStorage.getItem('userId');
let apiUrl = localStorage.getItem('apiUrl');
this.loginCallback(userId, accessToken, apiUrl, true);
}
},
loginCallback: function(userId, accessToken, apiUrl, restored) {
if (restored) {
console.log("Restoring from localStorage")
console.log("Restoring from localStorage");
} else {
userId = '@' + userId.replace('@', '')
localStorage.setItem('userId', userId)
localStorage.setItem('accessToken', accessToken)
localStorage.setItem('apiUrl', apiUrl)
userId = '@' + userId.replace('@', '');
localStorage.setItem('userId', userId);
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('apiUrl', apiUrl);
}
let client = sdk.createClient({
baseUrl: apiUrl,
@ -54,48 +52,57 @@ let App = create({
this.setState({
client: client
})
this.startClient(client)
});
this.startClient(client);
},
updateRooms: function (client) {
let rooms = {};
client.getRooms().forEach((room) => {
rooms[room.roomId] = room;
});
this.setState({rooms: rooms});
},
startClient: function(client) {
console.log(client)
client.on("sync", (state, prevState, data) => {
console.log(client);
client.on("sync", (state, _prevState, _data) => {
if (state == "ERROR") {
/* FIXME: Implement? */
} else if (state == "SYNCING") {
let rooms = {}
client.getRooms().forEach((room) => {
rooms[room.roomId] = room
})
this.setState({rooms: rooms})
this.updateRooms(client);
} else if (state == "PREPARED") {
/* FIXME: Implement? */
}
})
client.on("Room.localEchoUpdated", (event) => {
let rooms = {}
client.getRooms().forEach((room) => {
rooms[room.roomId] = room
})
this.setState({rooms: rooms})
})
client.startClient()
});
client.on("Room.localEchoUpdated", (_event) => {
this.updateRooms(client);
});
client.startClient();
},
render: function() {
if (this.state.client == undefined) {
//Login screen
return <Login callback={this.loginCallback}/>
}
return <Login callback={this.loginCallback}/>;
} else {
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}/>
</>
)
);
}
}
})
});
ReactDOM.render(
<App />,
document.getElementById('root')
)
);

@ -1,11 +1,11 @@
'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",
@ -23,109 +23,124 @@ let login = create({
error: null,
valid: false
}
}
};
},
login: function() {
this.setState({error: ""})
return Promise.try(() => {
this.setState({error: ""});
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) {
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])
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()
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) => {
console.log("ERROR fetching homeserver url", error)
let hsState = Object.assign(this.state.hs, {error: error, valid: false, prompt: true})
this.setState({hs: hsState})
/* 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() {
console.log("Logging in")
let user = this.state.formState.user.replace('@', '')
let password = this.state.formState.pass
let hs = this.state.apiUrl
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;
let data = {
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);
let url = hs + "/_matrix/client/r0/login"
this.setState({ json: responseJson });
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)
this.props.callback(responseJson.user_id, responseJson.access_token, homeserverUrl);
} else {
this.setState({error: responseJson.error})
this.setState({ error: responseJson.error });
}
})
.catch((error) => {
console.error(url, 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(':')
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})
formState.hs = parts[1];
let hsState = Object.assign(this.state.hs, {error: null, valid: false});
this.setState({hs: hsState});
}
this.setState({formState: formState})
this.setState({formState: formState});
},
handlePassChange: function(e) {
let formState = this.state.formState
formState.pass = e.target.value
this.setState({formState: formState})
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}})
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"
let hsState = "inactive";
if (this.state.hs.prompt) {
hsState = "active"
hsState = "active";
}
if (this.state.hs.error != null) {
hsState = "error"
hsState = "error";
}
if (this.state.hs.valid) {
hsState = "validated"
hsState = "validated";
}
return (
@ -142,7 +157,7 @@ let login = create({
<label htmlFor="hs" className={hsState}>Homeserver: </label>
{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.handleHomeserverChange}/>
</>
) : (
<span id="hs">{this.state.formState["hs"]}</span>
@ -151,78 +166,95 @@ let login = create({
<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));
function getApiServer(hostname) {
return new Promise((resolve, reject) => {
console.log("Checking for api server from mxid", urllib.format(hostname))
checkApi(hostname).then(() => {
return checkApi(parsedUrl);
}).then(() => {
// Hostname is a valid api server
hostname.pathname = ""
resolve(urllib.format(hostname))
return buildUrl(parsedUrl, "");
}).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")
})
})
})
/* 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) => {
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)
return reject()
console.log("Invalid homeserver url", versionUrl);
/* FIXME: Error types */
throw new Error("Invalid homeserver URL");
}
resolve()
}).catch((err) => {
reject(err)
})
})
});
});
}
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) => {
}));
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")
reject("No homeserver found")
console.log("no well-known in use");
/* FIXME: Error type */
throw new Error("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((_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) => {
console.log("Error in json", err)
reject("Error while parsing .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, {
function buildUrl(parsedUrl, path) {
return urllib.format(Object.assign(parsedUrl, {
pathname: path
}))
}));
}
module.exports = login
module.exports = login;

@ -1,67 +1,56 @@
'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: []})
}
return sanitize(this.content.formatted_body, {allowedTags: []});
} else {
return this.content.body;
}
if (this.type == "m.room.member") {
} else if (this.type == "m.room.member") {
if (this.content.membership == "invite") {
plain = `${this.sender} invited ${this.state_key}`
return `${this.sender} invited ${this.state_key}`;
} else if (this.content.membership == "join") {
plain = `${this.state_key} joined the room`
return `${this.state_key} joined the room`;
} else if (this.content.membership == "leave") {
plain = `${this.state_key} left the room`
return `${this.state_key} left the room`;
} else if (this.content.membership == "kick") {
plain = `${this.sender} kicked ${this.state_key}`
return `${this.sender} kicked ${this.state_key}`;
} else if (this.content.membership == "ban") {
plain = `${this.sender} banned ${this.state_key}`
return `${this.sender} banned ${this.state_key}`;
} else {
return "unknown member event";
}
}
if (this.type == "m.room.avatar") {
} else if (this.type == "m.room.avatar") {
if (this.content.url.length > 0) {
plain = `${this.sender} changed the room avatar`
}
return `${this.sender} changed the room avatar`;
}
if (this.type == "m.room.name") {
return `${this.sender} changed the room name to ${this.content.name}`
} else if (this.type == "m.room.name") {
return `${this.sender} changed the room name to ${this.content.name}`;
} else {
return "unknown event";
}
return plain
}
}
};
let chat = create({
displayName: "Chat",
@ -70,101 +59,85 @@ let chat = create({
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
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;
}
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)
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})
this.setState({ ref: ref });
}
},
onReplyClick: function(e) {
this.setState({replyEvent: 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})
},
if (!this.state.loading) {
let client = this.props.client;
let timeline = client.getRoom(this.props.roomId).getLiveTimeline();
render: function() {
let client = this.props.client
let empty = (
<div className="main">
</div>
)
if (this.props.roomId == undefined) {
//empty screen
return empty
}
this.setState({loading: true});
let room = client.getRoom(this.props.roomId)
if (room == null) {
return empty
}
let messageGroups = {
current: [],
groups: [],
sender: "",
type: ""
return Promise.try(() => {
return client.paginateEventTimeline(timeline, {backwards: true});
}).then(() => {
this.setState({loading: false});
});
}
},
// 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.
render: function() {
let client = this.props.client;
let liveTimeline = room.getLiveTimeline()
let liveTimelineEvents = liveTimeline.getEvents()
let empty = <div className="main" />;
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)
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);
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">
@ -177,51 +150,52 @@ let chat = create({
<span>load older messages</span>
}
</div>
{events}
{(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>
)
);
}
}
}
})
});
let EventGroup = create({
displayName: "EventGroup",
function EventGroup({ events, room, client, onReplyClick }) {
let setAvatarRef = withElement((element) => {
generateJdenticon(user.userId).update(element);
});
getInitialState: function() {
let user = this.props.client.getUser(this.props.events[0].sender)
let avatar = <svg id="avatar" ref={this.avatarRef} />
let user = client.getUser(events[0].sender);
let avatar = expression(() => {
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}/>
}
let url = generateThumbnailUrl({
homeserver: client.baseUrl,
mxc: parseMXC(user.avatarUrl),
width: 128,
height: 128
});
return {
user: user,
avatar: avatar
return <img id="avatar" src={url} />;
} else {
return <svg id="avatar" ref={setAvatarRef} />;
}
},
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}
return (
<div className="eventGroup">
{avatar}
<div className="col">
<User user={this.state.user}/>
{events}
<User user={user}/>
{events.map((event, key) => {
return <Event event={event} key={key} client={client} room={room} onReplyClick={onReplyClick}/>;
})}
</div>
</div>
}
})
);
}
module.exports = chat
module.exports = chat;

@ -1,54 +1,53 @@
'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')
}
};
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
let event = this.props.event;
let state = "";
let reply = "";
let element = "unsupported event: " + event.type;
if (event.local) {
state = " local"
state = " local";
}
if (event.type == "m.room.message") {
let msgtype = event.content.msgtype;
let formattedEvent = parseEvent(event)
let formattedEvent = parseEvent(event); /* FIXME: Specify context */
let parsedReply = formattedEvent.parsedReply
let parsedReply = formattedEvent.parsedReply;
if (parsedReply.isReply) {
let repliedEvent = this.props.room.findEventById(parsedReply.to)
let shortText, repliedUser
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"}
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)
repliedUser = this.props.client.getUser(repliedEvent.event.sender);
shortText = parseEvent(repliedEvent.event); /* FIXME: Specify context */
if (shortText.html) {
shortText = <span dangerouslySetInnerHTML={{__html: shortText.body}}/>
shortText = <span dangerouslySetInnerHTML={{__html: shortText.body}}/>;
} else {
shortText = shortText.body
shortText = shortText.body;
}
}
reply = (
@ -56,60 +55,60 @@ let Event = create({
<User user={repliedUser}/>
{shortText}
</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)) {
element = React.createElement(stateElement, {event: event})
element = React.createElement(stateElement, {event: event});
}
return (
<div className={"event" + state} onClick={() => {
this.props.onReplyClick(event)
console.log(event)
this.props.onReplyClick(event);
console.log(event);
}}>
{reply}
{element}
</div>
)
);
}
})
});
function parseEvent(event, context) {
function parseEvent(event, _context) {
// context can be either 'main' or 'reply'
let body = event.content.body
let html = false
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
body = riot.sanitize(event.content.formatted_body);
html = true;
}
if (body) {
body = body.trim()
body = body.trim();
}
let parsedReply = parseReply(event, body)
let parsedReply = parseReply(event, body);
if (parsedReply.isReply) {
// 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) {
let replyTo
let replyTo;
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) {
// strip <mx-reply> from message if it exists
body = body.replace(mxReplyRegex, "")
body = body.replace(mxReplyRegex, "");
}
} catch(err) {
// 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,34 +1,25 @@
'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
let event = this.props.event;
if (event.content.url == undefined) {
return null
return null;
}
return mediaLib.parseEvent(this.props.client, event, 1000, 1000)
},
updateSize: function(e) {
console.log("image was loaded")
return mediaLib.parseEvent(this.props.client, event, 1000, 1000);
},
render: function() {
let event = this.props.event
let event = this.props.event;
if (this.state == null) {
return "malformed image event: " + event.content.body
return "malformed image event: " + event.content.body;
}
return (
@ -37,8 +28,8 @@ let Event = create({
<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",
render: function() {
let event = this.props.event
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",
render: function() {
let event = this.props.event
let formattedEvent = this.props.formattedEvent
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 =
<div className="body">
{formattedEvent.body}
</div>
</div>;
}
let eventClass = ""
let eventClass = "";
if (event.local) {
eventClass += " local"
eventClass += " local";
}
return <div className={eventClass}>
{eventBody}
</div>
</div>;
}
})
});
module.exports = Event
module.exports = Event;

@ -1,40 +1,18 @@
'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",
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
}
/* FIXME: Cache this to speed it up */
color: generateJdenticon(this.props.user.userId).primaryColor()
};
},
render: function() {
@ -42,8 +20,8 @@ let User = create({
<div className="user" style={{color: this.state.color}}>
{this.props.user.displayName}
</div>
)
);
}
})
});
module.exports = User
module.exports = User;

@ -1,30 +1,25 @@
'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
let event = this.props.event;
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() {
let event = this.props.event
let event = this.props.event;
if (this.state == null) {
return "malformed video event: " + event.content.body
return "malformed video event: " + event.content.body;
}
return (
@ -33,8 +28,8 @@ let Event = create({
<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) => {
module.exports = function FileUpload({ addUpload }) {
function handleChange(event) {
return Promise.map(Array.from(event.target.files), (file) => {
if (file.type.startsWith("image/")) {
let reader = new FileReader()
reader.onloadend = () => {
let fileObject = {
return Promise.try(() => {
return fileToDataUrl(file);
}).then((url) => {
return addUpload({
file: file,
preview: reader.result
}
this.props.addUpload(fileObject)
}
reader.readAsDataURL(file)
preview: url
});
});
} else {
this.props.addUpload({file: file, preview: "/icons/file.svg"})
return addUpload({
file: file,
preview: "/icons/file.svg"
});
}
});
}
})
},
render: function() {
let setFileRef = withElement((element) => {
element.addEventListener("change", handleChange);
return function() {
element.removeEventListener("change", handleChange);
};
});
return (
<div className="fileUpload">
<input type="file" id="fileUpload" multiple ref={this.setFileRef} />
<input type="file" id="fileUpload" multiple ref={setFileRef} />
<label htmlFor="fileUpload"><img src="/icons/file.svg"/></label>
</div>
)
}
})
module.exports = fileUpload
);
};

@ -1,9 +1,7 @@
'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",
@ -12,35 +10,35 @@ let FilterList = create({
return {
selection: "room0",
filter: ""
}
};
},
select: function(id) {
this.setState({selection: id, filter: ""})
this.state.inputRef.value = ""
this.props.callback(id)
this.setState({selection: id, filter: ""});
this.state.inputRef.value = "";
this.props.callback(id);
},
inputRef: function(ref) {
if (ref == null) {
return
return;
}
this.setState({
inputRef: ref
})
ref.addEventListener("keyup", debounce(this.input, 20))
});
ref.addEventListener("keyup", debounce(this.input, 20));
},
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 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,
@ -49,16 +47,16 @@ let FilterList = create({
listId: itemKey,
select: this.select,
properties: this.props.properties
}
return React.createElement(this.props.element, props)
})
};
return React.createElement(this.props.element, props);
});
return <>
<input className="filter" ref={this.inputRef} placeholder="Search"/>
<div className="list">
{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 = ""
let title = "";
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,13 +1,12 @@
'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",
@ -15,7 +14,7 @@ let input = create({
getInitialState: function() {
return {
uploads: []
}
};
},
setRef: function(ref) {
@ -24,30 +23,30 @@ let input = create({
// only send on plain 'enter'
if (e.key == "Enter") {
if (!e.shiftKey && !e.altKey && !e.ctrlKey) {
this.send(e)
return
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})
});
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})
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})
let uploads = this.state.uploads;
uploads.splice(index, 1);
this.setState({uploads: uploads});
},
resize_textarea: function(element) {
@ -66,119 +65,123 @@ let input = create({
},
send: function(e) {
let msg = e.target.value
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)
let parts = msg.split(' ');
let command = parts[0];
let result = handleCommands(command, parts);
if (result != null) {
if (result.type == "html") {
this.sendHTML(result.content)
this.sendHTML(result.content);
} else {
this.sendPlain(result.content)
this.sendPlain(result.content);
}
}
} else {
this.sendPlain(msg)
this.sendPlain(msg);
}
}
if (this.state.uploads.length > 0) {
this.uploadFiles(this.state.uploads)
this.setState({uploads: []})
this.uploadFiles(this.state.uploads);
this.setState({uploads: []});
}
e.target.value = ""
e.preventDefault()
this.resize_textarea_delayed(e)
e.target.value = "";
e.preventDefault();
this.resize_textarea_delayed(e);
},
uploadFiles: function(uploads) {
let client = this.props.client
Promise.map(uploads, (upload) => {
let client = this.props.client;
return Promise.map(uploads, (upload) => {
let fileUploadPromise = client.uploadContent(upload.file,
{onlyContentUri: false}).then((response) => {
return response.content_uri
})
return response.content_uri;
});
let mimeType = upload.file.type
let eventType = "m.file"
let additionalPromise
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,
return Promise.try(() => {
return 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})
);
}).catch((error) => {
console.error("neo: error getting thumbnail", error);
throw error;
}).then((thumbResult) => {
return client.uploadContent(thumbResult.thumbnail, {onlyContentUri: false});
}).then((response) => {
return resolve({
return {
thumbnail_url: response.content_uri,
thumbnail_info: {
mimetype: thumbnailType
}
})
})
})
};
});
}
if (mimeType.startsWith("image/")) {
eventType = "m.image"
eventType = "m.image";
additionalPromise = riot.loadImageElement(upload.file)
.then((element) => {return elementToThumbnail(element)})
.then((element) => {return elementToThumbnail(element);});
} else if (mimeType.startsWith("video/")) {
eventType = "m.video"
eventType = "m.video";
additionalPromise = riot.loadVideoElement(upload.file)
.then((element) => {return elementToThumbnail(element)})
.then((element) => {return elementToThumbnail(element);});
}
// create and upload thumbnail
let thumbnailType = "image/png"
let thumbnailType = "image/png";
if (mimeType == "image/jpeg") {
thumbnailType = mimeType
thumbnailType = mimeType;
}
} else if (mimeType.startsWith("audio/")) {
eventType = "m.audio"
eventType = "m.audio";
} else {
// m.file
}
Promise.all([fileUploadPromise, additionalPromise]).then((result) => {
console.log(result)
return Promise.all([fileUploadPromise, additionalPromise]).then((result) => {
console.log(result);
let info = {
mimetype: mimeType
}
};
if (result[1] != undefined) {
info = Object.assign(info, result[1])
info = Object.assign(info, result[1]);
}
client.sendEvent(this.props.roomId, "m.room.message", {
return client.sendEvent(this.props.roomId, "m.room.message", {
body: upload.file.name,
msgtype: eventType,
info: info,
url: result[0]
})
})
})
});
});
});
},
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) => {
};
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)
console.log(err);
}
})
});
},
sendHTML: function(html) {
@ -187,13 +190,14 @@ let input = create({
formatted_body: html,
format: "org.matrix.custom.html",
msgtype: "m.text"
}
};
content = this.sendReply(content)
content = this.sendReply(content);
this.props.client.sendEvent(this.props.roomId, "m.room.message", content, (err, res) => {
console.log(err)
})
/* FIXME: Promisify */
this.props.client.sendEvent(this.props.roomId, "m.room.message", content, (err, _res) => {
console.log(err);
});
},
sendReply: function(content) {
@ -202,10 +206,10 @@ let input = create({
'm.in_reply_to': {
event_id: this.props.replyEvent.event_id
}
};
this.props.onReplyClick();
}
this.props.onReplyClick()
}
return content
return content;
},
render: function() {
@ -223,7 +227,7 @@ let input = create({
<img src={upload.preview}/>
<span onClick={() => this.removeUpload(key)}>X</span>
</div>
)
);
})}
</div>
}
@ -231,26 +235,26 @@ let input = create({
<textarea ref={this.setRef} rows="1" spellCheck="false" placeholder="unencrypted message"></textarea>
<FileUpload addUpload={this.addUpload}/>
</div>
</div>
</div>;
}
})
});
function handleCommands(command, parts) {
if (command == "/rainbow") {
if (parts.length < 2) {
return
return;
}
let string = parts[1]
let string = parts[1];
for(let i=2; i < parts.length; i++) {
string += " " + parts[i]
string += " " + parts[i];
}
let html = rainbowTransform(string)
let html = rainbowTransform(string);
return {
type: 'html',
content: html
};
}
}
return null
return null;
}
function rainbowTransform(text) {
@ -274,4 +278,4 @@ function rainbowTransform(text) {
return rainbow;
}
module.exports = input
module.exports = input;

@ -1,7 +1,6 @@
'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",
@ -13,8 +12,8 @@ let Loading = create({
<div className="bounce2"/>
<div className="bounce3"/>
</div>
)
);
}
})
});
module.exports = Loading
module.exports = Loading;

@ -1,33 +1,30 @@
'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 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')
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`
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 {
@ -36,49 +33,49 @@ let RoomListItem = create({
avatarUrl: avatarUrl,
jdenticon: jdenticon,
tries: 0
}
};
},
jdenticonRef: function(ref) {
jdenticon.update(ref, this.props.content.roomId)
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
let fallbackMediaRepos = this.props.properties.options.fallbackMediaRepos;
if (this.state.tries < fallbackMediaRepos.length) {
let avatarUrl = this.state.avatarUrl
avatarUrl.hs = fallbackMediaRepos[this.state.tries]
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})
this.setState({avatarUrl: null, avatar: jdenticon});
}
},
setRef: function(ref) {
if (ref == null) {
return
return;
}
this.setState({ref: ref})
ref.addEventListener("click", () => {this.props.select(this.props.listId)})
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
return null;
}
let className = "roomListItem"
let className = "roomListItem";
if (this.props.selected) {
className += " active"
className += " active";
}
if (this.state.unread) {
className += " unread"
className += " unread";
}
return <div className={className} ref={this.setRef}>
{this.state.avatarUrl ?
@ -87,9 +84,9 @@ let RoomListItem = create({
this.state.jdenticon
}
<span id="name">{this.props.content.name}</span>
</div>
</div>;
}
})
});
let Sidebar = create({
displayName: "Sidebar",
@ -97,21 +94,21 @@ let Sidebar = create({
getInitialState: function() {
return {
filter: ""
}
};
},
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>
<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 h = maxHeight;
let w = maxWidth;
let media_url = client.mxcUrlToHttp(event.content.url)
let thumb_url = event.content.url
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
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
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
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)
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
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
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)
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");
}
};

@ -1,6 +1,9 @@
'use strict';
/*
Copyright 2015, 2016 OpenMarket Ltd
With modifications, by f0x
With modifications, by Sven Slootweg <admin@cryto.net>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
@ -14,7 +17,9 @@ limitations under the License.
const Promise = require('bluebird');
const sanitize = require('sanitize-html');
//require("blueimp-canvas-to-blob");
const fileToDataUrl = require("./file-to-data-url");
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
/**
@ -36,29 +41,70 @@ const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
* and a thumbnail key.
*/
function canvasToBlob(canvas, mimeType) {
return new Promise((resolve, _reject) => {
canvas.toBlob(function(blob) {
resolve(blob);
}, mimeType);
});
}
function awaitImageLoad(image) {
return new Promise((resolve, reject) => {
image.onload = function() {
resolve();
};
image.onerror = function(e) {
reject(e);
};
});
}
function awaitVideoLoad(video) {
return new Promise((resolve, reject) => {
video.onloadeddata = function() {
resolve({
width: video.videoWidth,
height: video.videoHeight
});
};
video.onerror = function(error) {
reject(error);
};
});
}
module.exports = {
createThumbnail: function(element, inputWidth, inputHeight, mimeType) {
return new Promise(function(resolve, reject) {
return Promise.try(() => {
const MAX_WIDTH = 800;
const MAX_HEIGHT = 600;
let targetWidth = inputWidth;
let targetHeight = inputHeight;
if (targetHeight > MAX_HEIGHT) {
targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
targetHeight = MAX_HEIGHT;
}
if (targetWidth > MAX_WIDTH) {
targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth));
targetWidth = MAX_WIDTH;
}
const canvas = document.createElement("canvas");
canvas.width = targetWidth;
canvas.height = targetHeight;
const canvas = Object.assign(document.createElement("canvas"), {
width: targetWidth,
height: targetHeight
});
canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight);
canvas.toBlob(function(thumbnail) {
resolve({
return Promise.try(() => {
return canvasToBlob(canvas, mimeType);
}).then((thumbnail) => {
return {
info: {
thumbnail_info: {
w: targetWidth,
@ -70,8 +116,8 @@ module.exports = {
h: inputHeight,
},
thumbnail: thumbnail,
};
});
}, mimeType);
});
},
@ -82,20 +128,18 @@ module.exports = {
* @return {Promise} A promise that resolves with the html image element.
*/
loadImageElement: function(imageFile) {
return new Promise(function(resolve, reject) {
// Load the file into an html element
return Promise.try(() => {
const img = document.createElement("img");
const objectUrl = URL.createObjectURL(imageFile);
img.src = objectUrl;
// Once ready, create a thumbnail
img.onload = function() {
return Promise.try(() => {
return awaitImageLoad(img);
}).then(() => {
URL.revokeObjectURL(objectUrl);
resolve(img);
};
img.onerror = function(e) {
reject(e);
};
return img;
});
});
},
@ -106,29 +150,19 @@ module.exports = {
* @return {Promise} A promise that resolves with the video image element.
*/
loadVideoElement: function(videoFile) {
return new Promise(function(resolve, reject) {
// Load the file into an html element
const video = document.createElement("video");
const reader = new FileReader();
reader.onload = function(e) {
video.src = e.target.result;
return Promise.try(() => {
return fileToDataUrl(videoFile);
}).then((url) => {
const video = Object.assign(document.createElement("video"), {
src: url
});
// Once ready, returns its size
// Wait until we have enough data to thumbnail the first frame.
video.onloadeddata = function() {
video.width = video.videoWidth
video.height = video.videoHeight
resolve(video);
};
video.onerror = function(e) {
reject(e);
};
};
reader.onerror = function(e) {
reject(e);
};
reader.readAsDataURL(videoFile);
return Promise.try(() => {
return awaitVideoLoad(video);
}).then((dimensions) => {
/* FIXME: Check whether this can be improved, it's a bit dirty to shoehorn the dimensions onto the video object like this */
return Object.assign(video, dimensions);
});
});
},
@ -163,7 +197,7 @@ module.exports = {
transformTags: { // custom to matrix
// 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
// because transformTags is used _before_ we filter by allowedSchemesByTag and
// we don't want to allow images with `https?` `src`s.
@ -209,10 +243,7 @@ module.exports = {
Object.keys(customCSSMapper).forEach((customAttributeKey) => {
const cssAttributeKey = customCSSMapper[customAttributeKey];
const customAttributeValue = attribs[customAttributeKey];
if (customAttributeValue &&
typeof customAttributeValue === 'string' &&
COLOR_REGEX.test(customAttributeValue)
) {
if (customAttributeValue && typeof customAttributeValue === 'string' && COLOR_REGEX.test(customAttributeValue)) {
style += cssAttributeKey + ":" + customAttributeValue + ";";
delete attribs[customAttributeKey];
}

@ -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,20 +1,25 @@
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)
/* 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}`
return `${hs}/_matrix/media/v1/thumbnail/${mxc}?w=${w}&h=${h}&method=${method}`;
} else {
return `${hs}/_matrix/media/v1/download/${mxc}`
return `${hs}/_matrix/media/v1/download/${mxc}`;
}
}
}
};
let mockEventTemplate = {
type: "m.room.message",
@ -43,47 +48,47 @@ let mockEventTemplate = {
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
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
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
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])
})
assert.equal(expected[key], params[key]);
});
}

10285
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save