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

refactor
Sven Slootweg 5 years ago
parent b7f794ffa6
commit aca6f3768d

@ -1,88 +1,88 @@
const gulp = require('gulp')
const 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();
}]));

@ -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 <Login callback={this.loginCallback}/>
}
return (
render: function() {
if (this.state.client == undefined) {
//Login screen
return <Login callback={this.loginCallback}/>;
}
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')
)
<App />,
document.getElementById('root')
);

@ -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 (
<div className="loginwrapper">
<img src="./neo.png"/>
<div className="errorMessage">{this.state.error}</div>
<div className="login">
<label htmlFor="user">Username: </label>
<input type="text" id="user" placeholder="@user:homeserver.tld" value={this.state.formState["user"]} onChange={this.handleUserChange}/>
<label htmlFor="pass">Password: </label>
<input type="password" id="pass" placeholder="password" value={this.state.formState["pass"]} onChange={this.handlePassChange}/>
<label htmlFor="hs" className={hsState}>Homeserver: </label>
{this.state.hs.prompt ? (
displayName: "Login",
getInitialState: function() {
return {
error: null,
formState: {
user: "",
pass: "",
hs: ""
},
hs: {
prompt: false,
error: null,
valid: false
}
};
},
login: function() {
this.setState({error: ""});
if (this.state.hs.valid) {
return this.doLogin();
}
let parts = this.state.formState.user.split(':');
if (parts.length != 2) {
return this.setState({error: "Please enter a full mxid, like username:homeserver.tld"});
}
let hostname = urllib.parse("https://" + parts[1]);
getApiServer(hostname).then((hs) => {
console.log("Using API server", hs);
let formState = this.state.formState;
formState.user = parts[0];
formState.hs = hs;
let hsState = Object.assign(this.state.hs, {valid: true});
this.setState({apiUrl: hs, formState: formState, hs: hsState});
this.doLogin();
}).catch((error) => {
console.log("ERROR fetching homeserver url", error);
let hsState = Object.assign(this.state.hs, {error: error, valid: false, prompt: true});
this.setState({hs: hsState});
});
},
doLogin: function() {
console.log("Logging in");
let user = this.state.formState.user.replace('@', '');
let password = this.state.formState.pass;
let hs = this.state.apiUrl;
let data = {
user: user,
password: password,
type: "m.login.password",
initial_device_display_name: "Neo v4",
};
let url = hs + "/_matrix/client/r0/login";
fetch(url, {
body: JSON.stringify(data),
headers: {
'content-type': 'application/json'
},
method: 'POST',
}).then((response) => response.json())
.then((responseJson) => {
console.log("got access token", responseJson);
this.setState({json: responseJson});
if(responseJson.access_token != undefined) {
this.props.callback(responseJson.user_id, responseJson.access_token, hs);
} else {
this.setState({error: responseJson.error});
}
})
.catch((error) => {
console.error(url, error);
});
},
handleUserChange: function(e) {
let formState = this.state.formState;
let user = e.target.value;
formState.user = e.target.value;
let parts = user.split(':');
if (parts.length == 2) {
formState.hs = parts[1];
let hsState = Object.assign(this.state.hs, {error: null, valid: false});
this.setState({hs: hsState});
}
this.setState({formState: formState});
},
handlePassChange: function(e) {
let formState = this.state.formState;
formState.pass = e.target.value;
this.setState({formState: formState});
},
handleHsChange: function(e) {
let formState = this.state.formState;
formState.hs = e.target.value;
this.setState({formState: formState});
this.setState({hs: {error: null, valid: false, prompt: true, changed: true}});
},
render: function() {
let hsState = "inactive";
if (this.state.hs.prompt) {
hsState = "active";
}
if (this.state.hs.error != null) {
hsState = "error";
}
if (this.state.hs.valid) {
hsState = "validated";
}
return (
<div className="loginwrapper">
<img src="./neo.png"/>
<div className="errorMessage">{this.state.error}</div>
<div className="login">
<label htmlFor="user">Username: </label>
<input type="text" id="user" placeholder="@user:homeserver.tld" value={this.state.formState["user"]} onChange={this.handleUserChange}/>
<label htmlFor="pass">Password: </label>
<input type="password" id="pass" placeholder="password" value={this.state.formState["pass"]} onChange={this.handlePassChange}/>
<label htmlFor="hs" className={hsState}>Homeserver: </label>
{this.state.hs.prompt ? (
<>
<input type="text" id="hs" value={this.state.formState["hs"]} onChange={this.handleHsChange}/>
</>
) : (
<span id="hs">{this.state.formState["hs"]}</span>
)}
) : (
<span id="hs">{this.state.formState["hs"]}</span>
)}
<button onClick={()=>this.login()}>Log in</button>
</div>
</div>
)
}
})
<button onClick={()=>this.login()}>Log in</button>
</div>
</div>
);
}
});
function getApiServer(hostname) {
return new Promise((resolve, reject) => {
console.log("Checking for api server from mxid", urllib.format(hostname))
checkApi(hostname).then(() => {
// Hostname is a valid api server
hostname.pathname = ""
resolve(urllib.format(hostname))
}).catch(() => {
console.log("trying .well-known")
tryWellKnown(hostname).then((hostname) => {
console.log("got .well-known host", hostname)
resolve(hostname)
}).catch((err) => {
reject("Fatal error trying to get API host")
})
})
})
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;

@ -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 = (
<div className="main">
</div>
)
if (this.props.roomId == undefined) {
//empty screen
return empty
}
let room = client.getRoom(this.props.roomId)
if (room == null) {
return empty
}
let messageGroups = {
current: [],
groups: [],
sender: "",
type: ""
}
// if the sender is the same, add it to the 'current' messageGroup, if not,
// push the old one to 'groups' and start with a new array.
let liveTimeline = room.getLiveTimeline()
let liveTimelineEvents = liveTimeline.getEvents()
let events = []
if (liveTimelineEvents.length > 0) {
liveTimelineEvents.forEach((MatrixEvent) => {
let event = MatrixEvent.event;
event = Object.assign(event, eventFunctions)
if (event.sender == null) { // localecho messages
event.sender = event.user_id
event.local = true
}
if (event.sender != messageGroups.sender || event.type != messageGroups.type) {
messageGroups.sender = event.sender
messageGroups.type = event.type
if (messageGroups.current.length != 0) {
messageGroups.groups.push(messageGroups.current)
}
messageGroups.current = []
}
messageGroups.current.push(event)
})
messageGroups.groups.push(messageGroups.current)
events = messageGroups.groups.map((events, id) => {
return <EventGroup key={`${this.props.roomId}-${events[0].event_id}`} events={events} client={this.props.client} room={room} onReplyClick={this.onReplyClick}/>
})
}
//TODO: replace with something that only renders events in view
return (
<div className="main">
<Info room={room} />
<div className="chat" ref={this.setRef}>
<div className="events">
<div className="paginateBackwards" onClick={this.paginateBackwards}>
{this.state.loading ?
<Loading/> :
<span>load older messages</span>
}
</div>
{events}
</div>
</div>
<Input client={client} roomId={this.props.roomId} replyEvent={this.state.replyEvent} onReplyClick={this.onReplyClick}/>
</div>
)
}
})
displayName: "Chat",
getInitialState: function() {
return {
ref: null,
loading: false
};
},
getSnapshotBeforeUpdate: function(oldProps, oldState) {
let ref = this.state.ref;
if (ref == null) {return null;}
if ((ref.scrollHeight - ref.offsetHeight) - ref.scrollTop < 100) { // Less than 100px from bottom
return true;
}
return null;
},
componentDidUpdate(prevProps, prevState, snapshot) {
let ref = this.state.ref;
if (ref == null) {return;}
if (snapshot) { // scroll to bottom
ref.scrollTop = (ref.scrollHeight - ref.offsetHeight);
}
},
setRef: function(ref) {
if (ref != null) {
this.setState({ref: ref});
}
},
onReplyClick: function(e) {
this.setState({replyEvent: e});
},
paginateBackwards: function() {
if (this.state.loading) {
return;
}
let client = this.props.client;
client.paginateEventTimeline(client.getRoom(this.props.roomId).getLiveTimeline(), {backwards: true}).then(() => {
this.setState({loading: false});
});
this.setState({loading: true});
},
render: function() {
let client = this.props.client;
let empty = (
<div className="main">
</div>
);
if (this.props.roomId == undefined) {
//empty screen
return empty;
}
let room = client.getRoom(this.props.roomId);
if (room == null) {
return empty;
}
let messageGroups = {
current: [],
groups: [],
sender: "",
type: ""
};
// if the sender is the same, add it to the 'current' messageGroup, if not,
// push the old one to 'groups' and start with a new array.
let liveTimeline = room.getLiveTimeline();
let liveTimelineEvents = liveTimeline.getEvents();
let events = [];
if (liveTimelineEvents.length > 0) {
liveTimelineEvents.forEach((MatrixEvent) => {
let event = MatrixEvent.event;
event = Object.assign(event, eventFunctions);
if (event.sender == null) { // localecho messages
event.sender = event.user_id;
event.local = true;
}
if (event.sender != messageGroups.sender || event.type != messageGroups.type) {
messageGroups.sender = event.sender;
messageGroups.type = event.type;
if (messageGroups.current.length != 0) {
messageGroups.groups.push(messageGroups.current);
}
messageGroups.current = [];
}
messageGroups.current.push(event);
});
messageGroups.groups.push(messageGroups.current);
events = messageGroups.groups.map((events, id) => {
return <EventGroup key={`${this.props.roomId}-${events[0].event_id}`} events={events} client={this.props.client} room={room} onReplyClick={this.onReplyClick}/>;
});
}
//TODO: replace with something that only renders events in view
return (
<div className="main">
<Info room={room} />
<div className="chat" ref={this.setRef}>
<div className="events">
<div className="paginateBackwards" onClick={this.paginateBackwards}>
{this.state.loading ?
<Loading/> :
<span>load older messages</span>
}
</div>
{events}
</div>
</div>
<Input client={client} roomId={this.props.roomId} replyEvent={this.state.replyEvent} onReplyClick={this.onReplyClick}/>
</div>
);
}
});
let EventGroup = create({
displayName: "EventGroup",
getInitialState: function() {
let user = this.props.client.getUser(this.props.events[0].sender)
let avatar = <svg id="avatar" ref={this.avatarRef} />
if (user.avatarUrl != null) {
let hs = this.props.client.baseUrl
let media_mxc = user.avatarUrl.slice(6)
let url = `${hs}/_matrix/media/v1/thumbnail/${media_mxc}?width=128&height=128&method=scale`
avatar = <img id="avatar" src={url}/>
}
return {
user: user,
avatar: avatar
}
},
avatarRef: function(ref) {
jdenticon.update(ref, this.state.user.userId)
},
render: function() {
let events = this.props.events.map((event, key) => {
return <Event event={event} key={key} client={this.props.client} room={this.props.room} onReplyClick={this.props.onReplyClick}/>
})
return <div className="eventGroup">
{this.state.avatar}
<div className="col">
<User user={this.state.user}/>
{events}
</div>
</div>
}
})
module.exports = chat
displayName: "EventGroup",
getInitialState: function() {
let user = this.props.client.getUser(this.props.events[0].sender);
let avatar = <svg id="avatar" ref={this.avatarRef} />;
if (user.avatarUrl != null) {
let hs = this.props.client.baseUrl;
let media_mxc = user.avatarUrl.slice(6);
let url = `${hs}/_matrix/media/v1/thumbnail/${media_mxc}?width=128&height=128&method=scale`;
avatar = <img id="avatar" src={url}/>;
}
return {
user: user,
avatar: avatar
};
},
avatarRef: function(ref) {
jdenticon.update(ref, this.state.user.userId);
},
render: function() {
let events = this.props.events.map((event, key) => {
return <Event event={event} key={key} client={this.props.client} room={this.props.room} onReplyClick={this.props.onReplyClick}/>;
});
return <div className="eventGroup">
{this.state.avatar}
<div className="col">
<User user={this.state.user}/>
{events}
</div>
</div>;
}
});
module.exports = chat;

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

@ -1,44 +1,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 (
<div className="body">
<a href={this.state.full} target="_blank">
<img src={this.state.thumb} style={{maxHeight: this.state.size.h, maxWidth: this.state.size.w}}/>
</a>
</div>
)
}
})
displayName: "m.image",
getInitialState: function() {
let event = this.props.event;
if (event.content.url == undefined) {
return null;
}
return mediaLib.parseEvent(this.props.client, event, 1000, 1000);
},
updateSize: function(e) {
console.log("image was loaded");
},
render: function() {
let event = this.props.event;
if (this.state == null) {
return "malformed image event: " + event.content.body;
}
return (
<div className="body">
<a href={this.state.full} target="_blank">
<img src={this.state.thumb} style={{maxHeight: this.state.size.h, maxWidth: this.state.size.w}}/>
</a>
</div>
);
}
});
module.exports = Event;

@ -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 (
<div className="body">
{event.plaintext()}
</div>
)
}
})
render: function() {
let event = this.props.event;
return (
<div className="body">
{event.plaintext()}
</div>
);
}
});
module.exports = Event
module.exports = Event;

@ -1,43 +1,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 = <div
className="body"
dangerouslySetInnerHTML={{__html: formattedEvent.body}}
/>
} else {
eventBody =
if (formattedEvent.html) {
eventBody = <div
className="body"
dangerouslySetInnerHTML={{__html: formattedEvent.body}}
/>;
} else {
eventBody =
<div className="body">
{formattedEvent.body}
</div>
}
{formattedEvent.body}
</div>;
}
let eventClass = ""
if (event.local) {
eventClass += " local"
}
let eventClass = "";
if (event.local) {
eventClass += " local";
}
return <div className={eventClass}>
{eventBody}
</div>
}
})
return <div className={eventClass}>
{eventBody}
</div>;
}
});
module.exports = Event
module.exports = Event;

@ -1,49 +1,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 (
<div className="user" style={{color: this.state.color}}>
{this.props.user.displayName}
</div>
)
}
})
render: function() {
return (
<div className="user" style={{color: this.state.color}}>
{this.props.user.displayName}
</div>
);
}
});
module.exports = User
module.exports = User;

@ -1,40 +1,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 (
<div className="body">
<video controls poster={this.state.thumb} style={{maxHeight: this.state.size.h, maxWidth: this.state.size.w}}>
<source src={this.state.full}></source>
</video>
</div>
)
}
})
displayName: "m.video",
getInitialState: function() {
let event = this.props.event;
if (event.content.url == undefined) {
return null;
}
return mediaLib.parseEvent(this.props.client, event, 1000, 1000);
},
render: function() {
let event = this.props.event;
if (this.state == null) {
return "malformed video event: " + event.content.body;
}
return (
<div className="body">
<video controls poster={this.state.thumb} style={{maxHeight: this.state.size.h, maxWidth: this.state.size.w}}>
<source src={this.state.full}></source>
</video>
</div>
);
}
});
module.exports = Event;

@ -1,46 +1,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 (
<div className="fileUpload">
<input type="file" id="fileUpload" multiple ref={this.setFileRef} />
<label htmlFor="fileUpload"><img src="/icons/file.svg"/></label>
</div>
)
}
})
render: function() {
return (
<div className="fileUpload">
<input type="file" id="fileUpload" multiple ref={this.setFileRef} />
<label htmlFor="fileUpload"><img src="/icons/file.svg"/></label>
</div>
);
}
});
module.exports = fileUpload
module.exports = fileUpload;

@ -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 <>
<input className="filter" ref={this.inputRef} placeholder="Search"/>
<div className="list">
{items}
{items}
</div>
</>
}
})
</>;
}
});
module.exports = FilterList
module.exports = FilterList;

@ -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 <div className="info">{title}</div>
}
})
displayName: "Info",
render: function() {
let title = "";
if (this.props.room != undefined) {
title = this.props.room.name;
}
return <div className="info">{title}</div>;
}
});
module.exports = info
module.exports = info;

@ -1,277 +1,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 <div className="input">
{this.props.replyEvent &&
render: function() {
return <div className="input">
{this.props.replyEvent &&
<div className="replyEvent" onClick={() => this.props.onReplyClick()}>
{this.props.replyEvent.plaintext()}
{this.props.replyEvent.plaintext()}
</div>
}
{this.state.uploads.length > 0 &&
}
{this.state.uploads.length > 0 &&
<div className="imgPreview">
{this.state.uploads.map((upload, key) => {
return (
<div key={key}>
<img src={upload.preview}/>
<span onClick={() => this.removeUpload(key)}>X</span>
</div>
)
})}
{this.state.uploads.map((upload, key) => {
return (
<div key={key}>
<img src={upload.preview}/>
<span onClick={() => this.removeUpload(key)}>X</span>
</div>
);
})}
</div>
}
<div className="content">
<textarea ref={this.setRef} rows="1" spellCheck="false" placeholder="unencrypted message"></textarea>
<FileUpload addUpload={this.addUpload}/>
</div>
</div>
}
})
}
<div className="content">
<textarea ref={this.setRef} rows="1" spellCheck="false" placeholder="unencrypted message"></textarea>
<FileUpload addUpload={this.addUpload}/>
</div>
</div>;
}
});
function handleCommands(command, parts) {
if (command == "/rainbow") {
if (parts.length < 2) {
return
}
let string = parts[1]
for(let i=2; i < parts.length; i++) {
string += " " + parts[i]
}
let html = rainbowTransform(string)
return {
type: 'html',
content: html
}
}
return null
if (command == "/rainbow") {
if (parts.length < 2) {
return;
}
let string = parts[1];
for(let i=2; i < parts.length; i++) {
string += " " + parts[i];
}
let html = rainbowTransform(string);
return {
type: 'html',
content: html
};
}
return null;
}
function rainbowTransform(text) {
let array = text.split("");
let delta = 360/text.length;
if (delta < 10) {
delta = 10;
} else if (delta > 20) {
delta = 20;
}
let h = -1 * delta; // start at beginning
let array = text.split("");
let delta = 360/text.length;
if (delta < 10) {
delta = 10;
} else if (delta > 20) {
delta = 20;
}
let h = -1 * delta; // start at beginning
let rainbowArray = array.map((char) => {
h = h + delta;
if (h > 360) {
h = 0;
}
return `<font color="${colorConvert.hsl.hex(h, 100, 50)}">${char}</font>`;
});
let rainbow = rainbowArray.join("");
return rainbow;
let rainbowArray = array.map((char) => {
h = h + delta;
if (h > 360) {
h = 0;
}
return `<font color="${colorConvert.hsl.hex(h, 100, 50)}">${char}</font>`;
});
let rainbow = rainbowArray.join("");
return rainbow;
}
module.exports = input
module.exports = input;

@ -1,20 +1,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 (
<div className="spinner">
<div className="bounce1"/>
<div className="bounce2"/>
<div className="bounce3"/>
</div>
)
}
})
render: function() {
return (
<div className="spinner">
<div className="bounce1"/>
<div className="bounce2"/>
<div className="bounce3"/>
</div>
);
}
});
module.exports = Loading
module.exports = Loading;

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

@ -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}
};
}
}
};

@ -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 };
},
},
}
};

@ -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]);
});
}

Loading…
Cancel
Save