Update client deps: react 16.3, babel 7
This commit is contained in:
parent
1ae7d867a9
commit
0cbbc1b8ff
46 changed files with 1125 additions and 808 deletions
92
client/src/js/utils/Socket.js
Normal file
92
client/src/js/utils/Socket.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
56
client/src/js/utils/__tests__/util.test.js
Normal file
56
client/src/js/utils/__tests__/util.test.js
Normal 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)));
|
||||
});
|
8
client/src/js/utils/createReducer.js
Normal file
8
client/src/js/utils/createReducer.js
Normal 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;
|
||||
};
|
||||
}
|
64
client/src/js/utils/index.js
Normal file
64
client/src/js/utils/index.js
Normal 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];
|
||||
}
|
||||
}
|
||||
}
|
59
client/src/js/utils/linkify.js
Normal file
59
client/src/js/utils/linkify.js
Normal 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;
|
||||
}
|
52
client/src/js/utils/messageHeight.js
Normal file
52
client/src/js/utils/messageHeight.js
Normal 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;
|
||||
}
|
111
client/src/js/utils/observe.js
Normal file
111
client/src/js/utils/observe.js
Normal 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();
|
||||
});
|
||||
}
|
107
client/src/js/utils/router.js
Normal file
107
client/src/js/utils/router.js
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
12
client/src/js/utils/scrollPosition.js
Normal file
12
client/src/js/utils/scrollPosition.js
Normal 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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue