diff --git a/gulpfile.js b/gulpfile.js index 1cde1fc..ccd7f5c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,88 +1,88 @@ -const gulp = require('gulp') -const sass = require('gulp-sass') -const concat = require('gulp-concat') -const gutil = require('gulp-util') -const imagemin = require('gulp-imagemin') -const cache = require('gulp-cache') -const gulpIf = require('gulp-if') -const browserify = require('browserify') -const del = require('del') +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'); -const source = require('vinyl-source-stream') -const buffer = require('vinyl-buffer') -const sourcemaps = require('gulp-sourcemaps') +const source = require('vinyl-source-stream'); +const buffer = require('vinyl-buffer'); +const sourcemaps = require('gulp-sourcemaps'); -const budo = require('budo') -const babelify = require('babelify') +const budo = require('budo'); +const babelify = require('babelify'); -const cssFiles = 'src/scss/**/*.?(s)css' +const cssFiles = 'src/scss/**/*.?(s)css'; let css = gulp.src(cssFiles) - .pipe(sass()) - .pipe(concat('style.css')) - .pipe(gulp.dest('build')) + .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"])) -}) + budo("src/app.js", { + live: true, + dir: "build", + port: 3000, + browserify: { + transform: babelify + } + }).on('exit', cb); + gulp.watch(cssFiles, gulp.series(["sass"])); +}); gulp.task("clean", function(done) { - del.sync('build') - done() -}) + del.sync('build'); + done(); +}); gulp.task("sass", function() { - return gulp.src(cssFiles) - .pipe(sass()) - .pipe(concat('style.css')) - .pipe(gulp.dest('./build')) -}) + return gulp.src(cssFiles) + .pipe(sass()) + .pipe(concat('style.css')) + .pipe(gulp.dest('./build')); +}); gulp.task("assets", function() { - return gulp.src(["src/assets/**/*"]) - .pipe(gulpIf('*.+(png|jpg|jpeg|gif|svg)', - cache(imagemin({ - interlaced: true - })) - )) - .pipe(gulp.dest('build')) -}) + return gulp.src(["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(['src/app.js', "src/components/**/*"]) + .pipe(babel({ + presets: [ + ['@babel/env', { + modules: false + }] + ] + })) + .pipe(gulp.dest('build')); +}); gulp.task('js', function() { - let b = browserify({ - entries: 'src/app.js', - debug: false, - transform: [babelify.configure({ - presets: ['@babel/preset-env', '@babel/preset-react'] - })] - }) - return b.bundle() - .pipe(source('src/app.js')) - .pipe(buffer()) - .pipe(sourcemaps.init({ loadMaps: true })) - .pipe(gulp.dest('build')) -}) + let b = browserify({ + entries: 'src/app.js', + debug: false, + transform: [babelify.configure({ + presets: ['@babel/preset-env', '@babel/preset-react'] + })] + }); + return b.bundle() + .pipe(source('src/app.js')) + .pipe(buffer()) + .pipe(sourcemaps.init({ loadMaps: true })) + .pipe(gulp.dest('build')); +}); gulp.task('build', gulp.parallel(['clean', 'assets', 'js', 'sass', function(done) { - done() -}])) + done(); +}])); diff --git a/src/app.js b/src/app.js index e4750f4..5a6c156 100644 --- a/src/app.js +++ b/src/app.js @@ -1,14 +1,14 @@ -'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') +'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') +const Sidebar = require('./components/sidebar.js'); +const Login = require('./components/Login.js'); +const Chat = require('./components/chat.js'); // Things that will get settings: // colorscheme @@ -16,86 +16,86 @@ const Chat = require('./components/chat.js') // incoming/outgoing message alignment (split) let App = create({ - displayName: "App", + displayName: "App", - getInitialState: function() { - return { - rooms: [], - options: { - fallbackMediaRepos: [] - } - } - }, + getInitialState: function() { + return { + rooms: [], + options: { + fallbackMediaRepos: [] + } + }; + }, - componentDidMount: function() { - //check if accessToken is stored in localStorage - 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) - } - }, + componentDidMount: function() { + //check if accessToken is stored in localStorage + 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); + } + }, - loginCallback: function(userId, accessToken, apiUrl, restored) { - if (restored) { - console.log("Restoring from localStorage") - } else { - userId = '@' + userId.replace('@', '') - localStorage.setItem('userId', userId) - localStorage.setItem('accessToken', accessToken) - localStorage.setItem('apiUrl', apiUrl) - } - let client = sdk.createClient({ - baseUrl: apiUrl, - accessToken: accessToken, - userId: userId - }); + loginCallback: function(userId, accessToken, apiUrl, restored) { + if (restored) { + console.log("Restoring from localStorage"); + } else { + userId = '@' + userId.replace('@', ''); + localStorage.setItem('userId', userId); + localStorage.setItem('accessToken', accessToken); + localStorage.setItem('apiUrl', apiUrl); + } + let client = sdk.createClient({ + baseUrl: apiUrl, + accessToken: accessToken, + userId: userId + }); - this.setState({ - client: client - }) - this.startClient(client) - }, + this.setState({ + client: client + }); + this.startClient(client); + }, - startClient: function(client) { - console.log(client) - client.on("sync", (state, prevState, data) => { - if (state == "ERROR") { - } else if (state == "SYNCING") { - let rooms = {} - client.getRooms().forEach((room) => { - rooms[room.roomId] = room - }) - this.setState({rooms: rooms}) - } else if (state == "PREPARED") { - } - }) - client.on("Room.localEchoUpdated", (event) => { - let rooms = {} - client.getRooms().forEach((room) => { - rooms[room.roomId] = room - }) - this.setState({rooms: rooms}) - }) - client.startClient() - }, + startClient: function(client) { + console.log(client); + client.on("sync", (state, prevState, data) => { + if (state == "ERROR") { + } else if (state == "SYNCING") { + let rooms = {}; + client.getRooms().forEach((room) => { + rooms[room.roomId] = room; + }); + this.setState({rooms: rooms}); + } else if (state == "PREPARED") { + } + }); + client.on("Room.localEchoUpdated", (event) => { + let rooms = {}; + client.getRooms().forEach((room) => { + rooms[room.roomId] = room; + }); + this.setState({rooms: rooms}); + }); + client.startClient(); + }, - render: function() { - if (this.state.client == undefined) { - //Login screen - return - } - return ( + render: function() { + if (this.state.client == undefined) { + //Login screen + return ; + } + return ( <> - {this.setState({roomId: roomId})}}/> + {this.setState({roomId: roomId});}}/> - ) - } -}) + ); + } +}); ReactDOM.render( - , - document.getElementById('root') -) + , + document.getElementById('root') +); diff --git a/src/components/Login.js b/src/components/Login.js index ec0ce3d..30661e7 100644 --- a/src/components/Login.js +++ b/src/components/Login.js @@ -1,228 +1,228 @@ -'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 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'); let login = create({ - displayName: "Login", - - getInitialState: function() { - return { - error: null, - formState: { - user: "", - pass: "", - hs: "" - }, - hs: { - prompt: false, - error: null, - valid: false - } - } - }, - - login: function() { - this.setState({error: ""}) - - if (this.state.hs.valid) { - return this.doLogin() - } - - let parts = this.state.formState.user.split(':') - if (parts.length != 2) { - return this.setState({error: "Please enter a full mxid, like username:homeserver.tld"}) - } - - let hostname = urllib.parse("https://" + parts[1]) - getApiServer(hostname).then((hs) => { - console.log("Using API server", hs) - let formState = this.state.formState - formState.user = parts[0] - formState.hs = hs - let hsState = Object.assign(this.state.hs, {valid: true}) - this.setState({apiUrl: hs, formState: formState, hs: hsState}) - this.doLogin() - }).catch((error) => { - console.log("ERROR fetching homeserver url", error) - let hsState = Object.assign(this.state.hs, {error: error, valid: false, prompt: true}) - this.setState({hs: hsState}) - }) - }, - - doLogin: function() { - console.log("Logging in") - let user = this.state.formState.user.replace('@', '') - let password = this.state.formState.pass - let hs = this.state.apiUrl - - let data = { - user: user, - password: password, - type: "m.login.password", - initial_device_display_name: "Neo v4", - }; - - let url = hs + "/_matrix/client/r0/login" - - fetch(url, { - body: JSON.stringify(data), - headers: { - 'content-type': 'application/json' - }, - method: 'POST', - }).then((response) => response.json()) - .then((responseJson) => { - console.log("got access token", responseJson) - this.setState({json: responseJson}) - if(responseJson.access_token != undefined) { - this.props.callback(responseJson.user_id, responseJson.access_token, hs) - } else { - this.setState({error: responseJson.error}) - } - }) - .catch((error) => { - console.error(url, error); - }); - }, - - handleUserChange: function(e) { - let formState = this.state.formState - let user = e.target.value - formState.user = e.target.value - let parts = user.split(':') - if (parts.length == 2) { - formState.hs = parts[1] - let hsState = Object.assign(this.state.hs, {error: null, valid: false}) - this.setState({hs: hsState}) - } - this.setState({formState: formState}) - }, - - handlePassChange: function(e) { - let formState = this.state.formState - formState.pass = e.target.value - this.setState({formState: formState}) - }, - - handleHsChange: function(e) { - let formState = this.state.formState - formState.hs = e.target.value - this.setState({formState: formState}) - this.setState({hs: {error: null, valid: false, prompt: true, changed: true}}) - }, - - render: function() { - let hsState = "inactive" - if (this.state.hs.prompt) { - hsState = "active" - } - if (this.state.hs.error != null) { - hsState = "error" - } - if (this.state.hs.valid) { - hsState = "validated" - } - - return ( -
- -
{this.state.error}
-
- - - - - - - - {this.state.hs.prompt ? ( + displayName: "Login", + + getInitialState: function() { + return { + error: null, + formState: { + user: "", + pass: "", + hs: "" + }, + hs: { + prompt: false, + error: null, + valid: false + } + }; + }, + + login: function() { + this.setState({error: ""}); + + if (this.state.hs.valid) { + return this.doLogin(); + } + + let parts = this.state.formState.user.split(':'); + if (parts.length != 2) { + return this.setState({error: "Please enter a full mxid, like username:homeserver.tld"}); + } + + let hostname = urllib.parse("https://" + parts[1]); + getApiServer(hostname).then((hs) => { + console.log("Using API server", hs); + let formState = this.state.formState; + formState.user = parts[0]; + formState.hs = hs; + let hsState = Object.assign(this.state.hs, {valid: true}); + this.setState({apiUrl: hs, formState: formState, hs: hsState}); + this.doLogin(); + }).catch((error) => { + console.log("ERROR fetching homeserver url", error); + let hsState = Object.assign(this.state.hs, {error: error, valid: false, prompt: true}); + this.setState({hs: hsState}); + }); + }, + + doLogin: function() { + console.log("Logging in"); + let user = this.state.formState.user.replace('@', ''); + let password = this.state.formState.pass; + let hs = this.state.apiUrl; + + let data = { + user: user, + password: password, + type: "m.login.password", + initial_device_display_name: "Neo v4", + }; + + let url = hs + "/_matrix/client/r0/login"; + + fetch(url, { + body: JSON.stringify(data), + headers: { + 'content-type': 'application/json' + }, + method: 'POST', + }).then((response) => response.json()) + .then((responseJson) => { + console.log("got access token", responseJson); + this.setState({json: responseJson}); + if(responseJson.access_token != undefined) { + this.props.callback(responseJson.user_id, responseJson.access_token, hs); + } else { + this.setState({error: responseJson.error}); + } + }) + .catch((error) => { + console.error(url, error); + }); + }, + + handleUserChange: function(e) { + let formState = this.state.formState; + let user = e.target.value; + formState.user = e.target.value; + let parts = user.split(':'); + if (parts.length == 2) { + formState.hs = parts[1]; + let hsState = Object.assign(this.state.hs, {error: null, valid: false}); + this.setState({hs: hsState}); + } + this.setState({formState: formState}); + }, + + handlePassChange: function(e) { + let formState = this.state.formState; + formState.pass = e.target.value; + this.setState({formState: formState}); + }, + + handleHsChange: function(e) { + let formState = this.state.formState; + formState.hs = e.target.value; + this.setState({formState: formState}); + this.setState({hs: {error: null, valid: false, prompt: true, changed: true}}); + }, + + render: function() { + let hsState = "inactive"; + if (this.state.hs.prompt) { + hsState = "active"; + } + if (this.state.hs.error != null) { + hsState = "error"; + } + if (this.state.hs.valid) { + hsState = "validated"; + } + + return ( +
+ +
{this.state.error}
+
+ + + + + + + + {this.state.hs.prompt ? ( <> - ) : ( - {this.state.formState["hs"]} - )} + ) : ( + {this.state.formState["hs"]} + )} - -
-
- ) - } -}) + +
+
+ ); + } +}); function getApiServer(hostname) { - return new Promise((resolve, reject) => { - console.log("Checking for api server from mxid", urllib.format(hostname)) - checkApi(hostname).then(() => { - // Hostname is a valid api server - hostname.pathname = "" - resolve(urllib.format(hostname)) - }).catch(() => { - console.log("trying .well-known") - tryWellKnown(hostname).then((hostname) => { - console.log("got .well-known host", hostname) - resolve(hostname) - }).catch((err) => { - reject("Fatal error trying to get API host") - }) - }) - }) + return new Promise((resolve, reject) => { + console.log("Checking for api server from mxid", urllib.format(hostname)); + checkApi(hostname).then(() => { + // Hostname is a valid api server + hostname.pathname = ""; + resolve(urllib.format(hostname)); + }).catch(() => { + console.log("trying .well-known"); + tryWellKnown(hostname).then((hostname) => { + console.log("got .well-known host", hostname); + resolve(hostname); + }).catch((err) => { + reject("Fatal error trying to get API host"); + }); + }); + }); } function checkApi(host) { - let versionUrl = buildUrl(host, "/_matrix/client/versions") - return new Promise((resolve, reject) => { - fetch(versionUrl).then((response) => { - if (response.status != 200) { - console.log("Invalid homeserver url", versionUrl) - return reject() - } - resolve() - }).catch((err) => { - reject(err) - }) - }) + let versionUrl = buildUrl(host, "/_matrix/client/versions"); + return new Promise((resolve, reject) => { + fetch(versionUrl).then((response) => { + if (response.status != 200) { + console.log("Invalid homeserver url", versionUrl); + return reject(); + } + resolve(); + }).catch((err) => { + reject(err); + }); + }); } function tryWellKnown(host) { - let wellKnownUrl = urllib.format(Object.assign(host, { - pathname: "/.well-known/matrix/client" - })) - console.log("Trying", wellKnownUrl, "for .well-known") - return new Promise((resolve, reject) => { - return fetch(wellKnownUrl) - .then((response) => { - if (response.status != 200) { - console.log("no well-known in use") - reject("No homeserver found") - } - return response - }).catch((error) => { - reject("can't fetch .well-known") - }) - .then((response) => response.json()) - .then((json) => { - console.log("Parsed json", json) - if (json['m.homeserver'] != undefined && json['m.homeserver'].base_url != undefined) { - resolve(json['m.homeserver'].base_url) - } - }) - .catch((err) => { - console.log("Error in json", err) - reject("Error while parsing .well-known") - }) - }) + let wellKnownUrl = urllib.format(Object.assign(host, { + pathname: "/.well-known/matrix/client" + })); + console.log("Trying", wellKnownUrl, "for .well-known"); + return new Promise((resolve, reject) => { + return fetch(wellKnownUrl) + .then((response) => { + if (response.status != 200) { + console.log("no well-known in use"); + reject("No homeserver found"); + } + return response; + }).catch((error) => { + reject("can't fetch .well-known"); + }) + .then((response) => response.json()) + .then((json) => { + console.log("Parsed json", json); + if (json['m.homeserver'] != undefined && json['m.homeserver'].base_url != undefined) { + resolve(json['m.homeserver'].base_url); + } + }) + .catch((err) => { + console.log("Error in json", err); + reject("Error while parsing .well-known"); + }); + }); } function buildUrl(host, path) { - return urllib.format(Object.assign(host, { - pathname: path - })) + return urllib.format(Object.assign(host, { + pathname: path + })); } -module.exports = login +module.exports = login; diff --git a/src/components/chat.js b/src/components/chat.js index 929622a..2b494a4 100644 --- a/src/components/chat.js +++ b/src/components/chat.js @@ -1,227 +1,227 @@ -'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') +'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" + lightness: { + color: [0.58, 0.66], + grayscale: [0.30, 0.90] + }, + saturation: { + color: 0.66, + grayscale: 0.00 + }, + backColor: "#00000000" }; let eventFunctions = { - plaintext: function() { - let plain = "unknown event" - - if (this.type == "m.room.message") { - plain = this.content.body - - if (this.content.format == "org.matrix.custom.html") { - plain = sanitize(this.content.formatted_body, {allowedTags: []}) - } - } - if (this.type == "m.room.member") { - if (this.content.membership == "invite") { - plain = `${this.sender} invited ${this.state_key}` - } else if (this.content.membership == "join") { - plain = `${this.state_key} joined the room` - } else if (this.content.membership == "leave") { - plain = `${this.state_key} left the room` - } else if (this.content.membership == "kick") { - plain = `${this.sender} kicked ${this.state_key}` - } else if (this.content.membership == "ban") { - plain = `${this.sender} banned ${this.state_key}` - } - } - if (this.type == "m.room.avatar") { - if (this.content.url.length > 0) { - plain = `${this.sender} changed the room avatar` - } - } - if (this.type == "m.room.name") { - return `${this.sender} changed the room name to ${this.content.name}` - } - return plain - } -} + plaintext: function() { + let plain = "unknown event"; + + if (this.type == "m.room.message") { + plain = this.content.body; + + if (this.content.format == "org.matrix.custom.html") { + plain = sanitize(this.content.formatted_body, {allowedTags: []}); + } + } + if (this.type == "m.room.member") { + if (this.content.membership == "invite") { + plain = `${this.sender} invited ${this.state_key}`; + } else if (this.content.membership == "join") { + plain = `${this.state_key} joined the room`; + } else if (this.content.membership == "leave") { + plain = `${this.state_key} left the room`; + } else if (this.content.membership == "kick") { + plain = `${this.sender} kicked ${this.state_key}`; + } else if (this.content.membership == "ban") { + plain = `${this.sender} banned ${this.state_key}`; + } + } + if (this.type == "m.room.avatar") { + if (this.content.url.length > 0) { + plain = `${this.sender} changed the room avatar`; + } + } + if (this.type == "m.room.name") { + return `${this.sender} changed the room name to ${this.content.name}`; + } + return plain; + } +}; let chat = create({ - displayName: "Chat", - - getInitialState: function() { - return { - ref: null, - loading: false - } - }, - - getSnapshotBeforeUpdate: function(oldProps, oldState) { - let ref = this.state.ref - if (ref == null) {return null} - if ((ref.scrollHeight - ref.offsetHeight) - ref.scrollTop < 100) { // Less than 100px from bottom - return true - } - return null - }, - - componentDidUpdate(prevProps, prevState, snapshot) { - let ref = this.state.ref - if (ref == null) {return} - if (snapshot) { // scroll to bottom - ref.scrollTop = (ref.scrollHeight - ref.offsetHeight) - } - }, - - setRef: function(ref) { - if (ref != null) { - this.setState({ref: ref}) - } - }, - - onReplyClick: function(e) { - this.setState({replyEvent: e}) - }, - - paginateBackwards: function() { - if (this.state.loading) { - return - } - let client = this.props.client - client.paginateEventTimeline(client.getRoom(this.props.roomId).getLiveTimeline(), {backwards: true}).then(() => { - this.setState({loading: false}) - }) - this.setState({loading: true}) - }, - - render: function() { - let client = this.props.client - let empty = ( -
-
- ) - if (this.props.roomId == undefined) { - //empty screen - return empty - } - - let room = client.getRoom(this.props.roomId) - if (room == null) { - return empty - } - - let messageGroups = { - current: [], - groups: [], - sender: "", - type: "" - } - - // if the sender is the same, add it to the 'current' messageGroup, if not, - // push the old one to 'groups' and start with a new array. - - let liveTimeline = room.getLiveTimeline() - let liveTimelineEvents = liveTimeline.getEvents() - - let events = [] - if (liveTimelineEvents.length > 0) { - liveTimelineEvents.forEach((MatrixEvent) => { - let event = MatrixEvent.event; - event = Object.assign(event, eventFunctions) - if (event.sender == null) { // localecho messages - event.sender = event.user_id - event.local = true - } - if (event.sender != messageGroups.sender || event.type != messageGroups.type) { - messageGroups.sender = event.sender - messageGroups.type = event.type - if (messageGroups.current.length != 0) { - messageGroups.groups.push(messageGroups.current) - } - messageGroups.current = [] - } - messageGroups.current.push(event) - }) - messageGroups.groups.push(messageGroups.current) - - events = messageGroups.groups.map((events, id) => { - return - }) - } - //TODO: replace with something that only renders events in view - return ( -
- -
-
-
- {this.state.loading ? - : - load older messages - } -
- {events} -
-
- -
- ) - } -}) + displayName: "Chat", + + getInitialState: function() { + return { + ref: null, + loading: false + }; + }, + + getSnapshotBeforeUpdate: function(oldProps, oldState) { + let ref = this.state.ref; + if (ref == null) {return null;} + if ((ref.scrollHeight - ref.offsetHeight) - ref.scrollTop < 100) { // Less than 100px from bottom + return true; + } + return null; + }, + + componentDidUpdate(prevProps, prevState, snapshot) { + let ref = this.state.ref; + if (ref == null) {return;} + if (snapshot) { // scroll to bottom + ref.scrollTop = (ref.scrollHeight - ref.offsetHeight); + } + }, + + setRef: function(ref) { + if (ref != null) { + this.setState({ref: ref}); + } + }, + + onReplyClick: function(e) { + this.setState({replyEvent: e}); + }, + + paginateBackwards: function() { + if (this.state.loading) { + return; + } + let client = this.props.client; + client.paginateEventTimeline(client.getRoom(this.props.roomId).getLiveTimeline(), {backwards: true}).then(() => { + this.setState({loading: false}); + }); + this.setState({loading: true}); + }, + + render: function() { + let client = this.props.client; + let empty = ( +
+
+ ); + if (this.props.roomId == undefined) { + //empty screen + return empty; + } + + let room = client.getRoom(this.props.roomId); + if (room == null) { + return empty; + } + + let messageGroups = { + current: [], + groups: [], + sender: "", + type: "" + }; + + // if the sender is the same, add it to the 'current' messageGroup, if not, + // push the old one to 'groups' and start with a new array. + + let liveTimeline = room.getLiveTimeline(); + let liveTimelineEvents = liveTimeline.getEvents(); + + let events = []; + if (liveTimelineEvents.length > 0) { + liveTimelineEvents.forEach((MatrixEvent) => { + let event = MatrixEvent.event; + event = Object.assign(event, eventFunctions); + if (event.sender == null) { // localecho messages + event.sender = event.user_id; + event.local = true; + } + if (event.sender != messageGroups.sender || event.type != messageGroups.type) { + messageGroups.sender = event.sender; + messageGroups.type = event.type; + if (messageGroups.current.length != 0) { + messageGroups.groups.push(messageGroups.current); + } + messageGroups.current = []; + } + messageGroups.current.push(event); + }); + messageGroups.groups.push(messageGroups.current); + + events = messageGroups.groups.map((events, id) => { + return ; + }); + } + //TODO: replace with something that only renders events in view + return ( +
+ +
+
+
+ {this.state.loading ? + : + load older messages + } +
+ {events} +
+
+ +
+ ); + } +}); let EventGroup = create({ - displayName: "EventGroup", - - getInitialState: function() { - let user = this.props.client.getUser(this.props.events[0].sender) - let avatar = - - 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 = - } - - return { - user: user, - avatar: avatar - } - }, - - avatarRef: function(ref) { - jdenticon.update(ref, this.state.user.userId) - }, - - render: function() { - let events = this.props.events.map((event, key) => { - return - }) - return
- {this.state.avatar} -
- - {events} -
-
- } -}) - -module.exports = chat + displayName: "EventGroup", + + getInitialState: function() { + let user = this.props.client.getUser(this.props.events[0].sender); + let avatar = ; + + 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 = ; + } + + return { + user: user, + avatar: avatar + }; + }, + + avatarRef: function(ref) { + jdenticon.update(ref, this.state.user.userId); + }, + + render: function() { + let events = this.props.events.map((event, key) => { + return ; + }); + return
+ {this.state.avatar} +
+ + {events} +
+
; + } +}); + +module.exports = chat; diff --git a/src/components/events/Event.js b/src/components/events/Event.js index 4acc490..1ec257c 100644 --- a/src/components/events/Event.js +++ b/src/components/events/Event.js @@ -1,115 +1,115 @@ -'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 ReactDOM = require('react-dom'); +const create = require('create-react-class'); +const defaultValue = require('default-value'); -const riot = require('../../lib/riot-utils.js') +const riot = require('../../lib/riot-utils.js'); -const User = require('./user.js') -const stateElement = require('./state.js') +const User = require('./user.js'); +const stateElement = require('./state.js'); const elements = { - "m.text": require('./text.js'), - "m.image": require('./image.js'), - "m.video": require('./video.js') -} + "m.text": require('./text.js'), + "m.image": require('./image.js'), + "m.video": require('./video.js') +}; -const mxReplyRegex = /^[\s\S]+<\/mx-reply>/ +const mxReplyRegex = /^[\s\S]+<\/mx-reply>/; let Event = create({ - displayName: "Event", - - render: function() { - let event = this.props.event - let state = "" - let reply = "" - let element = "unsupported event: " + event.type - - if (event.local) { - state = " local" - } - - if (event.type == "m.room.message") { - let msgtype = event.content.msgtype; - let formattedEvent = parseEvent(event) - - let parsedReply = formattedEvent.parsedReply - if (parsedReply.isReply) { - let repliedEvent = this.props.room.findEventById(parsedReply.to) - let shortText, repliedUser - if (repliedEvent == undefined) { - shortText = "Can't load this event" - repliedUser = {userId: "NEO_UNKNOWN", displayName: "Unknown User"} - // fall back on content? - } else { - repliedUser = this.props.client.getUser(repliedEvent.event.sender) - shortText = parseEvent(repliedEvent.event) - if (shortText.html) { - shortText = - } else { - shortText = shortText.body - } - } - reply = ( -
- - {shortText} -
- ) - } - element = React.createElement(defaultValue(elements[msgtype], elements["m.text"]), {formattedEvent: formattedEvent, event: event, client: this.props.client}) - } else if (["m.room.name", "m.room.member", "m.room.avatar"].includes(event.type)) { - element = React.createElement(stateElement, {event: event}) - } - - return ( -
{ - this.props.onReplyClick(event) - console.log(event) - }}> - {reply} - {element} -
- ) - } -}) + displayName: "Event", + + render: function() { + let event = this.props.event; + let state = ""; + let reply = ""; + let element = "unsupported event: " + event.type; + + if (event.local) { + state = " local"; + } + + if (event.type == "m.room.message") { + let msgtype = event.content.msgtype; + let formattedEvent = parseEvent(event); + + let parsedReply = formattedEvent.parsedReply; + if (parsedReply.isReply) { + let repliedEvent = this.props.room.findEventById(parsedReply.to); + let shortText, repliedUser; + if (repliedEvent == undefined) { + shortText = "Can't load this event"; + repliedUser = {userId: "NEO_UNKNOWN", displayName: "Unknown User"}; + // fall back on content? + } else { + repliedUser = this.props.client.getUser(repliedEvent.event.sender); + shortText = parseEvent(repliedEvent.event); + if (shortText.html) { + shortText = ; + } else { + shortText = shortText.body; + } + } + reply = ( +
+ + {shortText} +
+ ); + } + element = React.createElement(defaultValue(elements[msgtype], elements["m.text"]), {formattedEvent: formattedEvent, event: event, client: this.props.client}); + } else if (["m.room.name", "m.room.member", "m.room.avatar"].includes(event.type)) { + element = React.createElement(stateElement, {event: event}); + } + + return ( +
{ + this.props.onReplyClick(event); + console.log(event); + }}> + {reply} + {element} +
+ ); + } +}); function parseEvent(event, context) { - // context can be either 'main' or 'reply' - let body = event.content.body - let html = false - - if (event.content.format == "org.matrix.custom.html") { - body = riot.sanitize(event.content.formatted_body) - html = true - } - - if (body) { - body = body.trim() - } - - let parsedReply = parseReply(event, body) - if (parsedReply.isReply) { - // body with fallback stripped - body = parsedReply.body - } - return {body: body, parsedReply: parsedReply, html: html} + // context can be either 'main' or 'reply' + let body = event.content.body; + let html = false; + + if (event.content.format == "org.matrix.custom.html") { + body = riot.sanitize(event.content.formatted_body); + html = true; + } + + if (body) { + body = body.trim(); + } + + let parsedReply = parseReply(event, body); + if (parsedReply.isReply) { + // body with fallback stripped + body = parsedReply.body; + } + return {body: body, parsedReply: parsedReply, html: html}; } function parseReply(event, body) { - let replyTo - try { - replyTo = event.content['m.relates_to']['m.in_reply_to'].event_id - if (replyTo) { - // strip from message if it exists - body = body.replace(mxReplyRegex, "") - } - } catch(err) { - // no reply - return {isReply: false} - } - return {isReply: true, body: body, to: replyTo} + let replyTo; + try { + replyTo = event.content['m.relates_to']['m.in_reply_to'].event_id; + if (replyTo) { + // strip from message if it exists + body = body.replace(mxReplyRegex, ""); + } + } catch(err) { + // no reply + return {isReply: false}; + } + return {isReply: true, body: body, to: replyTo}; } -module.exports = Event +module.exports = Event; diff --git a/src/components/events/image.js b/src/components/events/image.js index dd61b38..96c8f19 100644 --- a/src/components/events/image.js +++ b/src/components/events/image.js @@ -1,44 +1,44 @@ -'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 ReactDOM = require('react-dom'); +const create = require('create-react-class'); +const Promise = require('bluebird'); +const defaultValue = require('default-value'); -const mediaLib = require('../../lib/media.js') +const mediaLib = require('../../lib/media.js'); -const Text = require('./text.js') +const Text = require('./text.js'); let Event = create({ - displayName: "m.image", - - getInitialState: function() { - let event = this.props.event - if (event.content.url == undefined) { - return null - } - return mediaLib.parseEvent(this.props.client, event, 1000, 1000) - }, - - updateSize: function(e) { - console.log("image was loaded") - }, - - render: function() { - let event = this.props.event - - if (this.state == null) { - return "malformed image event: " + event.content.body - } - - return ( -
- - - -
- ) - } -}) + displayName: "m.image", + + getInitialState: function() { + let event = this.props.event; + if (event.content.url == undefined) { + return null; + } + return mediaLib.parseEvent(this.props.client, event, 1000, 1000); + }, + + updateSize: function(e) { + console.log("image was loaded"); + }, + + render: function() { + let event = this.props.event; + + if (this.state == null) { + return "malformed image event: " + event.content.body; + } + + return ( +
+ + + +
+ ); + } +}); module.exports = Event; diff --git a/src/components/events/state.js b/src/components/events/state.js index 3a20017..7700bc8 100644 --- a/src/components/events/state.js +++ b/src/components/events/state.js @@ -1,19 +1,19 @@ -'use strict' -const React = require('react') -const ReactDOM = require('react-dom') -const create = require('create-react-class') +'use strict'; +const React = require('react'); +const ReactDOM = require('react-dom'); +const create = require('create-react-class'); let Event = create({ - displayName: "genericStateEvent", + displayName: "genericStateEvent", - render: function() { - let event = this.props.event - return ( -
- {event.plaintext()} -
- ) - } -}) + render: function() { + let event = this.props.event; + return ( +
+ {event.plaintext()} +
+ ); + } +}); -module.exports = Event +module.exports = Event; diff --git a/src/components/events/text.js b/src/components/events/text.js index 9d4e163..4ac7dde 100644 --- a/src/components/events/text.js +++ b/src/components/events/text.js @@ -1,43 +1,43 @@ -'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 ReactDOM = require('react-dom'); +const create = require('create-react-class'); +const Promise = require('bluebird'); -const riot = require('../../lib/riot-utils.js') +const riot = require('../../lib/riot-utils.js'); let Event = create({ - displayName: "m.text", + displayName: "m.text", - render: function() { - let event = this.props.event - let formattedEvent = this.props.formattedEvent + render: function() { + let event = this.props.event; + let formattedEvent = this.props.formattedEvent; - let eventBody + let eventBody; - if (formattedEvent.html) { - eventBody =
- } else { - eventBody = + if (formattedEvent.html) { + eventBody =
; + } else { + eventBody =
- {formattedEvent.body} -
- } + {formattedEvent.body} +
; + } - let eventClass = "" - if (event.local) { - eventClass += " local" - } + let eventClass = ""; + if (event.local) { + eventClass += " local"; + } - return
- {eventBody} -
- } -}) + return
+ {eventBody} +
; + } +}); -module.exports = Event +module.exports = Event; diff --git a/src/components/events/user.js b/src/components/events/user.js index cc34fd0..fc37d05 100644 --- a/src/components/events/user.js +++ b/src/components/events/user.js @@ -1,49 +1,49 @@ -'use strict' -const React = require('react') -const ReactDOM = require('react-dom') -const create = require('create-react-class') -const jdenticon = require('jdenticon') +'use strict'; +const React = require('react'); +const ReactDOM = require('react-dom'); +const create = require('create-react-class'); +const jdenticon = require('jdenticon'); jdenticon.config = { - lightness: { - color: [0.58, 0.66], - grayscale: [0.30, 0.90] - }, - saturation: { - color: 0.66, - grayscale: 0.00 - }, - backColor: "#00000000" + lightness: { + color: [0.58, 0.66], + grayscale: [0.30, 0.90] + }, + saturation: { + color: 0.66, + grayscale: 0.00 + }, + backColor: "#00000000" }; let User = create({ - displayName: "user", + displayName: "user", - getInitialState: function() { - let icon = jdenticon.toSvg(this.props.user.userId, 200) - let match = icon.match(/#([a-f0-9]{6})/g) - let color = '#ff0000' - for(let i=match.length-1; i>= 0; i--) { - color = match[i] - let r = color.substr(1, 2) - let g = color.substr(3, 2) - let b = color.substr(5, 2) - if (r != g && g != b) { // not greyscale - break - } - } - return { - color: color - } - }, + getInitialState: function() { + 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 + }; + }, - render: function() { - return ( -
- {this.props.user.displayName} -
- ) - } -}) + render: function() { + return ( +
+ {this.props.user.displayName} +
+ ); + } +}); -module.exports = User +module.exports = User; diff --git a/src/components/events/video.js b/src/components/events/video.js index a32150c..b0d6d79 100644 --- a/src/components/events/video.js +++ b/src/components/events/video.js @@ -1,40 +1,40 @@ -'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 ReactDOM = require('react-dom'); +const create = require('create-react-class'); +const Promise = require('bluebird'); +const defaultValue = require('default-value'); -const mediaLib = require('../../lib/media.js') +const mediaLib = require('../../lib/media.js'); -const Text = require('./text.js') +const Text = require('./text.js'); let Event = create({ - displayName: "m.video", - - getInitialState: function() { - let event = this.props.event - if (event.content.url == undefined) { - return null - } - return mediaLib.parseEvent(this.props.client, event, 1000, 1000) - }, - - render: function() { - let event = this.props.event - - if (this.state == null) { - return "malformed video event: " + event.content.body - } - - return ( -
- -
- ) - } -}) + displayName: "m.video", + + getInitialState: function() { + let event = this.props.event; + if (event.content.url == undefined) { + return null; + } + return mediaLib.parseEvent(this.props.client, event, 1000, 1000); + }, + + render: function() { + let event = this.props.event; + + if (this.state == null) { + return "malformed video event: " + event.content.body; + } + + return ( +
+ +
+ ); + } +}); module.exports = Event; diff --git a/src/components/fileUpload.js b/src/components/fileUpload.js index 900bf16..1d0cf8a 100644 --- a/src/components/fileUpload.js +++ b/src/components/fileUpload.js @@ -1,46 +1,46 @@ -'use strict' -const React = require('react') -const ReactDOM = require('react-dom') -const create = require('create-react-class') +'use strict'; +const React = require('react'); +const ReactDOM = require('react-dom'); +const create = require('create-react-class'); let fileUpload = create({ - displayName: "FileUpload", + displayName: "FileUpload", - setFileRef: function(e) { - if (e != null) { - e.addEventListener('change', this.startUpload) - this.setState({ - fileRef: e - }) - } - }, + setFileRef: function(e) { + if (e != null) { + e.addEventListener('change', this.startUpload); + this.setState({ + fileRef: e + }); + } + }, - startUpload: function(e) { - Array.from(e.target.files).forEach((file) => { - if (file.type.startsWith("image/")) { - let reader = new FileReader() - reader.onloadend = () => { - let fileObject = { - file: file, - preview: reader.result - } - this.props.addUpload(fileObject) - } - reader.readAsDataURL(file) - } else { - this.props.addUpload({file: file, preview: "/icons/file.svg"}) - } - }) - }, + startUpload: function(e) { + Array.from(e.target.files).forEach((file) => { + if (file.type.startsWith("image/")) { + let reader = new FileReader(); + reader.onloadend = () => { + let fileObject = { + file: file, + preview: reader.result + }; + this.props.addUpload(fileObject); + }; + reader.readAsDataURL(file); + } else { + this.props.addUpload({file: file, preview: "/icons/file.svg"}); + } + }); + }, - render: function() { - return ( -
- - -
- ) - } -}) + render: function() { + return ( +
+ + +
+ ); + } +}); -module.exports = fileUpload +module.exports = fileUpload; diff --git a/src/components/filterList.js b/src/components/filterList.js index a7ee525..6ba4e4d 100644 --- a/src/components/filterList.js +++ b/src/components/filterList.js @@ -1,64 +1,64 @@ -'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 ReactDOM = require('react-dom'); +const create = require('create-react-class'); +const Promise = require('bluebird'); +const debounce = require('debounce'); let FilterList = create({ - displayName: "FilterList", + displayName: "FilterList", - getInitialState: function() { - return { - selection: "room0", - filter: "" - } - }, + getInitialState: function() { + return { + selection: "room0", + filter: "" + }; + }, - select: function(id) { - this.setState({selection: id, filter: ""}) - this.state.inputRef.value = "" - this.props.callback(id) - }, + select: function(id) { + this.setState({selection: id, filter: ""}); + this.state.inputRef.value = ""; + this.props.callback(id); + }, - inputRef: function(ref) { - if (ref == null) { - return - } - this.setState({ - inputRef: ref - }) - ref.addEventListener("keyup", debounce(this.input, 20)) - }, + inputRef: function(ref) { + if (ref == null) { + return; + } + this.setState({ + inputRef: ref + }); + ref.addEventListener("keyup", debounce(this.input, 20)); + }, - input: function(e) { - this.setState({ - filter: e.target.value.toUpperCase() - }) - }, + input: function(e) { + this.setState({ + filter: e.target.value.toUpperCase() + }); + }, - render: function() { - let keys = Object.keys(this.props.items) - let items = keys.map((itemKey, id) => { - let item = this.props.items[itemKey] - let props = { - selected: this.state.selection == itemKey, - filter: this.state.filter, - content: item, - key: itemKey, - listId: itemKey, - select: this.select, - properties: this.props.properties - } - return React.createElement(this.props.element, props) - }) - return <> + render: function() { + let keys = Object.keys(this.props.items); + let items = keys.map((itemKey, id) => { + let item = this.props.items[itemKey]; + let props = { + selected: this.state.selection == itemKey, + filter: this.state.filter, + content: item, + key: itemKey, + listId: itemKey, + select: this.select, + properties: this.props.properties + }; + return React.createElement(this.props.element, props); + }); + return <>
- {items} + {items}
- - } -}) + ; + } +}); -module.exports = FilterList +module.exports = FilterList; diff --git a/src/components/info.js b/src/components/info.js index c2c26ea..54bf3b9 100644 --- a/src/components/info.js +++ b/src/components/info.js @@ -1,19 +1,19 @@ -'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 ReactDOM = require('react-dom'); +const create = require('create-react-class'); +const Promise = require('bluebird'); let info = create({ - displayName: "Info", - render: function() { - let title = "" - if (this.props.room != undefined) { - title = this.props.room.name - } - return
{title}
- } -}) + displayName: "Info", + render: function() { + let title = ""; + if (this.props.room != undefined) { + title = this.props.room.name; + } + return
{title}
; + } +}); -module.exports = info +module.exports = info; diff --git a/src/components/input.js b/src/components/input.js index 2860c0a..abc6ac9 100644 --- a/src/components/input.js +++ b/src/components/input.js @@ -1,277 +1,277 @@ -'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 ReactDOM = require('react-dom'); +const create = require('create-react-class'); +const Promise = require('bluebird'); +const colorConvert = require('color-convert'); +const sanitize = require('sanitize-html'); -const riot = require('../lib/riot-utils.js') -const FileUpload = require('./fileUpload.js') +const riot = require('../lib/riot-utils.js'); +const FileUpload = require('./fileUpload.js'); let input = create({ - displayName: "Input", + displayName: "Input", - getInitialState: function() { - return { - uploads: [] - } - }, + getInitialState: function() { + return { + uploads: [] + }; + }, - setRef: function(ref) { - if (ref !=null) { - ref.addEventListener("keydown", (e) => { - // only send on plain 'enter' - if (e.key == "Enter") { - if (!e.shiftKey && !e.altKey && !e.ctrlKey) { - this.send(e) - return - } - } - }) - ref.addEventListener('change', this.resize_textarea) - ref.addEventListener('cut', this.resize_textarea_delayed) - ref.addEventListener('paste', this.resize_textarea_delayed) - ref.addEventListener('drop', this.resize_textarea_delayed) - ref.addEventListener('keydown', this.resize_textarea_delayed) - this.setState({ref: ref}) - } - }, + setRef: function(ref) { + if (ref !=null) { + ref.addEventListener("keydown", (e) => { + // only send on plain 'enter' + if (e.key == "Enter") { + if (!e.shiftKey && !e.altKey && !e.ctrlKey) { + this.send(e); + return; + } + } + }); + ref.addEventListener('change', this.resize_textarea); + ref.addEventListener('cut', this.resize_textarea_delayed); + ref.addEventListener('paste', this.resize_textarea_delayed); + ref.addEventListener('drop', this.resize_textarea_delayed); + ref.addEventListener('keydown', this.resize_textarea_delayed); + this.setState({ref: ref}); + } + }, - addUpload: function(upload) { - let uploads = this.state.uploads - uploads.push(upload) - this.setState({uploads: uploads}) - }, + addUpload: function(upload) { + let uploads = this.state.uploads; + uploads.push(upload); + this.setState({uploads: uploads}); + }, - removeUpload: function(index) { - let uploads = this.state.uploads - uploads.splice(index, 1) - this.setState({uploads: uploads}) - }, + removeUpload: function(index) { + let uploads = this.state.uploads; + uploads.splice(index, 1); + this.setState({uploads: uploads}); + }, - resize_textarea: function(element) { - if (element == undefined) { - return; - } - let ref = element.target; - if (ref != undefined) { - ref.style.height = 'auto'; - ref.style.height = ref.scrollHeight+'px'; - } - }, + resize_textarea: function(element) { + if (element == undefined) { + return; + } + let ref = element.target; + if (ref != undefined) { + ref.style.height = 'auto'; + ref.style.height = ref.scrollHeight+'px'; + } + }, - resize_textarea_delayed: function(e) { - setTimeout(() => this.resize_textarea(e), 5); - }, + resize_textarea_delayed: function(e) { + setTimeout(() => this.resize_textarea(e), 5); + }, - send: function(e) { - let msg = e.target.value - if (msg.trim().length != 0) { - //TODO: parse markdown (commonmark?) - if (msg.startsWith('/')) { - // Handle other commands - let parts = msg.split(' ') - let command = parts[0] - let result = handleCommands(command, parts) - if (result != null) { - if (result.type == "html") { - this.sendHTML(result.content) - } else { - this.sendPlain(result.content) - } - } - } else { - this.sendPlain(msg) - } - } + send: function(e) { + let msg = e.target.value; + if (msg.trim().length != 0) { + //TODO: parse markdown (commonmark?) + if (msg.startsWith('/')) { + // Handle other commands + let parts = msg.split(' '); + let command = parts[0]; + let result = handleCommands(command, parts); + if (result != null) { + if (result.type == "html") { + this.sendHTML(result.content); + } else { + this.sendPlain(result.content); + } + } + } else { + this.sendPlain(msg); + } + } - if (this.state.uploads.length > 0) { - this.uploadFiles(this.state.uploads) - this.setState({uploads: []}) - } - e.target.value = "" - e.preventDefault() - this.resize_textarea_delayed(e) - }, + if (this.state.uploads.length > 0) { + this.uploadFiles(this.state.uploads); + this.setState({uploads: []}); + } + e.target.value = ""; + e.preventDefault(); + this.resize_textarea_delayed(e); + }, - uploadFiles: function(uploads) { - let client = this.props.client - Promise.map(uploads, (upload) => { - let fileUploadPromise = client.uploadContent(upload.file, - {onlyContentUri: false}).then((response) => { - return response.content_uri - }) + uploadFiles: function(uploads) { + let client = this.props.client; + Promise.map(uploads, (upload) => { + let fileUploadPromise = client.uploadContent(upload.file, + {onlyContentUri: false}).then((response) => { + return response.content_uri; + }); - let mimeType = upload.file.type - let eventType = "m.file" - let additionalPromise - if (mimeType.startsWith("image/") || mimeType.startsWith("video/")) { - function elementToThumbnail(element) { - return new Promise((resolve, reject) => { - riot.createThumbnail(element, - element.width, - element.height, - thumbnailType - ) - .catch((error) => { - console.error("neo: error getting thumbnail", error) - reject() - }) - .then((thumbResult) => { - return client.uploadContent(thumbResult.thumbnail, {onlyContentUri: false}) - }).then((response) => { - return resolve({ - thumbnail_url: response.content_uri, - thumbnail_info: { - mimetype: thumbnailType - } - }) - }) + let mimeType = upload.file.type; + let eventType = "m.file"; + let additionalPromise; + if (mimeType.startsWith("image/") || mimeType.startsWith("video/")) { + function elementToThumbnail(element) { + return new Promise((resolve, reject) => { + riot.createThumbnail(element, + element.width, + element.height, + thumbnailType + ) + .catch((error) => { + console.error("neo: error getting thumbnail", error); + reject(); + }) + .then((thumbResult) => { + return client.uploadContent(thumbResult.thumbnail, {onlyContentUri: false}); + }).then((response) => { + return resolve({ + thumbnail_url: response.content_uri, + thumbnail_info: { + mimetype: thumbnailType + } + }); + }); - }) - } - if (mimeType.startsWith("image/")) { - eventType = "m.image" - additionalPromise = riot.loadImageElement(upload.file) - .then((element) => {return elementToThumbnail(element)}) - } else if (mimeType.startsWith("video/")) { - eventType = "m.video" - additionalPromise = riot.loadVideoElement(upload.file) - .then((element) => {return elementToThumbnail(element)}) - } - // create and upload thumbnail - let thumbnailType = "image/png" - if (mimeType == "image/jpeg") { - thumbnailType = mimeType - } - } else if (mimeType.startsWith("audio/")) { - eventType = "m.audio" - } else { - // m.file - } - Promise.all([fileUploadPromise, additionalPromise]).then((result) => { - console.log(result) - let info = { - mimetype: mimeType - } - if (result[1] != undefined) { - info = Object.assign(info, result[1]) - } - client.sendEvent(this.props.roomId, "m.room.message", { - body: upload.file.name, - msgtype: eventType, - info: info, - url: result[0] - }) - }) - }) - }, + }); + } + if (mimeType.startsWith("image/")) { + eventType = "m.image"; + additionalPromise = riot.loadImageElement(upload.file) + .then((element) => {return elementToThumbnail(element);}); + } else if (mimeType.startsWith("video/")) { + eventType = "m.video"; + additionalPromise = riot.loadVideoElement(upload.file) + .then((element) => {return elementToThumbnail(element);}); + } + // create and upload thumbnail + let thumbnailType = "image/png"; + if (mimeType == "image/jpeg") { + thumbnailType = mimeType; + } + } else if (mimeType.startsWith("audio/")) { + eventType = "m.audio"; + } else { + // m.file + } + Promise.all([fileUploadPromise, additionalPromise]).then((result) => { + console.log(result); + let info = { + mimetype: mimeType + }; + if (result[1] != undefined) { + info = Object.assign(info, result[1]); + } + client.sendEvent(this.props.roomId, "m.room.message", { + body: upload.file.name, + msgtype: eventType, + info: info, + url: result[0] + }); + }); + }); + }, - sendPlain: function(string) { - let content = { - body: string, - msgtype: "m.text" - } - content = this.sendReply(content) - this.props.client.sendEvent(this.props.roomId, "m.room.message", content, (err, res) => { - if (err != null) { - console.log(err) - } - }) - }, + sendPlain: function(string) { + let content = { + body: string, + msgtype: "m.text" + }; + content = this.sendReply(content); + this.props.client.sendEvent(this.props.roomId, "m.room.message", content, (err, res) => { + if (err != null) { + console.log(err); + } + }); + }, - sendHTML: function(html) { - let content = { - body: sanitize(html, {allowedTags: []}), - formatted_body: html, - format: "org.matrix.custom.html", - msgtype: "m.text" - } + sendHTML: function(html) { + let content = { + body: sanitize(html, {allowedTags: []}), + 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) - }) - }, + this.props.client.sendEvent(this.props.roomId, "m.room.message", content, (err, res) => { + console.log(err); + }); + }, - sendReply: function(content) { - if (this.props.replyEvent != undefined) { - content['m.relates_to'] = { - 'm.in_reply_to': { - event_id: this.props.replyEvent.event_id - } - } - this.props.onReplyClick() - } - return content - }, + sendReply: function(content) { + if (this.props.replyEvent != undefined) { + content['m.relates_to'] = { + 'm.in_reply_to': { + event_id: this.props.replyEvent.event_id + } + }; + this.props.onReplyClick(); + } + return content; + }, - render: function() { - return
- {this.props.replyEvent && + render: function() { + return
+ {this.props.replyEvent &&
this.props.onReplyClick()}> - {this.props.replyEvent.plaintext()} + {this.props.replyEvent.plaintext()}
- } - {this.state.uploads.length > 0 && + } + {this.state.uploads.length > 0 &&
- {this.state.uploads.map((upload, key) => { - return ( -
- - this.removeUpload(key)}>X -
- ) - })} + {this.state.uploads.map((upload, key) => { + return ( +
+ + this.removeUpload(key)}>X +
+ ); + })}
- } -
- - -
-
- } -}) + } +
+ + +
+
; + } +}); function handleCommands(command, parts) { - if (command == "/rainbow") { - if (parts.length < 2) { - return - } - let string = parts[1] - for(let i=2; i < parts.length; i++) { - string += " " + parts[i] - } - let html = rainbowTransform(string) - return { - type: 'html', - content: html - } - } - return null + if (command == "/rainbow") { + if (parts.length < 2) { + return; + } + let string = parts[1]; + for(let i=2; i < parts.length; i++) { + string += " " + parts[i]; + } + let html = rainbowTransform(string); + return { + type: 'html', + content: html + }; + } + return null; } function rainbowTransform(text) { - let array = text.split(""); - let delta = 360/text.length; - if (delta < 10) { - delta = 10; - } else if (delta > 20) { - delta = 20; - } - let h = -1 * delta; // start at beginning + let array = text.split(""); + let delta = 360/text.length; + if (delta < 10) { + delta = 10; + } else if (delta > 20) { + delta = 20; + } + let h = -1 * delta; // start at beginning - let rainbowArray = array.map((char) => { - h = h + delta; - if (h > 360) { - h = 0; - } - return `${char}`; - }); - let rainbow = rainbowArray.join(""); - return rainbow; + let rainbowArray = array.map((char) => { + h = h + delta; + if (h > 360) { + h = 0; + } + return `${char}`; + }); + let rainbow = rainbowArray.join(""); + return rainbow; } -module.exports = input +module.exports = input; diff --git a/src/components/loading.js b/src/components/loading.js index 0731ad2..8056a74 100644 --- a/src/components/loading.js +++ b/src/components/loading.js @@ -1,20 +1,20 @@ -'use strict' -const React = require('react') -const ReactDOM = require('react-dom') -const create = require('create-react-class') +'use strict'; +const React = require('react'); +const ReactDOM = require('react-dom'); +const create = require('create-react-class'); let Loading = create({ - displayName: "Loading", + displayName: "Loading", - render: function() { - return ( -
-
-
-
-
- ) - } -}) + render: function() { + return ( +
+
+
+
+
+ ); + } +}); -module.exports = Loading +module.exports = Loading; diff --git a/src/components/sidebar.js b/src/components/sidebar.js index 0200359..b90d571 100644 --- a/src/components/sidebar.js +++ b/src/components/sidebar.js @@ -1,117 +1,117 @@ -'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 ReactDOM = require('react-dom'); +const create = require('create-react-class'); +const Promise = require('bluebird'); +const debounce = require('debounce'); +const jdenticon = require('jdenticon'); -const FilterList = require('./filterList.js') +const FilterList = require('./filterList.js'); let RoomListItem = create({ - displayName: "RoomListItem", - - getInitialState: function() { - let room = this.props.content - let client = this.props.properties.client - let jdenticon = - let avatarUrl - - let roomState = room.getLiveTimeline().getState('f') - let avatarState = roomState.getStateEvents('m.room.avatar') - if (avatarState.length > 0) { - let event = avatarState[avatarState.length-1].event - let hs = client.baseUrl - let media_mxc = event.content.url.slice(6) - let path = `/_matrix/media/v1/thumbnail/${media_mxc}?width=128&height=128&method=scale` - avatarUrl = { - hs: hs, - path: path - } - } - - return { - filterName: room.name.toUpperCase(), - unread: Math.random() > 0.7, - avatarUrl: avatarUrl, - jdenticon: jdenticon, - tries: 0 - } - }, - - jdenticonRef: function(ref) { - jdenticon.update(ref, this.props.content.roomId) - }, - - avatarFallback: function() { - // instead of falling back on jdenticon immediately, we can try - // a third-party homeserver's media repo - // this does come with trust issues, and is opt-in in settings - let fallbackMediaRepos = this.props.properties.options.fallbackMediaRepos - - if (this.state.tries < fallbackMediaRepos.length) { - let avatarUrl = this.state.avatarUrl - avatarUrl.hs = fallbackMediaRepos[this.state.tries] - this.setState({ - avatarUrl: avatarUrl, - tries: this.state.tries + 1 - }) - } else { - this.setState({avatarUrl: null, avatar: jdenticon}) - } - }, - - setRef: function(ref) { - if (ref == null) { - return - } - this.setState({ref: ref}) - ref.addEventListener("click", () => {this.props.select(this.props.listId)}) - }, - - render: function() { - if (this.state.filterName.indexOf(this.props.filter) == -1) { - return null - } - let className = "roomListItem" - if (this.props.selected) { - className += " active" - } - if (this.state.unread) { - className += " unread" - } - return
- {this.state.avatarUrl ? - - : - this.state.jdenticon - } - {this.props.content.name} -
- } -}) + displayName: "RoomListItem", + + getInitialState: function() { + let room = this.props.content; + let client = this.props.properties.client; + let jdenticon = ; + let avatarUrl; + + let roomState = room.getLiveTimeline().getState('f'); + let avatarState = roomState.getStateEvents('m.room.avatar'); + if (avatarState.length > 0) { + let event = avatarState[avatarState.length-1].event; + let hs = client.baseUrl; + let media_mxc = event.content.url.slice(6); + let path = `/_matrix/media/v1/thumbnail/${media_mxc}?width=128&height=128&method=scale`; + avatarUrl = { + hs: hs, + path: path + }; + } + + return { + filterName: room.name.toUpperCase(), + unread: Math.random() > 0.7, + avatarUrl: avatarUrl, + jdenticon: jdenticon, + tries: 0 + }; + }, + + jdenticonRef: function(ref) { + jdenticon.update(ref, this.props.content.roomId); + }, + + avatarFallback: function() { + // instead of falling back on jdenticon immediately, we can try + // a third-party homeserver's media repo + // this does come with trust issues, and is opt-in in settings + let fallbackMediaRepos = this.props.properties.options.fallbackMediaRepos; + + if (this.state.tries < fallbackMediaRepos.length) { + let avatarUrl = this.state.avatarUrl; + avatarUrl.hs = fallbackMediaRepos[this.state.tries]; + this.setState({ + avatarUrl: avatarUrl, + tries: this.state.tries + 1 + }); + } else { + this.setState({avatarUrl: null, avatar: jdenticon}); + } + }, + + setRef: function(ref) { + if (ref == null) { + return; + } + this.setState({ref: ref}); + ref.addEventListener("click", () => {this.props.select(this.props.listId);}); + }, + + render: function() { + if (this.state.filterName.indexOf(this.props.filter) == -1) { + return null; + } + let className = "roomListItem"; + if (this.props.selected) { + className += " active"; + } + if (this.state.unread) { + className += " unread"; + } + return
+ {this.state.avatarUrl ? + + : + this.state.jdenticon + } + {this.props.content.name} +
; + } +}); let Sidebar = create({ - displayName: "Sidebar", + displayName: "Sidebar", - getInitialState: function() { - return { - filter: "" - } - }, + getInitialState: function() { + return { + filter: "" + }; + }, - setFilter: function(filter) { - this.setState({ - filter: filter.toUpperCase() - }) - }, + setFilter: function(filter) { + this.setState({ + filter: filter.toUpperCase() + }); + }, - render: function() { - return
- {this.props.selectRoom(roomId)}}/> -
- } -}) + render: function() { + return
+ {this.props.selectRoom(roomId);}}/> +
; + } +}); -module.exports = Sidebar +module.exports = Sidebar; diff --git a/src/lib/media.js b/src/lib/media.js index a85ecfe..d362ed0 100644 --- a/src/lib/media.js +++ b/src/lib/media.js @@ -1,56 +1,56 @@ // should be able to handle images, stickers, and video module.exports = { - parseEvent: function(client, event, maxHeight, maxWidth) { - if (event.content.msgtype == "m.image") { - let h = maxHeight - let w = maxWidth - - let media_url = client.mxcUrlToHttp(event.content.url) - let thumb_url = event.content.url - - if (event.content.info != null) { - if (event.content.info.thumbnail_url != null) { - thumb_url = event.content.info.thumbnail_url - } - - if (event.content.info.thumbnail_info != null) { - h = (event.content.info.thumbnail_info.h < maxHeight) ? event.content.info.thumbnail_info.h : h - w = (event.content.info.thumbnail_info.w < maxWidth) ? event.content.info.thumbnail_info.w : w - } else { - h = (event.content.info.h < maxHeight) ? event.content.info.h : h - w = (event.content.info.w < maxWidth) ? event.content.info.w : w - } - } - - thumb_url = client.mxcUrlToHttp(thumb_url, w, h, 'scale', false) - - return { - full: media_url, - thumb: thumb_url, - size: {h, w} - } - } - if (event.content.msgtype == "m.video") { - let thumb = null - let h = maxHeight - let w = maxWidth - - if (event.content.info != null) { - if (event.content.info.thumbnail_url != null) { - if (event.content.info.thumbnail_info != null) { - h = (event.content.info.thumbnail_info.h < maxHeight) ? event.content.info.thumbnail_info.h : h - w = (event.content.info.thumbnail_info.w < maxWidth) ? event.content.info.thumbnail_info.w : w - } - - thumb = client.mxcUrlToHttp(event.content.thumbnail, w, h, 'scale', false) - } - } - return { - full: client.mxcUrlToHttp(event.content.url), - thumb: thumb, - size: {h, w} - } - } - } -} + parseEvent: function(client, event, maxHeight, maxWidth) { + if (event.content.msgtype == "m.image") { + let h = maxHeight; + let w = maxWidth; + + let media_url = client.mxcUrlToHttp(event.content.url); + let thumb_url = event.content.url; + + if (event.content.info != null) { + if (event.content.info.thumbnail_url != null) { + thumb_url = event.content.info.thumbnail_url; + } + + if (event.content.info.thumbnail_info != null) { + h = (event.content.info.thumbnail_info.h < maxHeight) ? event.content.info.thumbnail_info.h : h; + w = (event.content.info.thumbnail_info.w < maxWidth) ? event.content.info.thumbnail_info.w : w; + } else { + h = (event.content.info.h < maxHeight) ? event.content.info.h : h; + w = (event.content.info.w < maxWidth) ? event.content.info.w : w; + } + } + + thumb_url = client.mxcUrlToHttp(thumb_url, w, h, 'scale', false); + + return { + full: media_url, + thumb: thumb_url, + size: {h, w} + }; + } + if (event.content.msgtype == "m.video") { + let thumb = null; + let h = maxHeight; + let w = maxWidth; + + if (event.content.info != null) { + if (event.content.info.thumbnail_url != null) { + if (event.content.info.thumbnail_info != null) { + h = (event.content.info.thumbnail_info.h < maxHeight) ? event.content.info.thumbnail_info.h : h; + w = (event.content.info.thumbnail_info.w < maxWidth) ? event.content.info.thumbnail_info.w : w; + } + + thumb = client.mxcUrlToHttp(event.content.thumbnail, w, h, 'scale', false); + } + } + return { + full: client.mxcUrlToHttp(event.content.url), + thumb: thumb, + size: {h, w} + }; + } + } +}; diff --git a/src/lib/riot-utils.js b/src/lib/riot-utils.js index 025c202..a80ad8c 100644 --- a/src/lib/riot-utils.js +++ b/src/lib/riot-utils.js @@ -37,193 +37,193 @@ const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; */ module.exports = { - createThumbnail: function(element, inputWidth, inputHeight, mimeType) { - return new Promise(function(resolve, reject) { - 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; - canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); - canvas.toBlob(function(thumbnail) { - resolve({ - info: { - thumbnail_info: { - w: targetWidth, - h: targetHeight, - mimetype: thumbnail.type, - size: thumbnail.size, - }, - w: inputWidth, - h: inputHeight, - }, - thumbnail: thumbnail, - }); - }, mimeType); - }); - }, - - /** + createThumbnail: function(element, inputWidth, inputHeight, mimeType) { + return new Promise(function(resolve, reject) { + 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; + canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); + canvas.toBlob(function(thumbnail) { + resolve({ + info: { + thumbnail_info: { + w: targetWidth, + h: targetHeight, + mimetype: thumbnail.type, + size: thumbnail.size, + }, + w: inputWidth, + h: inputHeight, + }, + thumbnail: thumbnail, + }); + }, mimeType); + }); + }, + + /** * Load a file into a newly created image element. * * @param {File} file The file to load in an image element. * @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 - const img = document.createElement("img"); - const objectUrl = URL.createObjectURL(imageFile); - img.src = objectUrl; - - // Once ready, create a thumbnail - img.onload = function() { - URL.revokeObjectURL(objectUrl); - resolve(img); - }; - img.onerror = function(e) { - reject(e); - }; - }); - }, - - /** + loadImageElement: function(imageFile) { + return new Promise(function(resolve, reject) { + // Load the file into an html element + const img = document.createElement("img"); + const objectUrl = URL.createObjectURL(imageFile); + img.src = objectUrl; + + // Once ready, create a thumbnail + img.onload = function() { + URL.revokeObjectURL(objectUrl); + resolve(img); + }; + img.onerror = function(e) { + reject(e); + }; + }); + }, + + /** * Load a file into a newly created video element. * * @param {File} file The file to load in an video element. * @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; - - // 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); - }); - }, - - sanitize: function(html) { - return sanitize(html, this.sanitizeHtmlParams); - }, - - sanitizeHtmlParams: { - allowedTags: [ - 'font', // custom to matrix for IRC-style font coloring - 'del', // for markdown - 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'a', 'ul', 'ol', 'sup', 'sub', - 'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', - 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img', - 'mx-reply', 'mx-rainbow' - ], - allowedAttributes: { - // custom ones first: - 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 - a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix - img: ['src', 'width', 'height', 'alt', 'title'], - ol: ['start'], - 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 - selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'], - // URL schemes we permit - allowedSchemes: ['http', 'https', 'ftp', 'mailto', 'magnet'], - - allowProtocolRelative: false, - - transformTags: { // custom to matrix - // add blank targets to all hyperlinks except vector URLs - '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. - //if (!attribs.src || !attribs.src.startsWith('mxc://')) { - return { tagName, attribs: {}}; - //} - //attribs.src = MatrixClientPeg.get().mxcUrlToHttp( - // attribs.src, - // attribs.width || 800, - // attribs.height || 600 - //); - //return { tagName: tagName, attribs: attribs }; - }, - - 'code': function(tagName, attribs) { - if (typeof attribs.class !== 'undefined') { - // Filter out all classes other than ones starting with language- for syntax highlighting. - const classes = attribs.class.split(/\s+/).filter(function(cl) { - return cl.startsWith('language-'); - }); - attribs.class = classes.join(' '); - } - return { - tagName: tagName, - attribs: attribs, - }; - }, - - '*': function(tagName, attribs) { - // Delete any style previously assigned, style is an allowedTag for font and span - // because attributes are stripped after transforming - delete attribs.style; - - // Sanitise and transform data-mx-color and data-mx-bg-color to their CSS - // equivalents - const customCSSMapper = { - 'data-mx-color': 'color', - 'data-mx-bg-color': 'background-color', - // $customAttributeKey: $cssAttributeKey - }; - - let style = ""; - Object.keys(customCSSMapper).forEach((customAttributeKey) => { - const cssAttributeKey = customCSSMapper[customAttributeKey]; - const customAttributeValue = attribs[customAttributeKey]; - if (customAttributeValue && + 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; + + // 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); + }); + }, + + sanitize: function(html) { + return sanitize(html, this.sanitizeHtmlParams); + }, + + sanitizeHtmlParams: { + allowedTags: [ + 'font', // custom to matrix for IRC-style font coloring + 'del', // for markdown + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'a', 'ul', 'ol', 'sup', 'sub', + 'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', + 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img', + 'mx-reply', 'mx-rainbow' + ], + allowedAttributes: { + // custom ones first: + 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 + a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix + img: ['src', 'width', 'height', 'alt', 'title'], + ol: ['start'], + 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 + selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'], + // URL schemes we permit + allowedSchemes: ['http', 'https', 'ftp', 'mailto', 'magnet'], + + allowProtocolRelative: false, + + transformTags: { // custom to matrix + // add blank targets to all hyperlinks except vector URLs + '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. + //if (!attribs.src || !attribs.src.startsWith('mxc://')) { + return { tagName, attribs: {}}; + //} + //attribs.src = MatrixClientPeg.get().mxcUrlToHttp( + // attribs.src, + // attribs.width || 800, + // attribs.height || 600 + //); + //return { tagName: tagName, attribs: attribs }; + }, + + 'code': function(tagName, attribs) { + if (typeof attribs.class !== 'undefined') { + // Filter out all classes other than ones starting with language- for syntax highlighting. + const classes = attribs.class.split(/\s+/).filter(function(cl) { + return cl.startsWith('language-'); + }); + attribs.class = classes.join(' '); + } + return { + tagName: tagName, + attribs: attribs, + }; + }, + + '*': function(tagName, attribs) { + // Delete any style previously assigned, style is an allowedTag for font and span + // because attributes are stripped after transforming + delete attribs.style; + + // Sanitise and transform data-mx-color and data-mx-bg-color to their CSS + // equivalents + const customCSSMapper = { + 'data-mx-color': 'color', + 'data-mx-bg-color': 'background-color', + // $customAttributeKey: $cssAttributeKey + }; + + let style = ""; + Object.keys(customCSSMapper).forEach((customAttributeKey) => { + const cssAttributeKey = customCSSMapper[customAttributeKey]; + const customAttributeValue = attribs[customAttributeKey]; + if (customAttributeValue && typeof customAttributeValue === 'string' && COLOR_REGEX.test(customAttributeValue) - ) { - style += cssAttributeKey + ":" + customAttributeValue + ";"; - delete attribs[customAttributeKey]; - } - }); - - if (style) { - attribs.style = style; - } - - return { tagName: tagName, attribs: attribs }; - }, - }, - } + ) { + style += cssAttributeKey + ":" + customAttributeValue + ";"; + delete attribs[customAttributeKey]; + } + }); + + if (style) { + attribs.style = style; + } + + return { tagName: tagName, attribs: attribs }; + }, + }, + } }; diff --git a/tests/lib/media.js b/tests/lib/media.js index 441cb0b..68d376b 100644 --- a/tests/lib/media.js +++ b/tests/lib/media.js @@ -1,89 +1,89 @@ -let assert = require('assert') -let urllib = require('url') -let querystring = require('querystring') +let assert = require('assert'); +let urllib = require('url'); +let querystring = require('querystring'); -let mediaLib = require('../../lib/media.js') +let mediaLib = require('../../lib/media.js'); let client = { - mxcUrlToHttp: function(url, w, h, method, allowDirectLinks) { - let hs = "https://chat.privacytools.io" - let mxc = url.slice(6) - if (w) { - return `${hs}/_matrix/media/v1/thumbnail/${mxc}?w=${w}&h=${h}&method=${method}` - } else { - return `${hs}/_matrix/media/v1/download/${mxc}` - } - } -} + mxcUrlToHttp: function(url, w, h, method, allowDirectLinks) { + let hs = "https://chat.privacytools.io"; + let mxc = url.slice(6); + if (w) { + return `${hs}/_matrix/media/v1/thumbnail/${mxc}?w=${w}&h=${h}&method=${method}`; + } else { + return `${hs}/_matrix/media/v1/download/${mxc}`; + } + } +}; let mockEventTemplate = { - type: "m.room.message", - sender: "@f0x:privacytools.io", - content: { - body: "image.png", - info: { - size: 16692, - mimetype: "image/png", - thumbnail_info: { - w: 268, - h: 141, - mimetype: "image/png", - size: 16896 - }, - w: 268, - h: 141, - thumbnail_url: "mxc://privacytools.io/zBSerdKMhaXSIxfjzCmOnhXH" - }, - msgtype: "m.image", - url: "mxc://privacytools.io/khPaFfeRyNdzlSttZraeAUre" - }, - event_id: "$aaa:matrix.org", - origin_server_ts: 1558470168199, - unsigned: { - age: 143237861 - }, - room_id: "!aaa:matrix.org" -} + type: "m.room.message", + sender: "@f0x:privacytools.io", + content: { + body: "image.png", + info: { + size: 16692, + mimetype: "image/png", + thumbnail_info: { + w: 268, + h: 141, + mimetype: "image/png", + size: 16896 + }, + w: 268, + h: 141, + thumbnail_url: "mxc://privacytools.io/zBSerdKMhaXSIxfjzCmOnhXH" + }, + msgtype: "m.image", + url: "mxc://privacytools.io/khPaFfeRyNdzlSttZraeAUre" + }, + event_id: "$aaa:matrix.org", + origin_server_ts: 1558470168199, + unsigned: { + age: 143237861 + }, + room_id: "!aaa:matrix.org" +}; describe('media', function() { - describe('#parseEvent()', function() { - it('event without info', function() { - let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate)) - mockEvent.content.info = null + describe('#parseEvent()', function() { + it('event without info', function() { + let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate)); + mockEvent.content.info = null; - checkParsedEvent(mockEvent, { - w: 1000, - h: 1000, - method: 'scale' - }) - }), - it('event without thumbnail', function() { - let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate)) - mockEvent.content.info.thumbnail_url = null - mockEvent.content.info.thumbnail_info = null - checkParsedEvent(mockEvent, { - w: 268, - h: 141, - method: 'scale' - }) - }) - it('event without thumbnail_info', function() { - let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate)) - mockEvent.content.info.thumbnail_url = null - checkParsedEvent(mockEvent, { - w: 268, - h: 141, - method: 'scale' - }) - }) - }) -}) + checkParsedEvent(mockEvent, { + w: 1000, + h: 1000, + method: 'scale' + }); + }), + it('event without thumbnail', function() { + let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate)); + mockEvent.content.info.thumbnail_url = null; + mockEvent.content.info.thumbnail_info = null; + checkParsedEvent(mockEvent, { + w: 268, + h: 141, + method: 'scale' + }); + }); + it('event without thumbnail_info', function() { + let mockEvent = JSON.parse(JSON.stringify(mockEventTemplate)); + mockEvent.content.info.thumbnail_url = null; + checkParsedEvent(mockEvent, { + w: 268, + h: 141, + method: 'scale' + }); + }); + }); +}); function checkParsedEvent(mockEvent, expected) { - let media = mediaLib.parseEvent(client, mockEvent, 1000, 1000) - let params = querystring.decode(urllib.parse(media.thumb).query) + let media = mediaLib.parseEvent(client, mockEvent, 1000, 1000); + let params = querystring.decode(urllib.parse(media.thumb).query); - Object.keys(params).forEach((key) => { - assert.equal(expected[key], params[key]) - }) + Object.keys(params).forEach((key) => { + assert.equal(expected[key], params[key]); + }); }