Update client deps: react 16.3, babel 7

This commit is contained in:
Ken-Håvard Lieng 2018-04-05 21:13:32 +02:00
parent 1ae7d867a9
commit 0cbbc1b8ff
46 changed files with 1125 additions and 808 deletions

View file

@ -0,0 +1,92 @@
import Backoff from 'backo';
export default class Socket {
constructor(host) {
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
this.url = `${protocol}://${host}/ws${window.location.pathname}`;
this.connectTimeout = 20000;
this.pingTimeout = 30000;
this.backoff = new Backoff({
min: 1000,
max: 5000,
jitter: 0.25
});
this.handlers = [];
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.timeoutConnect = setTimeout(() => {
this.ws.close();
this.retry();
}, this.connectTimeout);
this.ws.onopen = () => {
this.emit('_connected', true);
clearTimeout(this.timeoutConnect);
this.backoff.reset();
this.setTimeoutPing();
};
this.ws.onclose = () => {
this.emit('_connected', false);
clearTimeout(this.timeoutConnect);
clearTimeout(this.timeoutPing);
if (!this.closing) {
this.retry();
}
this.closing = false;
};
this.ws.onerror = () => {
clearTimeout(this.timeoutConnect);
clearTimeout(this.timeoutPing);
this.closing = true;
this.ws.close();
this.retry();
};
this.ws.onmessage = (e) => {
this.setTimeoutPing();
const msg = JSON.parse(e.data);
if (msg.type === 'ping') {
this.send('pong');
}
this.emit(msg.type, msg.data);
};
}
retry() {
setTimeout(() => this.connect(), this.backoff.duration());
}
send(type, data) {
this.ws.send(JSON.stringify({ type, data }));
}
setTimeoutPing() {
clearTimeout(this.timeoutPing);
this.timeoutPing = setTimeout(() => {
this.closing = true;
this.ws.close();
this.connect();
}, this.pingTimeout);
}
onMessage(handler) {
this.handlers.push(handler);
}
emit(type, data) {
for (let i = 0; i < this.handlers.length; i++) {
this.handlers[i](type, data);
}
}
}

View file

@ -0,0 +1,56 @@
import React from 'react';
import linkify from '../linkify';
describe('linkify()', () => {
const proto = href => href.indexOf('http') !== 0 ? `http://${href}` : href;
const linkTo = href => <a href={proto(href)} rel="noopener noreferrer" target="_blank">{href}</a>;
it('returns the arg when no matches are found', () => [
null,
undefined,
10,
false,
true,
'just some text',
''
].forEach(input => expect(linkify(input)).toBe(input)));
it('linkifies text', () => Object.entries({
'google.com': linkTo('google.com'),
'google.com stuff': [
linkTo('google.com'),
' stuff'
],
'cake google.com stuff': [
'cake ',
linkTo('google.com'),
' stuff'
],
'cake google.com stuff https://google.com': [
'cake ',
linkTo('google.com'),
' stuff ',
linkTo('https://google.com')
],
'cake google.com stuff pie https://google.com ': [
'cake ',
linkTo('google.com'),
' stuff pie ',
linkTo('https://google.com'),
' '
],
' google.com': [
' ',
linkTo('google.com')
],
'google.com ': [
linkTo('google.com'),
' '
],
'/google.com?': [
'/',
linkTo('google.com'),
'?'
]
}).forEach(([ input, expected ]) => expect(linkify(input)).toEqual(expected)));
});

View file

@ -0,0 +1,8 @@
export default function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (Object.prototype.hasOwnProperty.call(handlers, action.type)) {
return handlers[action.type](state, action);
}
return state;
};
}

View file

@ -0,0 +1,64 @@
import padStart from 'lodash/padStart';
export { findBreakpoints, messageHeight } from './messageHeight';
export { default as linkify } from './linkify';
export function normalizeChannel(channel) {
if (channel.indexOf('#') !== 0) {
return channel;
}
return channel.split('#').join('').toLowerCase();
}
export function isChannel(name) {
// TODO: Handle other channel types
return typeof name === 'string' && name[0] === '#';
}
export function timestamp(date = new Date()) {
const h = padStart(date.getHours(), 2, '0');
const m = padStart(date.getMinutes(), 2, '0');
return `${h}:${m}`;
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
export function stringWidth(str, font) {
ctx.font = font;
return ctx.measureText(str).width;
}
export function measureScrollBarWidth() {
const outer = document.createElement('div');
outer.style.visibility = 'hidden';
outer.style.width = '100px';
document.body.appendChild(outer);
const widthNoScroll = outer.offsetWidth;
outer.style.overflow = 'scroll';
const inner = document.createElement('div');
inner.style.width = '100%';
outer.appendChild(inner);
const widthWithScroll = inner.offsetWidth;
outer.parentNode.removeChild(outer);
return widthNoScroll - widthWithScroll;
}
export function find(arr, pred) {
if (!arr) {
return null;
}
for (let i = 0; i < arr.length; i++) {
if (pred(arr[i])) {
return arr[i];
}
}
}

View file

@ -0,0 +1,59 @@
import Autolinker from 'autolinker';
import React from 'react';
const autolinker = new Autolinker({
stripPrefix: false,
stripTrailingSlash: false
});
export default function linkify(text) {
let matches = autolinker.parseText(text);
if (matches.length === 0) {
return text;
}
const result = [];
let pos = 0;
matches = autolinker.compactMatches(matches);
for (let i = 0; i < matches.length; i++) {
const match = matches[i];
if (match.getType() === 'url') {
if (match.offset > pos) {
if (typeof result[result.length - 1] === 'string') {
result[result.length - 1] += text.slice(pos, match.offset);
} else {
result.push(text.slice(pos, match.offset));
}
}
result.push(
<a target="_blank" rel="noopener noreferrer" href={match.getAnchorHref()}>
{match.matchedText}
</a>
);
} else if (typeof result[result.length - 1] === 'string') {
result[result.length - 1] += text.slice(pos, match.offset + match.matchedText.length);
} else {
result.push(text.slice(pos, match.offset + match.matchedText.length));
}
pos = match.offset + match.matchedText.length;
}
if (pos < text.length) {
if (typeof result[result.length - 1] === 'string') {
result[result.length - 1] += text.slice(pos);
} else {
result.push(text.slice(pos));
}
}
if (result.length === 1) {
return result[0];
}
return result;
}

View file

@ -0,0 +1,52 @@
const lineHeight = 24;
const userListWidth = 200;
const smallScreen = 600;
export function findBreakpoints(text) {
const breakpoints = [];
for (let i = 0; i < text.length; i++) {
const char = text.charAt(i);
if (char === ' ') {
breakpoints.push({ end: i, next: i + 1 });
} else if (char === '-' && i !== text.length - 1) {
breakpoints.push({ end: i + 1, next: i + 1 });
}
}
return breakpoints;
}
export function messageHeight(message, wrapWidth, charWidth, indent = 0, windowWidth) {
let pad = (6 + (message.from ? message.from.length + 1 : 0)) * charWidth;
let height = lineHeight + 8;
if (message.channel && windowWidth > smallScreen) {
wrapWidth -= userListWidth;
}
if (pad + (message.length * charWidth) < wrapWidth) {
return height;
}
const breaks = message.breakpoints;
let prevBreak = 0;
let prevPos = 0;
for (let i = 0; i < breaks.length; i++) {
if (pad + ((breaks[i].end - prevBreak) * charWidth) >= wrapWidth) {
prevBreak = prevPos;
pad = indent;
height += lineHeight;
}
prevPos = breaks[i].next;
}
if (pad + ((message.length - prevBreak) * charWidth) >= wrapWidth) {
height += lineHeight;
}
return height;
}

View file

@ -0,0 +1,111 @@
function subscribeArray(store, selectors, handler, init) {
let state = store.getState();
let prev = selectors.map(selector => selector(state));
if (init) {
handler(...prev);
}
return store.subscribe(() => {
state = store.getState();
const next = [];
let changed = false;
for (let i = 0; i < selectors.length; i++) {
next[i] = selectors[i](state);
if (next[i] !== prev[i]) {
changed = true;
}
}
if (changed) {
handler(...next);
prev = next;
}
});
}
function subscribe(store, selector, handler, init) {
if (Array.isArray(selector)) {
return subscribeArray(store, selector, handler, init);
}
let prev = selector(store.getState());
if (init) {
handler(prev);
}
return store.subscribe(() => {
const next = selector(store.getState());
if (next !== prev) {
handler(next);
prev = next;
}
});
}
//
// Handler gets called every time the selector(s) change
//
export function observe(store, selector, handler) {
return subscribe(store, selector, handler, true);
}
//
// Handler gets called once the next time the selector(s) change
//
export function once(store, selector, handler) {
let done = false;
const unsubscribe = subscribe(store, selector, (...args) => {
if (!done) {
done = true;
handler(...args);
}
unsubscribe();
});
}
//
// Handler gets called once when the predicate returns true, the predicate gets passed
// the result of the selector(s), if no predicate is set it defaults to checking if the
// selector(s) return something truthy
//
export function when(store, selector, predicate, handler) {
if (arguments.length === 3) {
handler = predicate;
if (Array.isArray(selector)) {
predicate = (...args) => {
for (let i = 0; i < args.length; i++) {
if (!args[i]) {
return false;
}
}
return true;
};
} else {
predicate = o => o;
}
}
const state = store.getState();
if (Array.isArray(selector)) {
const val = selector.map(s => s(state));
if (predicate(...val)) {
return handler(...val);
}
} else {
const val = selector(state);
if (predicate(val)) {
return handler(val);
}
}
let done = false;
const unsubscribe = subscribe(store, selector, (...args) => {
if (!done && predicate(...args)) {
done = true;
handler(...args);
}
unsubscribe();
});
}

View file

@ -0,0 +1,107 @@
import createHistory from 'history/createBrowserHistory';
import UrlPattern from 'url-pattern';
const history = createHistory();
export const LOCATION_CHANGED = 'ROUTER_LOCATION_CHANGED';
export const PUSH = 'ROUTER_PUSH';
export const REPLACE = 'ROUTER_REPLACE';
export function locationChanged(route, params, location) {
return {
type: LOCATION_CHANGED,
route,
params,
location
};
}
export function push(path) {
return {
type: PUSH,
path
};
}
export function replace(path) {
return {
type: REPLACE,
path
};
}
export function routeReducer(state = {}, action) {
if (action.type === LOCATION_CHANGED) {
return {
route: action.route,
params: action.params,
location: action.location
};
}
return state;
}
export function routeMiddleware() {
return next => action => {
switch (action.type) {
case PUSH:
history.push(action.path);
break;
case REPLACE:
history.replace(action.path);
break;
default:
return next(action);
}
};
}
function decode(location) {
location.pathname = decodeURIComponent(location.pathname);
return location;
}
function match(routes, location) {
let params;
for (let i = 0; i < routes.length; i++) {
params = routes[i].pattern.match(location.pathname);
if (params !== null) {
const keys = Object.keys(params);
for (let j = 0; j < keys.length; j++) {
params[keys[j]] = decodeURIComponent(params[keys[j]]);
}
return locationChanged(routes[i].name, params, decode(location));
}
}
return null;
}
export default function initRouter(routes, store) {
const patterns = [];
const opts = {
segmentValueCharset: 'a-zA-Z0-9-_.%'
};
Object.keys(routes).forEach(name =>
patterns.push({
name,
pattern: new UrlPattern(routes[name], opts)
})
);
let matched = match(patterns, history.location);
if (matched) {
store.dispatch(matched);
} else {
matched = { location: {} };
}
history.listen(location => {
const nextMatch = match(patterns, location);
if (nextMatch && nextMatch.location.pathname !== matched.location.pathname) {
matched = nextMatch;
store.dispatch(matched);
}
});
}

View file

@ -0,0 +1,12 @@
const positions = {};
export function getScrollPos(key) {
if (key in positions) {
return positions[key];
}
return -1;
}
export function saveScrollPos(key, pos) {
positions[key] = pos;
}